diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index b71e748ea7..0000000000 --- a/.gitattributes +++ /dev/null @@ -1,9 +0,0 @@ -.gitattributes export-ignore -.github/ export-ignore -.gitignore export-ignore -AGENTS.md export-ignore -docs/ export-ignore -tests/ export-ignore - -*.php* diff=php -*.sh text eol=lf diff --git a/.github/funding.yml b/.github/funding.yml deleted file mode 100644 index 25adc95203..0000000000 --- a/.github/funding.yml +++ /dev/null @@ -1,2 +0,0 @@ -github: dg -custom: "https://nette.org/donate" diff --git a/.github/workflows/coding-style.yml b/.github/workflows/coding-style.yml deleted file mode 100644 index 27ec1b2828..0000000000 --- a/.github/workflows/coding-style.yml +++ /dev/null @@ -1,17 +0,0 @@ -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/.gitignore b/.gitignore index 06f62a2abd..e69de29bb2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +0,0 @@ - -doc/.DS_Store - -.DS_Store diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 6f6b53c17f..0000000000 --- a/AGENTS.md +++ /dev/null @@ -1,343 +0,0 @@ -# To My Agents! - -It is my fervent wish that this file guide every AI coding agent working with code in this repository. - -## Project Overview - -This is the official documentation repository for the Nette PHP framework ecosystem (`github.com/nette/docs`). It holds the user manual for 30 packages, written in Texy markup and published at nette.org, doc.nette.org, latte.nette.org, tester.nette.org and tracy.nette.org. - -Pages are authored in English and Czech. From those two sources they are translated into eight further languages: `de`, `es`, `fr`, `it`, `ja`, `pl`, `ru`, `tr`. - -## CRITICAL: Language Version Rules - -**All edits MUST go only into the `/cs/` and `/en/` versions, and always into both at once.** - -Everything below follows from a single mechanism: the translation system maps content between languages **line by line**. Break that correspondence and the mapping silently produces wrong translations in every other mutation. - -1. **Edit only `/cs/` and `/en/`** — never touch `de`, `es`, `fr`, `it`, `ja`, `pl`, `ru`, `tr`. Those are generated, and edits there would be overwritten. -2. **Edit both versions in the same change** — a change that lands in only one of the pair is an incomplete change. -3. **Keep perfect line alignment** — both files must end up with the same number of lines, and the same information must sit on the same line number. Line 14 of `application/en/presenters.texy` corresponds to line 14 of `application/cs/presenters.texy`. -4. **Keep the same file set** — `/cs/` and `/en/` must contain exactly the same `.texy` files. Adding a page means creating it in both. -5. **Verify before committing** — `wc -l package/en/file.texy package/cs/file.texy` must report identical counts. - -Adding or removing a line therefore always means adding or removing it in both files simultaneously, blank lines included. If a sentence needs one extra line in Czech, the English version needs a matching line as well. - -## Documentation Structure - -Documentation is organized by package, language and article: - -``` -/ -├── en/ # English version -├── cs/ # Czech version -├── de/, es/, ... # Generated language variants — do not edit -├── files/ # Shared images and assets -└── meta.json # Package metadata (documented version, repo, composer name) -``` - -The `version` field in `meta.json` says which version of the package the pages describe. It is the baseline for `.{data-version}` markers, see [below](#texy-documentation-modifiers). - -### Package Inventory (30 packages) - -**Nette Framework** (https://doc.nette.org): -- `application/` - MVC framework, presenters, routing, components -- `assets/` - Asset management and Vite integration -- `bootstrap/` - Application initialization and configuration -- `caching/` - Caching mechanisms -- `command-line/` - Building CLI applications: switches, options, ANSI output -- `component-model/` - Component system and lifecycle -- `database/` - Database access layer, Explorer, transactions -- `dependency-injection/` - DI container, autowiring, services -- `forms/` - Form creation, validation, rendering -- `http/` - HTTP request/response, sessions, URLs -- `mail/` - Email sending -- `neon/` - NEON format parser -- `nette/` - Main documentation hub, glossary, installation -- `php-generator/` - PHP code generation -- `robot-loader/` - Automatic class loader -- `safe-stream/` - Safe stream handling -- `schema/` - Schema validation and generation -- `security/` - Authentication, authorization, passwords -- `tokenizer/` - String tokenization (library no longer developed) -- `utils/` - Arrays, strings, filesystem, validation, datetime - -**Key projects from the Nette ecosystem:** -- `ai/` - Nette AI (subdomain https://ai.nette.org) -- `latte/` - Templating engine v3.0+ with a `/cookbook/` subdirectory, the only package with a nested structure (subdomain https://latte.nette.org) -- `tester/` - Testing framework with assertions and TestCase (subdomain https://tester.nette.org) -- `tracy/` - Debugger, dumper, extensions (subdomain https://tracy.nette.org) - -**Supporting** (subdomain https://doc.nette.org): -- `best-practices/` - Development patterns and recipes -- `contributing/` - Contribution guidelines and documentation standards -- `migrations/` - Version upgrades -- `quickstart/` - Getting started tutorial -- `tools/` - Developer tooling: Code Checker, Coding Standard, PHPStan rules, IDE support, X-ray -- `www/` - Website content, logo, license, packages (subdomain https://nette.org) - -**Structure notes:** -- Every package has `@home.texy` (entry point). Most also have `@meta.texy` (metadata) and `@left-menu.texy` or `@menu.texy` (navigation). - -**Branch structure:** -- `master` - Current documentation (latest versions) -- `doc-3.x` - Version 3.x documentation -- `doc-2.x` - Legacy 2.x documentation - -## CRITICAL: Always Write Plain ASCII Quotes - -**In `.texy` files, always type the plain ASCII `"` and `'`. Never type typographic -quotes directly**, in any language — not `„…"`, not `«…»`, not `“…”`. - -Texy converts them to the correct typographic pair for the page's language during -rendering, driven by `TypographyModule::$locales` (17 languages: bg, cs, de, el, en, -es, fr, hu, it, ja, pl, pt, ro, ru, sl, tr, uk). The same ASCII source therefore -renders as `„text"` in German, `«text»` in Russian and `「text」` in Japanese. - -```texy -Mit "Initialisierung der Umgebung" meinen wir … ← correct, Texy handles it -Mit „Initialisierung der Umgebung" meinen wir … ← wrong, and broken on top -``` - -Why this is a rule and not a preference: - -- **Typing them by hand produces broken pairs.** The closing characters (U+201C, - U+2018) are unreliable to produce, so what usually comes out is an opening `„` - followed by an ASCII `"` — a mismatched pair that no tool flags. This really - happened in `application/de/how-it-works.texy` and had to be repaired by script. -- **Hardcoded quotes ignore the language.** A `„…"` typed into the Russian mutation - stays German-looking; Texy would have produced `«…»`. -- **It keeps the source diffable and greppable.** One byte per quote, no invisible - code points to normalize later. - -The same applies to code samples: keep whatever the English source uses, since inside -a code block Texy performs no typographic substitution at all. - -**The one exception** is the Texy documentation itself, where typographic quotes are the -subject matter and must appear literally: `texy/{cs,en}/configuration.texy` (the list of -locales), `texy/{cs,en}/syntax.texy` and `texy/cs/@home.texy`. Do not ASCII-ize those -lines. When touching them, take the pairs from `TypographyModule::$locales` — writing -them by hand produces broken pairs like `„text"`, which is exactly what those lines -suffered from before. Note that plain ASCII is equally wrong there: Texy would re-typeset -it to the *page's* locale, so a sample meant to show English quotes would come out with -Czech ones on the Czech page. Everywhere else the ASCII rule holds without exception. - -## Texy Documentation Modifiers - -Use these modifiers when documenting API elements in `.texy` files: - -**Methods** - use `[method]` modifier: -```texy -static fromBlank(int $width, int $height, ?ImageColor $color=null): Image .[method] ------------------------------------------------------------------------------------ -Creates a new true color image of the given dimensions. The default color is black. -``` - -**Prefer expressive array types over bare `array`.** In signatures, describe what an -array holds using phpDoc-style shapes that mirror the code's `@return`/`@param`: -- `getComponents(): IComponent[]` instead of `getComponents(): array` -- `getComponentTree(): list` (a numerically indexed list) instead of `: array` -- `array` for keyed maps - -Bare `array` is the least informative part of a signature. These phpDoc shapes are not -native PHP types, which is fine here: they only appear in documentation. - -**Latte filters** - use `[filter]` modifier: -```texy -batch(int $length, mixed $item): array .[filter] ------------------------------------------------- -Filter that simplifies outputting linear data in table form. -``` - -**New features** - use `{data-version:X.Y}` modifier with version number: -```texy -New great feature .{data-version:3.1.0} ---------------------------------------- -Description of the feature. -``` - -**Only mark versions NEWER than the documented baseline.** The baseline is the version -in the package's `meta.json`. A `.{data-version}` marker is only meaningful for a feature -added *after* the baseline, so the reader knows they need a newer patch/minor. - -**The baseline is the FLOOR of the documented version range, not the latest release.** -This is critical when `meta.json` is a whole series like `3.x`: -- `meta.json: 3.x` means the page serves readers on **any** 3.y — including old 3.0. - So a reader on 3.0 genuinely needs to know a feature arrived in 3.3.1. Therefore - **keep** every `.{data-version:3.y.z}` where `3.y.z > 3.0` — the floor is `3.0`, and - markers above the floor are meaningful. (E.g. `Using with PSR-16 .{data-version:3.3.1}` - on a `3.x` page is CORRECT and must stay.) -- `meta.json: 4.0` (a concrete version) has floor `4.0`. Then `.{data-version:4.0.0}` and - any `.{data-version:3.x.x}` are redundant (`<= 4.0`) and should be removed; keep only - markers for later releases (`4.0.6`, `4.1`, …). - -Rules of thumb: -- Do **not** add `.{data-version:X.0}` on a page whose baseline floor is `X.0`. -- Do **not** keep markers `<=` the baseline floor (e.g. `3.1.0` on a page documenting `4.0`). -- When bumping `meta.json` to a new major (e.g. `3.x` → `4.0`), the floor jumps from `3.0` - to `4.0`: **strip** all markers `<= 4.0`, keep only later releases. - -**Deprecated items** - use `[deprecated]` modifier (without version number): -```texy -static rgb(int $red, int $green, int $blue, int $transparency=0): array .[method][deprecated] ---------------------------------------------------------------------------------------------- -This function has been replaced by the `ImageColor` class. -``` - -**Combined modifiers** (e.g., new filter): -```texy -accept .[filter]{data-version:3.1.0} ------------------------------------- -Filter used during migration from Latte 3.0 to confirm behavior change acceptance. -``` - -**Chaining multiple modifier groups** — append additional `{...}` blocks without a leading dot (only the first one has `.`): -```texy -Národní prostředí .{data-version:3.0.18}{toc: Locale} ------------------------------------------------------ -``` - -**`.{data-version:X.Y.Z}` placements that work:** -- After a heading text (before the underline) — marks the whole section. -- On a list item, after an inline element: `` - `ClassName` .{data-version:3.2.9} - description `` — marks the item. -- On its own line before a paragraph as a block modifier — marks that paragraph. -- At the end of a paragraph, after the sentence period — Texy renders it correctly as a - `data-version` attribute of the paragraph (verified; there are existing precedents, e.g. - `dependency-injection/en/factory.texy`, `forms/en/in-presenter.texy`, `templates.texy`). - -## Headings and Anchors - -All headings automatically generate URL anchors based on their text. This allows linking directly to any section. - -**Automatic anchors:** -- Heading "Installing Claude Code" → anchor `#installing-claude-code` -- Heading "What's Next" → anchor `#what-s-next` - -**Linking to sections:** -```texy -See [installation guide |getting-started#installing-claude-code] for details. -``` - -**Custom anchors** - use `.{#anchor-name}` modifier when you need a specific anchor: -```texy -Installing Claude Code .{#Installing} -===================================== -``` - -**API signature headings** — a heading that documents a method or filter with a full -signature plus a `.[method]` or `.[filter]` modifier anchors to just the **member name**, -not the whole signature. The parameter list, return type, the modifier itself, and any -trailing `.{data-version:…}` are all stripped. Link to it with `|#name`: -```texy -map(callable $transformer): iterable .[filter]{data-version:3.1.6} → [… |#map] -beforeRender(Latte\Runtime\Template $template): void .[method] → [… |#beforeRender] -``` -The name keeps its original case (`#localDate`, `#beforeRender`), but links resolve -case-insensitively, so `|#localdate` works too. This special rule needs both the -parentheses and the `.[method]`/`.[filter]` modifier; a parenless heading like -`first .[filter]` falls back to the generic rule (strip from the first `.[` / `.{`), -which still yields `#first`. - -**Czech / diacritic anchors** — Texy slugifies Czech headings by stripping diacritics and lowercasing (e.g. "Automatické opakování" → `#automaticke-opakovani`). When linking, you may also write the original heading text after `#` (including diacritics) and Texy resolves it correctly — both forms work: -```texy -[see |transactions#automaticke-opakovani] -[viz |transactions#Automatické opakování] -``` - -Custom anchors are useful when: -- The automatic anchor would be too long or unclear -- You want a stable anchor that won't change if you rename the heading -- You need to match an existing link from another page - -## Documentation Guidelines - -Follow Nette's documentation standards: -- Start with simple concepts, progress to advanced topics -- Test all code examples for accuracy -- Use clear, concise language -- Minimal use of highlighting and special formatting -- Adhere to Nette's coding standard in code examples - -English is the primary language. Use DeepL Translator for translations, which will be reviewed by contributors. - -## Czech Terminology Conventions - -When writing Czech documentation, keep these technical terms in English (they are part of common Czech programming slang): -- `marker interface` (not "značkovací rozhraní") -- `deadlock`, `idle timeout`, `callback` - -Translate domain terms that have established Czech equivalents: -- "serialization failure" → "serializační konflikt" -- "outermost transaction" → "vnější transakce" (not "nejvyšší") -- "transient error" → "přechodná chyba" - -## Writing Style - -Nette documentation is known for its **friendly, approachable language** that remains **technically precise**. This style is a core part of the Nette brand and must be maintained across all documentation. - -### Key Principles - -1. **Friendly and approachable** – Write as if explaining to a colleague, not writing a technical manual -2. **Completely understandable** – No assumed knowledge; explain every concept when first introduced -3. **Technically accurate** – Use precise terminology and correct examples -4. **Only brief where clarity allows** – Never sacrifice understanding for brevity - -### Good vs Bad Examples - -**Good example:** -> MCP Inspector allows AI to look directly at your application – to see what services you have registered, what tables are in your database, and what routes lead where. Without this, the AI would have to guess based on patterns it learned during training. - -**Bad example:** -> MCP Inspector provides runtime introspection via DI container, database schema, and router inspection tools. - -The good example explains what the tool does and why it matters. The bad example is technically correct but assumes the reader already understands the concepts. - -### Tone Guidelines - -- Use "you" and "your" to address the reader directly -- Use "we" when walking through steps together ("Let's start by...") -- Explain the "why" not just the "what" -- Use concrete examples instead of abstract descriptions -- Anticipate questions and answer them proactively -- Avoid jargon; when technical terms are necessary, explain them - -### Structure Guidelines - -- Start each page with a `.[perex]` or `
` (for multiple paragraphs) summary that explains what the reader will learn -- Use clear, descriptive headings that tell the reader what each section contains -- Break complex topics into digestible sections -- Use code examples liberally – they're often clearer than prose -- End sections with "What's Next" links when appropriate -- Put the `{{maintitle}}` and `{{description}}` directives at the very end of the page, in this order, after a blank line - -## Mandatory Self-Review After Every Change - -**Every time you add or rewrite text in the documentation, you MUST afterwards critically -review that text and act on your own critique.** Writing the text is only the first step; -the change is not finished until you have reviewed it and incorporated the resulting -improvements. - -This applies to **both original writing and translation**: - -1. **Original text (new or rewritten)** – After writing, critically evaluate it against the - Writing Style and Documentation Guidelines above: Is it clear and completely - understandable? Technically accurate? Friendly but precise? Free of jargon and - redundancy? Does it follow the ASCII quotes rule? Then rewrite to fix every weakness - you found. -2. **Translations** – After translating a text, critically evaluate the **quality of the - translation**: Does it faithfully convey the meaning of the source? Does it read naturally - in the target language (not like a machine translation)? Is terminology consistent with the - conventions above (e.g. Czech Terminology Conventions)? Then incorporate your findings and - correct the translation. - -Do not present a change as complete, and do not stop, until this review-and-fix pass has been -done. When the change is non-trivial, briefly summarize what the self-review found and what -you improved. - -## Before Committing - -1. Verify the `/cs/` and `/en/` pair has identical line counts. -2. Run the Nette Code Checker, which validates whitespace, encoding and BOM across the repository. It also runs in CI on every push and pull request: - ```sh - composer create-project nette/code-checker code-checker - code-checker/code-checker - ``` diff --git a/ai/cs/@home.texy b/ai/cs/@home.texy deleted file mode 100644 index 43cd8ce528..0000000000 --- a/ai/cs/@home.texy +++ /dev/null @@ -1,98 +0,0 @@ -Vibe coding s Nette -******************* - -
- -AI agent vám za pár sekund napíše sto řádků PHP. Jestli budou opravdu fungovat, o tom rozhoduje něco, co si agent sám obstarat nedokáže: základ, který si umí přečíst a ověřit a na kterém může stavět. - -Nette ho buduje léta, už v době, kdy to vypadalo jako detail pro fajnšmekry. - -- **Otypované do morku kostí** - agent typy čte, místo aby je hádal -- **Jasné konvence** - ví, kam která třída patří -- **Uzavřená smyčka** - vidí vaši skutečnou aplikaci a kontroluje si vlastní práci - -
- - -IDE bylo pro člověka, analýza je pro stroj -========================================== - -Roky jsme nástroje milovali za našeptávač, za typ, který vyskočí při najetí myší, za skok na definici. To všechno je ale pomoc pro lidské oko a ruku. Agent obrazovku nečte. Potřebuje informaci, kterou si přečte jako text a hlavně si ji umí ověřit sám. - -V tom je celý rozdíl mezi IDE a statickou analýzou. PhpStorm vám decentně podtrhne místo, kde možná předáváte `null`. PHPStan tutéž věc napíše do konzole jako chybu, kterou agent přečte, pochopí a opraví, aniž byste u toho museli sedět. - -Otázka proto už nezní "jak hezky mi editor popíše tohle API?", ale "kolik toho o sobě říká sám kód?" - - -Nette toho říká hodně -===================== - -Když PHP 7.1 doplnilo typový systém o nullable typy, bylo Nette vůbec prvním full-stack frameworkem, který si otypoval celé své API, včetně `declare(strict_types=1)` v každém souboru. Jakmile PHP 7.4 přidalo typované property, Nette je převzalo. Co uměl jazyk vyjádřit nativně, to Nette používalo, většinou dřív než ostatní. - -A tam, kde nativní typy nestačí, nastupují anotace. PHP neumí zapsat "neprázdný seznam řetězců" ani "návratová hodnota závisí na argumentu". Nette obojí zapíše, přesně tam, kde na tom záleží: - -```php -$data = $form->getValues(RegistrationData::class); -echo $data->email; // $data je RegistrationData, plně otypované -``` - -Žádný nativní typ PHP tohle nezvládne: "když ti předám jméno třídy, vrátíš mi instanci té třídy". Podmíněný návratový typ to zvládne a přečte si ho statická analýza, vaše IDE i agent. Napíšete `$data->emial` a chybu máte ještě před spuštěním. - -Stejná péče prostupuje celým frameworkem: array shapes, `class-string`, `positive-int`, `non-empty-string`. **Všechny balíčky Nette procházejí PHPStanem na plný level 8**, ne jen části, ne s hromadou výjimek. Stavíte na základu otypovaném tak důsledně, jak to jen jde. A na dobře otypovaném základu se snadno staví dobře otypovaná aplikace. - -.[note] -Celý příběh najdete v článku [AI agent je tak dobrý, jak dobré jsou vaše typy |https://blog.nette.org/cs/agenti-a-typova-kontrola]. - - -Celé je to o smyčce -=================== - -Agent je přesně tak dobrý, jak dobrá je zpětná vazba, kterou dostává. A ta nejlepší je smyčka. Něco napíše, spustí statickou analýzu a testy, přečte si výsledek, opraví chybu a zkusí to znovu. Napiš, ověř, oprav, opakuj. Tomuhle cyklu se říká agentní smyčka a je to okamžik, kdy se z generátoru textu stane něco, co kód opravdu doladí do funkčního stavu. - -Nette mu dá do ruky všechno, co k tomu potřebuje: - -- **PHPStan** s [rozšířením pro Nette |tools:phpstan-rules], které analyzátor naučí specifika frameworku: ví, že `$this['menu']` je vaše `MenuControl`, odvodí typ prvku z `addText()`, rozumí `#[Inject]` -- **[Nette Tester |tester:]** na testy. Zatím žádné nemáte? Nechte si je od agenta vygenerovat jako úplně první věc, protože bez nich nemá jak poznat, že něco rozbil. -- **Automatický lint** PHP, Latte, NEON, JSON a JavaScriptu po každé jednotlivé úpravě, takže rozbitá šablona se nikdy nedostane až do prohlížeče -- **Automatické formátování** PHP podle vašeho kódovacího standardu nástrojem [DressCode |tools:coding-standard], který agentovi řekne, co změnil, takže agent může pokračovat další úpravou, aniž by soubor četl znovu - - -Vaše aplikace, ne odhad -======================= - -Požádejte libovolnou AI, ať "vygeneruje entitu pro tabulku product", a bude vaše sloupce hádat podle vzorů, které pochytila při trénování. Jenže sloupec s cenou se u vás jmenuje `unit_price` a je tam `currency_id`, o kterém jste se nezmínili. - -**[MCP Inspector |mcp-inspector]** hádání odstraní. Je to server, díky kterému se agent podívá přímo do vaší běžící aplikace: na služby v DI kontejneru, na skutečné schéma databáze, na routy, na logy Tracy. Všechno je výhradně pro čtení a funguje to v každém editoru, který podporuje Model Context Protocol: Claude Code, Cursor, VS Code a další. - -Mění se i ladění. Místo kopírování stack trace do chatu řeknete "koukni do logu a zjisti, co se rozbilo", a agent si výjimku přečte sám. - - -Agent, který zná Nette -====================== - -**[Pluginy pro Claude Code a Codex |agent-plugins]** dodají to, co ve vašem kódu nikde nenajde: jak se věci v Nette dělají. Celá rodina skills pokrývá architekturu, formuláře, databázi, komponenty, bezpečnost, Latte, NEON a další. Každý se zapne sám, jakmile na jeho téma přijde řeč. Zeptáte se na validaci formuláře a naskočí skill pro formuláře. Zeptáte se na BlueScreen a naskočí ten pro Tracy. - -Výsledek vypadá, jako by ho psal někdo, kdo Nette dělá roky, ne jako obecné PHP poslepované z útržků. - - -Jak začít -========= - -Celé nastavení zabere zhruba deset minut: - -1. [Nainstalujte si AI nástroj |getting-started], doporučujeme Claude Code -2. [Přidejte pluginy pro Nette |agent-plugins], aby agent znal framework -3. [Nastavte MCP Inspector |mcp-inspector], aby znal *vaši* aplikaci - -Pak už jen popište, co chcete, zavřete agenta do smyčky a nechte ho pracovat. Až budete chtít z agenta vytěžit víc, přečtěte si [Vibe coding v praxi |vibe-coding]. - - -Když má s AI pracovat i vaše aplikace -===================================== - -Celá tahle stránka je o AI, která pomáhá psát kód. Uvnitř vaší aplikace ale může AI dělat docela jinou práci: shrnout článek, roztřídit příchozí dotazy, vytáhnout z faktury údaje do databáze. - -Na to je [AI Access |https://ai-access.deegee.dev], PHP knihovna s jedním rozhraním pro OpenAI, Claude, Gemini, DeepSeek i Grok. Přepnutí mezi nimi je změna jediného řádku. A protože nemá žádné závislosti, nepřitáhne vám do projektu SDK od výrobce ani konflikty verzí. - -{{maintitle: Vibe coding s Nette – PHP framework, kterému AI agent rozumí}} -{{description: AI agenti hádají přesně tolik, kolik jim váš kód dovolí. Nette je otypované do morku kostí, prochází PHPStanem na level 8 a dává agentovi ověřitelnou smyčku.}} diff --git a/ai/cs/@left-menu.texy b/ai/cs/@left-menu.texy deleted file mode 100644 index ca4b062c10..0000000000 --- a/ai/cs/@left-menu.texy +++ /dev/null @@ -1,5 +0,0 @@ -- [Úvod |@home] -- [Začínáme |getting-started] -- [Pluginy pro agenty |agent-plugins] -- [MCP Inspector |mcp-inspector] -- [Vibe coding v praxi |vibe-coding] diff --git a/ai/cs/@meta.texy b/ai/cs/@meta.texy deleted file mode 100644 index e06cc9886c..0000000000 --- a/ai/cs/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette AI}} diff --git a/ai/cs/guide.texy b/ai/cs/guide.texy deleted file mode 100644 index 3ec981a90e..0000000000 --- a/ai/cs/guide.texy +++ /dev/null @@ -1 +0,0 @@ -{{redirect: @home}} diff --git a/ai/cs/mcp-inspector.texy b/ai/cs/mcp-inspector.texy deleted file mode 100644 index 90a4755d4c..0000000000 --- a/ai/cs/mcp-inspector.texy +++ /dev/null @@ -1,328 +0,0 @@ -MCP Inspector -************* - -
- -"MCP Inspector":https://github.com/nette/mcp-inspector umožní AI asistentovi podívat se přímo do vaší Nette aplikace: vidí, jaké služby máte registrované v DI kontejneru, jak vypadají tabulky v databázi, která routa vede kam a co Tracy zalogovala v noci. Dozvíte se: - -- jak inspektor funguje a co všechno vidí -- jak ho nainstalovat dvěma příkazy -- co dělá který nástroj a jak si ho vyzkoušet z terminálu -- jak držet AI na krátkém vodítku: dotazy jen pro čtení, maskovaná hesla, vypínač - -
- -Bez inspektoru AI vaši aplikaci odhaduje podle vzorů, které pochytila při tréninku. S ním se AI zeptá vaší aplikace a dostane pravdu: skutečné sloupce, skutečné názvy služeb, skutečnou chybu. - -.[caution] -MCP Inspector je stále v **rané fázi vývoje a nemá zatím stabilní vydání**. Do prvního vydání ho instalujte příkazem `composer require --dev nette/mcp-inspector:@dev` a počítejte s tím, že se názvy nástrojů i konfigurace budou ještě měnit. - - -Jak to funguje -============== - -MCP Inspector je server mluvící **Model Context Protocolem (MCP)**, standardem, kterým AI nástroje jako Claude Code, Cursor nebo VS Code volají externí nástroje. Editor spustí inspektor jako proces na pozadí, a kdykoli AI potřebuje něco z vaší aplikace, zavolá některý z nástrojů inspektoru a dostane odpověď. - -Aby mohl odpovědět, sestaví inspektor DI kontejner vaší aplikace. Dělá to pomocí malého skriptu `mcp-bootstrap.php` v kořeni projektu, který vrací `Configurator` vaší aplikace se všemi přidanými konfigy; kontejner si inspektor vytvoří sám, v debug režimu a ve vlastním temp adresáři, takže se nikdy nedotkne cache vašeho webu. - -Kontejner zůstává mezi voláními živý, ale každé volání zkontroluje, zda se nezměnila konfigurace. Když upravíte `services.neon`, hned další volání nástroje vidí nové služby; restart editoru není potřeba. Pokud se přestavba nepovede, třeba kvůli překlepu v konfiguraci, inspektor dál obsluhuje poslední funkční kontejner a do výsledku přidá pole `_warning`, takže vám AI o selhání hned řekne. - -Vše je **ve výchozím stavu jen pro čtení**: inspektor čte vaše služby, schéma, routy a logy, ale nemůže měnit data ani konfiguraci a nikdy nespouští kód od AI. Jediná výjimka, spouštění modifikujícího SQL, je vypnutá, dokud ji výslovně [nepovolíte |#konfigurace-databaze]. - - -Instalace -========= - -Dva příkazy. První přidá balíček jako vývojovou závislost, druhý vygeneruje soubory, které inspektor potřebuje: - -```shell -composer require --dev nette/mcp-inspector:@dev -vendor/bin/mcp-inspector init -``` - -`init` vytvoří tři soubory a existující nikdy nepřepíše: - -| Soubor | Účel -|------|------ -| `mcp-bootstrap.php` | vrací `Nette\Bootstrap\Configurator` vaší aplikace (viz níže) -| `config/mcp-inspector.neon` | konfigurace inspektoru: co AI smí -| `.mcp.json` | registruje server `nette-inspector` pro Claude Code; stejný záznam dostanou `.cursor/mcp.json` a `.vscode/mcp.json`, pokud tyto adresáře existují - -Pak restartujte svůj AI nástroj (v Claude Code napište `/exit` a spusťte znovu `claude`): MCP servery se připojují při startu nástroje. - -Hodí se dvě volby. Když PHP neběží přímo na vašem počítači, předejte příkaz, který má AI nástroj použít: `--php="ddev exec php"`. Když projekt není aktuálním adresářem, přidejte `--project=CESTA`. - -Funguje to? Zeptejte se AI: - -``` -Jaké služby mám registrované v DI kontejneru? -``` - -Pokud odpověď vypíše skutečné služby vaší aplikace, máte hotovo. - - -Soubor mcp-bootstrap.php -======================== - -Inspektor potřebuje `Configurator` se všemi přidanými konfigy, ale *před* zavoláním `createContainer()`, protože kontejner si sestavuje sám. `init` se podívá na vaši třídu `App\Bootstrap` a soubor vygeneruje podle ní: - -- **Statická `App\Bootstrap::boot(): Configurator`** (klasický Web Project): soubor je prostě `return App\Bootstrap::boot();` -- **Objektový `Bootstrap` s `bootWebApplication(): Container`** (Web Project od roku 2024): přidejte metodu, která se zastaví před vytvořením kontejneru, a použijte ji na obou místech: - -```php -public function bootWebApplication(): Nette\DI\Container -{ - return $this->bootConfigurator()->createContainer(); -} - -public function bootConfigurator(): Configurator -{ - $this->initializeEnvironment(); - $this->setupContainer(); - return $this->configurator; -} -``` - -V `mcp-bootstrap.php` pak bude `return (new App\Bootstrap)->bootConfigurator();`. - -- **Vlastní bootstrap** (konstruktor s argumenty, multi-tenant aplikace a podobně): `init` zapíše šablonu s komentářem `TODO`, kterou doplníte. Volání `new Configurator` nechte uvnitř třídy `Bootstrap`, aby dál fungovala autodetekce `%appDir%` v Nette, která se dívá na soubor, jenž Configurator vytváří. Parametry se pohodlně předávají proměnnými prostředí nastavenými v `.mcp.json`: - -```php -$blog = getenv('BLOG') === 'phpfashion' ? App\Blog::PhpFashion : App\Blog::LaTrine; -return (new App\Bootstrap($blog))->bootConsoleConfigurator(); -``` - -Debug režim zapínat nemusíte; inspektor to udělá sám, protože v CLI se nikdy neautodetekuje a právě debug režim umožňuje živé načítání změn konfigurace. - - -Nástroje -======== - -Nástroje jsou seskupené podle toho, na co se dívají. Každá skupina se objeví jen tehdy, když má vaše aplikace odpovídající část: bez `nette/database` nejsou žádné nástroje `db_*` a AI nematou nástroje, které nemohou fungovat. - - -Aplikace --------- - -| Nástroj | Co dělá -|------|------ -| `app_get_info` | verze PHP a Nette, nainstalované balíčky Nette, adresáře a databázový driver - -AI má pokyn zavolat ho jako první, aby kód, který píše, odpovídal verzím, které skutečně používáte, například atributům PHP 8.3 nebo zvyklostem Nette 3.3. - - -DI kontejner ------------- - -| Nástroj | Co dělá -|------|------ -| `di_get_services` | vypíše služby s typy, tagy, aliasy a autowiringem, volitelně filtrované podřetězcem názvu nebo typu -| `di_get_service` | detaily jedné služby včetně toho, zda už byla vytvořena -| `di_find_by_type` | služby implementující třídu nebo rozhraní a kterou z nich vybere autowiring -| `di_find_by_tag` | služby nesoucí tag, s hodnotami tagu -| `di_get_parameter_names` | názvy parametrů, vnořené v tečkové notaci (`database.default.dsn`) -| `di_get_parameter` | hodnota jednoho parametru; tajemství (`password`, `token`, `dsn`, …) jsou maskována - -Když se zeptáte "jaké mám mailery?", AI zavolá `di_find_by_type("Nette\Mail\Mailer")` a vidí přesně to, co váš kontejner obsahuje. K dispozici jsou jen běhová data: inspektor zná typ, tagy a aliasy služby, ne výraz továrny ani volání `setup` z konfigurace. Tyto nástroje vyžadují `nette/di` 3.2.7 nebo novější; parametry navíc musí být exportované, a pokud máte v konfiguraci `di: export: parameters: no`, nástroje vám to řeknou. - - -Router ------- - -| Nástroj | Co dělá -|------|------ -| `router_get_routes` | všechny registrované routy s maskami, výchozími hodnotami a prefixy modulů -| `router_match_url` | který presenter a akce obsluhují URL, s parametry (např. `/article/123`) -| `router_generate_url` | URL pro presenter a akci, stejně jako to dělá `{link}` (např. `Article:show` s `{"id": 5}`) - -Inspektor běží bez HTTP požadavku, takže vaše aplikace nedokáže zjistit vlastní adresu tak, jak to dělá na webu. Řekněte jí ji v konfiguraci aplikace (`nette/http` 3.4): - -```neon -http: - baseUrl: https://example.com/ -``` - -Bez ní `router_generate_url` ohlásí chybu s návodem, co nastavit, a relativní URL předané do `router_match_url` se porovnávají vůči `http://localhost/`. - - -Databáze --------- - -| Nástroj | Co dělá -|------|------ -| `db_get_tables` | tabulky a pohledy -| `db_get_columns` | sloupce tabulky: typy, nullabilita, výchozí hodnoty, primární a cizí klíče -| `db_get_relationships` | vztahy přes cizí klíče mezi všemi tabulkami (belongsTo, hasMany) -| `db_get_indexes` | indexy tabulky -| `db_query` | spustí jeden SQL příkaz s hodnotami navázanými na zástupné znaky `?`; ve výchozím stavu jen pro čtení -| `db_explain_query` | spustí `EXPLAIN` nad dotazem `SELECT` - -Tahle skupina ukončí hádání o vašem schématu. "Vygeneruj entitu pro tabulku product" se změní ve volání `db_get_columns("product")` a entitu se sloupci, které skutečně máte. - -`db_query` dovolí AI podívat se i na data, třeba jaké hodnoty sloupec se stavem opravdu obsahuje. Ve výchozím stavu přijímá jen příkazy typu `SELECT` (`SELECT`, `SHOW`, `EXPLAIN`, `DESCRIBE`, `WITH`, `VALUES`, `TABLE`, jediný příkaz, žádné `INTO OUTFILE`) a na MySQL, PostgreSQL a SQLite je spouští uvnitř transakce jen pro čtení, takže cokoli, co by validátor přehlédl, odmítne sama databáze. Hodnoty sloupců, jejichž názvy vypadají na tajemství, jsou maskované a počet řádků je omezený. - - -Tracy ------ - -| Nástroj | Co dělá -|------|------ -| `tracy_get_log` | nejnovější záznamy logu podle úrovně (`exception` ve výchozím stavu, `error`, `warning`, …), každý s názvem svého reportu -| `tracy_get_report` | report výjimky tak, jak ho Tracy píše pro agenty: kód kolem výjimky, stack trace s argumenty a prostředí - -S těmito dvěma se mění podoba ladění. Místo kopírování stack trace do chatu řeknete "podívej se do logu a řekni mi, co se rozbilo", a AI si výjimku přečte sama. Adresář logů je ten, do kterého píše Tracy logger vaší aplikace, není co nastavovat. Markdownové reporty vyžadují Tracy 2.12 nebo novější; starší reporty jen v HTML se přečíst nedají. - - -Zkoušení nástrojů z terminálu -============================= - -K tomu, abyste viděli, co nástroj vrací, nepotřebujete AI. Příkaz `call` spustí nástroj přesně tak, jak by to udělal klient, a výsledek vypíše jako JSON: - -```shell -vendor/bin/mcp-inspector call app_get_info -vendor/bin/mcp-inspector call router_match_url '{"url": "/article/123"}' -vendor/bin/mcp-inspector call db_query '{"query": "SELECT * FROM product WHERE id = ?", "params": [1]}' -``` - -Je to nejrychlejší způsob, jak zkontrolovat bootstrap a konfiguraci a podívat se na to, co uvidí AI. Volby `--project`, `--bootstrap` a `--config` fungují i tady. - - -Konfigurace -=========== - -Inspektor je sám o sobě malá Nette aplikace a `config/mcp-inspector.neon` je konfigurace jeho vlastního DI kontejneru, s obvyklými sekcemi `parameters:`, `services:` a jednou sekcí pro každou skupinu nástrojů. Každá sekce je nepovinná; chybějící soubor znamená výchozí hodnoty. Tohle je soubor, který `init` vygeneruje: - -```neon -# Configuration of nette/mcp-inspector: a Nette DI config for the inspector's own container. -# The inspector reads it itself, do not add it to the application's configs. -# Every section is optional; missing keys use the defaults shown here. - -inspector: - # false keeps the inspector from starting at all - enabled: true - # tool names or patterns hidden from the agent, e.g. [db_*, tracy_get_log] - disableTools: [] - -database: - # true: only SELECT-like statements, run in a read-only transaction - # false: any statement, the agent can modify data - readOnly: true - # maximum number of rows returned by db_query - rowLimit: 100 -``` - -Inspektor si tento soubor čte sám; nepřidávejte ho do konfigurace své aplikace. Změny se projeví po restartu MCP serveru, který AI nástroj dělá spolu se svou session. - - -Skrývání nástrojů ------------------ - -`disableTools` přijímá názvy nástrojů nebo vzory s `*`. Nechcete, aby AI vůbec četla vaše data? Skryjte celou databázovou skupinu: - -```neon -inspector: - disableTools: [db_*] -``` - - -Konfigurace databáze --------------------- - -Jediná skutečná bezpečnostní otázka zní, zda AI smí měnit data, a ve výchozím stavu nesmí. Když to chcete, třeba na vývojové databázi, o kterou nejde, ochranu vypněte: - -```neon -database: - readOnly: false - rowLimit: 500 -``` - -S `readOnly: false` se spustí jakýkoli příkaz, včetně `UPDATE`, `DELETE` a DDL. AI nástroje jako Claude Code se vás pak před každým `db_query` zeptají na potvrzení, protože nástroj už o sobě netvrdí, že je jen pro čtení. - - -Jiné AI nástroje -================ - -MCP Inspector funguje s každým nástrojem, který mluví MCP. `init` ho zaregistruje pro Claude Code do `.mcp.json` a pro Cursor a VS Code, když v projektu najde jejich adresáře `.cursor` nebo `.vscode`. Pro jakýkoli jiný nástroj zaregistrujte příkaz, který spustí server přes standardní vstup a výstup: - -```json -{ - "mcpServers": { - "nette-inspector": { - "type": "stdio", - "command": "php", - "args": ["vendor/bin/mcp-inspector"] - } - } -} -``` - -Příkaz běží v kořeni projektu. Tam, kde to neplatí (některé editory spouštějí servery jinde), přidejte do argumentů `"--project=/cesta/k/projektu"`. Kde má konfigurační soubor ležet, najdete v dokumentaci svého AI nástroje. - - -Bezpečnost -========== - -Inspektor odhaluje DI graf, konfiguraci a data jakékoli aplikace, na kterou ho namíříte, proto ho miřte jen na vývojová prostředí a vývojová data. Proces v CLI nemá žádný spolehlivý způsob, jak poznat, že běží na produkčním serveru, a tak je ochrana vrstvená: - -1. **Vývojová závislost**: instalujte ho s `--dev` a `composer install --no-dev` na serveru ho nikdy nenainstaluje. -2. **Bezpečné výchozí hodnoty**: nic nemění data, tajemství jsou maskovaná, žádný nástroj nespouští PHP kód ani nezapisuje soubory. -3. **Vypínač**: `inspector: enabled: false` v `config/mcp-inspector.neon` nebo proměnná prostředí `MCP_INSPECTOR_DISABLED=1` způsobí, že server odmítne nastartovat. - -Dvě další věci se dějí potichu. Hodnoty pod klíči, které vypadají jako tajemství (`password`, `secret`, `token`, `apiKey`, `dsn`, …), vycházejí jako `***`, v parametrech i ve výsledcích dotazů. A výsledky nesoucí data z vaší aplikace, řádky z databáze a záznamy logu, jsou označené jako nedůvěryhodné, takže AI ví, že nemá poslouchat instrukce, které by v nich našla; uživatelský komentář "ignoruj své předchozí instrukce" zůstane jen komentářem. - - -Vlastní toolkity -================ - -Vaše aplikace má i vlastní fakta, která by AI ráda znala: čekající objednávky, feature flagy, tenanty. Přidejte toolkit: třídu implementující `Nette\McpInspector\Toolkit`, jejíž veřejné metody označené `#[McpTool]` se stanou nástroji. Docblock je popis nástroje, pište ho tedy pro AI: co nástroj vrací a kdy ho volat. - -```php -namespace App\Mcp; - -use Mcp\Capability\Attribute\McpTool; -use Mcp\Schema\ToolAnnotations; -use Nette\McpInspector\AppContainer; -use Nette\McpInspector\Toolkit; -use Nette\McpInspector\UntrustedData; - -class BlogToolkit implements Toolkit -{ - public function __construct( - private AppContainer $app, - ) {} - - public function isAvailable(): bool - { - return true; - } - - /** - * Get a blog post by ID. - * @param int $id Post ID - */ - #[UntrustedData] - #[McpTool(name: 'blog_get_post', title: 'Blog post', annotations: new ToolAnnotations(readOnlyHint: true))] - public function getPost(int $id): array - { - $post = $this->app->get()->getByType(BlogFacade::class)->getPost($id); - return $post ? ['id' => $post->id, 'title' => $post->title] : ['error' => 'not found']; - } -} -``` - -Několik věcí stojí za povšimnutí. Toolkit závisí na `AppContainer`, jehož `get()` vrací aktuální kontejner vaší aplikace, takže se respektuje znovunačtení konfigurace; přes něj se dostanete k jakékoli službě. `isAvailable()` dovolí toolkitu ustoupit, když aplikaci chybí to, co potřebuje. Atribut `#[UntrustedData]` označuje nástroj, jehož výsledek nese data z aplikace (příspěvky, komentáře, uživatelský vstup), a inspektor pak AI řekne, aby instrukce v nich neposlouchala. A `readOnlyHint: true` říká AI nástroji, že volání je bezpečné a nemusí se vás pokaždé ptát. - -Toolkit zaregistrujte jako službu v konfiguraci inspektoru, ne v konfiguraci aplikace: - -```neon -# config/mcp-inspector.neon -services: - - App\Mcp\BlogToolkit -``` - -AI teď může volat `blog_get_post` jako kterýkoli vestavěný nástroj. Vyzkoušejte ho nejdřív z terminálu: `vendor/bin/mcp-inspector call blog_get_post '{"id": 1}'`. - -{{composer: nette/mcp-inspector}} -{{repo: nette/mcp-inspector}} -{{maintitle: Nette MCP Inspector: AI vidí přímo do vaší aplikace}} -{{description: MCP Inspector umožní AI asistentovi podívat se přímo do vaší Nette aplikace: služby v DI kontejneru, databázové tabulky, routy a logy Tracy. Žádné hádání.}} diff --git a/ai/cs/tips.texy b/ai/cs/tips.texy deleted file mode 100644 index d348348e9a..0000000000 --- a/ai/cs/tips.texy +++ /dev/null @@ -1 +0,0 @@ -{{redirect: vibe-coding}} diff --git a/ai/cs/vibe-coding.texy b/ai/cs/vibe-coding.texy deleted file mode 100644 index 25a3b210bb..0000000000 --- a/ai/cs/vibe-coding.texy +++ /dev/null @@ -1,299 +0,0 @@ -Vibe coding v praxi -******************* - -
- -Vibe coding znamená, že běžnou řečí popíšete, co chcete, a PHP za vás napíše agent. Je to mocný způsob práce, ale jako každý nástroj vydá víc, když víte, jak na něj. Tahle stránka shrnuje praktické rady z reálné práce s Nette. - -- Jak psát prompty, které fungují -- Jak z MCP Inspectoru vytěžit maximum -- Osvědčené postupy pro běžné úkoly -- Čeho se vyvarovat - -
- - -Jak psát lepší prompty -====================== - - -Umění být konkrétní -------------------- - -Nejvíc ze všeho pomůže konkrétnost. Porovnejte tyhle dva prompty: - -**Mlhavý prompt:** - -``` -Vytvoř formulář -``` - -Tím AI skoro žádný kontext nedáváte. Jaký formulář? S jakými poli? S jakou validací? AI si musí zbytek domyslet a nemusí se trefit do toho, co potřebujete. - -**Konkrétní prompt:** - -``` -Vytvoř ProductForm s těmito poli: -- name: textové pole, povinné, maximálně 100 znaků -- price: desetinné číslo, povinné, musí být kladné -- description: textarea, nepovinné -- category: select z entit CategoryRow - -Použij vykreslování pro Bootstrap 5. Formulář má sloužit jak pro zakládání nových produktů, tak pro editaci stávajících. -``` - -Teď AI přesně ví, co potřebujete, a vygeneruje kód, který funguje napoprvé. - - -Odkazujte na existující vzory ------------------------------ - -Váš kód už nějaké vzory má. Místo abyste je popisovali, ukažte AI příklad: - -``` -Vytvoř OrderPresenter. Drž se stejných vzorů jako ProductPresenter: -stejná struktura, stejná práce s formuláři, stejné uspořádání šablon. -``` - -AI si ProductPresenter přečte a zopakuje postupy, které už používáte. - - -Nechte těžkou práci na MCP --------------------------- - -Pokud máte nainstalovaný MCP Inspector (a měli byste), nepopisujte AI svou aplikaci. Ať si ji zjistí sama: - -**Místo tohohle:** - -``` -Moje tabulka product má sloupce: id (int), name (varchar), price (decimal), -category_id (int, cizí klíč do category), created_at (datetime)... -``` - -**Řekněte prostě:** - -``` -Vygeneruj entitu pro tabulku product. -``` - -AI zavolá `db_get_columns("product")` a uvidí skutečné schéma. Vygenerovaná entita bude odpovídat vaší reálné databázi včetně sloupců, které byste možná zapomněli zmínit. - - -Řekněte, o co vám jde ---------------------- - -AI vám nevidí do hlavy. Pokud má váš požadavek nějaký důvod, podělte se o něj: - -``` -Potřebuju zrychlit výpis produktů. Teď se načítají všechny najednou, -což je při tisících položek pomalé. -Stránka musí umět filtrovat podle kategorie a řadit podle ceny nebo názvu. -``` - -Díky tomu AI navrhne vhodné řešení (stránkování, líné načítání, cache), místo aby slepě implementovala, co jste jí zadali. - - -Práce s MCP Inspectorem -======================= - -MCP Inspector vynikne, když ho použijete s rozmyslem. - - -Nejdřív prozkoumejte, pak stavte --------------------------------- - -Začínáte novou funkci? Nechte AI, ať se nejdřív zorientuje: - -``` -Chystám se přidat sledování objednávek. Než začneme: -1. Jaké mám služby související s objednávkami? -2. Jak vypadá moje tabulka order? -3. Které routy obsluhují stránky s objednávkami? -``` - -AI tím získá kontext o vašem stávajícím kódu a nová funkce do něj přirozeně zapadne. - - -Chytřejší ladění ----------------- - -Když se něco pokazí, nepopisujte chybu. Nechte AI, ať si ji přečte: - -``` -Něco se rozbilo. Podívej se do logu Tracy na poslední výjimku -a řekni mi, co se stalo. -``` - -AI zavolá `tracy_get_log()` a `tracy_get_report()`, přečte si stack trace a často odhalí příčinu rychleji, než byste ji stihli popsat. - - -Ověřte, než začnete tvořit --------------------------- - -Než přidáte nové routy nebo odkazy, ověřte si, co už existuje: - -``` -Který presenter obsluhuje /admin/products/edit? Chci mít jistotu, -že nevytvářím něco, co by kolidovalo. -``` - - -Osvědčené postupy -================= - -Tady jsou vyzkoušené přístupy k běžným úkolům. - - -Kompletní CRUD --------------- - -Nežádejte kousek po kousku. Dejte AI úplné zadání: - -``` -Vytvoř kompletní CRUD pro správu produktů: - -1. ProductPresenter s akcemi: list, add, edit, delete -2. ProductForm jako komponentu (pro přidání i editaci) -3. Latte šablony pro všechny akce -4. Návrh rout - -Vyjdi ze skutečného schématu tabulky product. Drž se postupů -z CategoryPresenter, pokud existuje. -``` - - -Přidání nové funkce -------------------- - -Rozdělte ji na fáze, které na sebe navazují: - -``` -Potřebuju k produktům přidat zákaznické recenze. - -Fáze 1 - datová vrstva: -- Podívej se na tabulku product -- Navrhni schéma tabulky review -- Vytvoř entitu ReviewRow - -Fáze 2 - business logika: -- Vytvoř ReviewService pro CRUD operace -- Přidej metody pro načtení recenzí k produktu - -Fáze 3 - UI: -- Přidej výpis recenzí do ProductPresenter:detail -- Vytvoř ReviewForm pro odesílání recenzí -``` - - -Refaktoring existujícího kódu ------------------------------ - -Nechte AI, ať kódu nejdřív porozumí: - -``` -Zanalyzuj třídu OrderService. Co dělá která metoda? -Vidíš tam nějaké code smells nebo prostor ke zlepšení? -``` - -A potom: - -``` -Metoda calculateTotal toho dělá příliš. Rozděl ji na menší metody -a zachovej přitom stejné veřejné rozhraní. -``` - - -Čemu se vyhnout -=============== - - -Nekontrolovat vygenerovaný kód ------------------------------- - -AI generuje kód rychle, ale to neznamená, že je každý řádek dokonalý. Vždycky projděte: - -- **Databázové dotazy** - Jsou efektivní? Nechybí jim indexy? -- **Bezpečnost** - Validuje se vstup? Kontrolují se oprávnění? -- **Hraniční případy** - Co se stane při prázdných datech? Při hodnotách null? - -AI je opravdu dobrá, ale pořád je to asistent. Za výsledný kód zodpovídáte vy. - - -Ignorovat hlášení linteru -------------------------- - -Pokud používáte [pluginy pro Claude Code nebo Codex |agent-plugins], kontrolují vám automaticky soubory PHP, Latte, NEON, JSON i JavaScript. Když některý ohlásí chybu, AI o ní ví. Místo ručního opravování prostě řekněte: - -``` -Oprav chybu, kterou jsi právě udělal. -``` - -AI si výstup kontroly přečte a chybu opraví. - - -Zapomenout zaregistrovat službu -------------------------------- - -Když AI vytvoří novou třídu služby, občas ji zapomene zaregistrovat do DI kontejneru. Pokud narazíte na chybu "Service not found", zeptejte se: - -``` -Co musím změnit v services.neon kvůli nové OrderExportService? -``` - - -Chtít všechno najednou ----------------------- - -AI zvládne i složité úkoly, ale někdy pomůže rozdělit je na menší: - -**Příliš ambiciózní:** - -``` -Postav mi kompletní e-shop s katalogem produktů, -košíkem, objednávkovým procesem, platbami, sledováním objednávek a administrací. -``` - -**Lepší přístup:** - -``` -Pojďme postavit e-shop krok za krokem. Začni katalogem produktů: -potřebuju výpis, detail a správu produktů. -``` - - -V čem AI vyniká a v čem ne -========================== - - -AI je skvělá na ---------------- - -- **Rutinní kód** - presentery, formuláře, entity, základní šablony -- **Držení se vzorů** - "udělej to jako X, ale pro Y" -- **Porozumění kódu** - "co dělá tahle metoda?" -- **Psaní testů** - testy k hotové implementaci -- **Refaktoring** - lepší struktura kódu při zachování chování - - -Ručně raději pište ------------------- - -- **Složitou business logiku** - doménová pravidla, která je potřeba promyslet -- **Výkonově kritický kód** - algoritmy, které potřebují optimalizaci -- **Bezpečnostně citlivý kód** - přihlašování, oprávnění, šifrování -- **Netradiční řešení** - věci, které se nedrží existujících vzorů - -AI je násobič, ne náhrada. Dobré vývojáře zrychlí, ale pořád potřebuje, aby ji dobrý vývojář vedl. - - -Závěrem -======= - -Vývoj s AI se nejlíp naučíte praxí. Začněte jednoduchými úkoly, všímejte si, co funguje, a postupně se pouštějte do složitějších projektů. - -A pamatujte: AI je váš asistent. Vývojář jste pořád vy. Vy rozhodujete, vy kontrolujete kód a vy zodpovídáte za kvalitu výsledku. - -Hodně zdaru! - -{{maintitle: Vibe coding v PHP s Nette: prompty a osvědčené postupy}} -{{description: Praktické rady pro vibe coding v PHP s Nette: jak psát prompty, které fungují, jak využít MCP Inspector, osvědčené postupy a čeho se vyvarovat.}} diff --git a/ai/en/@home.texy b/ai/en/@home.texy deleted file mode 100644 index 6a31ecee12..0000000000 --- a/ai/en/@home.texy +++ /dev/null @@ -1,98 +0,0 @@ -Vibe Coding with Nette -********************** - -
- -An AI agent will write you a hundred lines of PHP in a few seconds. Whether those hundred lines actually run is decided by something the agent cannot supply on its own: a foundation it can read, verify and build on. - -Nette has been building that foundation for years - back when it still looked like a detail only purists cared about. - -- **Typed to the bone** - the agent reads types instead of guessing them -- **Clear conventions** - it knows where every class belongs -- **A closed loop** - it sees your real application and checks its own work - -
- - -The IDE Was for You. Types Are for the Agent. -============================================= - -For years we judged our tools by how well they helped a human: autocomplete, a type popping up on hover, jump-to-definition. An agent never looks at your screen. It needs information it can read as plain text and, above all, verify on its own. - -That is the whole difference between an IDE and static analysis. PhpStorm discreetly underlines a place where you might be passing `null`. PHPStan prints the same thing to the console as an error the agent reads, understands and fixes - without you sitting there watching. - -So the question stopped being "how nicely does my editor describe this API?" and became "how much does the code say about itself?" - - -Nette Says a Lot -================ - -When PHP 7.1 completed the type system with nullable types, Nette was the first full-stack framework to type its entire API through, `declare(strict_types=1)` in every file included. Typed properties followed the moment PHP 7.4 shipped them. Whatever the language could express, Nette used - usually before anyone else. - -And where PHP itself falls short, annotations take over. Native types cannot say "a non-empty list of strings" or "the return value depends on the argument". Nette says both, right where it matters: - -```php -$data = $form->getValues(RegistrationData::class); -echo $data->email; // $data is a RegistrationData, fully typed -``` - -There is no native PHP type for "if you hand me a class name, I hand you back an instance of it". A conditional return type says exactly that, and static analysis, your IDE and the agent all read it. Write `$data->emial` and the mistake surfaces before the code is ever run. - -The same care runs through the whole framework: array shapes, `class-string`, `positive-int`, `non-empty-string`. **Every Nette package passes PHPStan at level 8** - not in parts, not with a pile of exceptions. You are building on a foundation typed as rigorously as it makes sense to type anything, and a well-typed foundation is what makes a well-typed application easy. - -.[note] -The full story is in the article [Your AI Agent Is Only as Good as Your Types |https://blog.nette.org/en/ai-agent-only-as-good-as-your-types]. - - -It's All About the Loop -======================= - -An agent is only as good as the feedback it gets, and the best feedback is a loop. It writes something, runs the analysis and the tests, reads the result, fixes the error and tries again. Write, check, fix, repeat. That loop is the moment a text generator turns into something that genuinely tunes code into working shape. - -Nette hands the agent every part of it: - -- **PHPStan** with the [Nette extension |tools:phpstan-rules], which teaches the analyzer framework specifics - it knows that `$this['menu']` is your `MenuControl`, it infers the control type from `addText()`, it understands `#[Inject]` -- **[Nette Tester |tester:]** for the tests. Don't have any yet? That is the very first thing to have the agent write, because without them it cannot tell that it just broke something. -- **Automatic linting** of PHP, Latte, NEON, JSON and JavaScript after every single edit, so a broken template never makes it as far as the browser -- **Automatic formatting** of PHP by your coding standard with [DressCode |tools:coding-standard], which tells the agent what it changed, so the agent can make its next edit without reading the file again - - -Your Application, Not a Guess -============================= - -Ask any AI to "generate an entity for the product table" and it will guess your columns from patterns it picked up during training. Except your price column is called `unit_price`, and there is a `currency_id` you never mentioned. - -**[MCP Inspector |mcp-inspector]** takes the guessing away. It is a server that lets the agent look straight into your running application: the services in your DI container, your real database schema, your routes, your Tracy logs. Everything it exposes is strictly read-only, and it works from any editor that speaks the Model Context Protocol: Claude Code, Cursor, VS Code and others. - -Debugging changes shape too. Instead of copying a stack trace into a chat window, you say "check the log and tell me what broke" - and the agent reads the exception itself. - - -Nette Knowledge, Built In -========================= - -The **[plugins for Claude Code and Codex |agent-plugins]** supply the part that is nowhere in your codebase: how things are actually done in Nette. A whole family of skills covering architecture, forms, database, components, security, Latte, NEON and more, each one switching itself on when the conversation turns to its topic. Ask about form validation and the forms skill wakes up. Ask about a BlueScreen and the Tracy one does. - -What comes out reads like an experienced Nette developer wrote it, not like generic PHP stitched together from snippets. - - -Get Started -=========== - -The whole setup takes about ten minutes: - -1. [Install your AI tool |getting-started] - we recommend Claude Code -2. [Add the Nette plugins |agent-plugins] so the agent knows the framework -3. [Set up MCP Inspector |mcp-inspector] so it knows *your* application - -Then describe what you want, close the agent into the loop, and let it work. When you want to get more out of your agent, read [Vibe Coding in Practice |vibe-coding]. - - -When Your Application Needs AI Too -================================== - -This whole page is about AI that helps you write code. Inside your application, AI can do something quite different: summarize an article, sort incoming messages, pull invoice data into your database. - -[AI Access |https://ai-access.deegee.dev] is the library for that: a single PHP interface to OpenAI, Claude, Gemini, DeepSeek and Grok. Switching between them is a one-line change. And since it has no dependencies, it brings no vendor SDK and no version conflicts into your project. - -{{maintitle: Vibe Coding with Nette – The PHP Framework AI Agents Can Read}} -{{description: AI agents guess as much as your code lets them. Nette is typed to the bone, passes PHPStan at level 8, and gives your agent a loop it can verify.}} diff --git a/ai/en/@left-menu.texy b/ai/en/@left-menu.texy deleted file mode 100644 index fe43cedd74..0000000000 --- a/ai/en/@left-menu.texy +++ /dev/null @@ -1,5 +0,0 @@ -- [Overview |@home] -- [Getting Started |getting-started] -- [Agent Plugins |agent-plugins] -- [MCP Inspector |mcp-inspector] -- [Vibe Coding in Practice |vibe-coding] diff --git a/ai/en/@meta.texy b/ai/en/@meta.texy deleted file mode 100644 index e06cc9886c..0000000000 --- a/ai/en/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette AI}} diff --git a/ai/en/agent-plugins.texy b/ai/en/agent-plugins.texy deleted file mode 100644 index 8c860d81c7..0000000000 --- a/ai/en/agent-plugins.texy +++ /dev/null @@ -1,477 +0,0 @@ -Nette Plugins for Claude Code and Codex -*************************************** - -
- -The "Nette plugins":https://github.com/nette/agent-plugins give Claude Code and Codex deep knowledge of the framework. Instead of generic PHP advice, you get recommendations that follow Nette conventions - from presenters and forms to Latte templates and database queries. - -The same plugins work in both tools and give you: -- A **broad set of skills** covering all major areas of Nette development -- **Automatic validation** of PHP, Latte, NEON, JSON and JavaScript files (the `nette-lint` plugin) -- **MCP Inspector integration** for real-time application introspection - -
- - -Installation -============ - -The plugins come as a family, so you install only what you need: - -| Plugin | Purpose | -|--------|---------| -| `nette` | Skills with deep framework knowledge | -| `nette-lint` | Automatic validation of PHP, Latte, NEON, JSON and JavaScript | -| `php-fixer` | Automatic PHP code style fixing with DressCode (optional) | -| `nette-dev` | Coding standards for framework contributors | - - -Claude Code ------------ - -If you haven't installed Claude Code yet, see the [complete setup guide |getting-started]. Once Claude Code is running, add the Nette marketplace and install the main plugin: - -```shell -/plugin marketplace add nette/agent-plugins -/plugin install nette@nette -``` - -For automatic validation of your PHP, Latte, NEON, JSON and JavaScript files after each edit, add the companion linter plugin: - -```shell -/plugin install nette-lint@nette -``` - - -Codex ------ - -In Codex, add the same marketplace from the command line and install the main plugin: - -```shell -codex plugin marketplace add nette/agent-plugins -codex plugin add nette@nette -``` - -The other plugins are installed the same way, for example `codex plugin add nette-lint@nette`. Then start a new session so Codex picks them up. - -There is one extra step for the plugins that check or fix your files. Codex doesn't run a plugin's hooks just because you installed it; you have to approve them first. Open `/hooks` in Codex, review the hook definitions of the installed plugins and trust them. When a plugin update changes its hooks, you have to review them again. - - -How Skills Work -=============== - -You don't need to activate skills manually. They turn on automatically based on what you're talking about. - -- Ask about "presenter structure" → the `nette-architecture` skill activates -- Ask about "form validation" → the `nette-forms` skill activates -- Ask about "Latte filters" → the `latte-templates` skill activates -- Ask about a "BlueScreen error" → the `tracy-debugging` skill activates - -This means you get relevant, context-aware help without having to think about which skill you need. The skills are identical in both clients, so whenever the examples below say Claude, the same applies to Codex. - -Each skill comes with the complete Nette documentation of its topic, converted to Markdown and bundled with the plugin. The skill itself holds the essentials and the common pitfalls; when Claude needs the details, such as every option of a form control or the signature of a Latte filter, it reads the relevant page from your disk instead of fetching the website. - -The plugin lives outside your project, so Claude Code asks for permission the first time it reads one of its files. To skip the prompt, allow reading the plugin directory in your settings: - -```json -{ - "permissions": { - "allow": ["Read(~/.claude/plugins/cache/nette/**)"] - } -} -``` - - -Available Skills -================ - -Here's what each skill covers: - - -nette-architecture ------------------- - -When you're designing your application structure, this skill guides you through: - -- **Directory organization** - Where to put presenters, services, entities, and components -- **Module design** - How to split your application into logical modules (Admin, Front, Api) -- **Presenter patterns** - When to use base presenters, how to handle authentication -- **Evolution strategy** - Start minimal, grow organically, refactor when needed - -The key principle: Don't over-engineer. Create subdirectories when you have 5+ related files, not before. - - -nette-routing -------------- - -URL masks and the router: - -- **Masks** - Optional parts, regular-expression constraints, defaults -- **Order** - Why the first matching route also decides which URL gets generated -- **Filters** - Slug to id mapping, one-way routes for retired addresses -- **Canonical URLs** - When the redirect fires and how to switch it off - -Example: Claude knows that a catch-all route placed first will quietly hijack every generated link. - - -nette-configuration -------------------- - -Everything about the DI container and NEON configuration: - -- **Service registration** - How to define services in `services.neon` -- **Autowiring** - When it works automatically and when you need explicit configuration -- **Parameters** - How to use configuration parameters across your application -- **Extensions** - Working with DI extensions from Nette and third parties - - -nette-database --------------- - -Covers both the raw SQL approach and the Database Explorer: - -- **Database Explorer** - Using `Selection` for queries, `ActiveRow` for entities -- **Entity conventions** - The `Row` suffix pattern, type hints with `@property-read` -- **Relationships** - Navigating foreign keys with colon notation -- **When to use what** - Explorer for CRUD, raw SQL for complex analytics - -Example: Claude knows that `->where('category.slug', $slug)` automatically joins the category table. - - -nette-forms ------------ - -Creating and handling forms the Nette way: - -- **Controls** - All built-in controls from text inputs to file uploads -- **Validation** - Built-in rules, custom validators, conditional validation -- **Rendering** - Manual rendering, Bootstrap integration, custom renderers -- **Patterns** - Create/edit forms, form components, AJAX submissions - - -nette-components ----------------- - -Components, signals and AJAX: - -- **Factories** - `createComponent()`, lazy creation, the component tree -- **Signals** - `handle()`, where they run, links inside a component -- **Snippets** - `redrawControl()`, snippet areas, dynamic snippets -- **Persistent parameters** - The `#[Persistent]` attribute and persistent components - -Example: Claude knows that inside a component template `n:href` always means a signal, and that reaching a presenter needs `{plink}`. - - -nette-security --------------- - -Authentication and authorization: - -- **Login** - The `Authenticator` interface, identities, error codes -- **Passwords** - Hashing as a service, rehashing on login -- **Permissions** - Roles, resources, and the `Permission` ACL -- **Configuration** - The `security:` section - -Example: Claude knows that `logout()` keeps the identity unless you ask it not to, so "has an identity" is not the same as "is logged in". - - -nette-http ----------- - -The request, the response and sessions: - -- **Request** - `UrlScript`, cookies, uploads, and the reverse-proxy trap -- **Sessions** - Sections with `set()` / `get()`, per-key expiration -- **Same-origin protection** - How CSRF defense works and how to allow a legitimate exception -- **SSRF** - Validating a URL that came from a user - -Example: Claude knows that a link from an e-mail counts as direct navigation, so a signal reached that way needs to be marked explicitly. - - -nette-caching -------------- - -Caching with nette/caching: - -- **Dependencies** - Expiration, tags, files, and sliding expiration -- **Invalidation** - Cleaning by tag, and which storages need a journal for it -- **Storages** - File, Memcached, SQLite, memory, and the null one for tests - -Example: Claude knows that tags silently need a journal, and that the constant is `Cache::Expire`. - - -nette-mail ----------- - -Sending e-mail: - -- **Message** - Recipients, HTML body with automatic image embedding, attachments -- **Mailers** - SMTP with OAuth 2.0, fallback, and writing to files in development -- **DKIM** - Signing outgoing mail - -Example: Claude knows that `setHtmlBody()` embeds images referenced in the HTML by itself, so no manual `cid:` juggling is needed. - - -nette-command-line ------------------- - -Command-line scripts with nette/command-line 2.0: - -- **Parameters** - Flags, options with required or optional values, arguments, defaults and normalizers -- **Commands** - Subcommands like `git remote add`, dispatched with `match` -- **Help and errors** - Help generated from the definition, messages the user understands -- **Console** - Colors that respect `NO_COLOR`, status lines, measuring text -- **Upgrading from 1.x** - A faithful rewrite of 1.x code, with a report of every place where 2.0 behaves differently - -Example: Claude knows that the second argument of `addOption()` is the description, so the alias has to be passed as `alias:`. - - -nette-php-generator -------------------- - -Generating PHP code with nette/php-generator: - -- **Elements** - Classes, interfaces, traits, enums, functions and closures -- **Namespaces and files** - Automatic shortening of class names by `use` clauses -- **Bodies** - Placeholders that export values as valid PHP -- **Existing code** - Loading classes and files and changing them - -Example: Claude knows that in a body with placeholders, `??` and `?->` have to be escaped. - - -nette-schema ------------- - -Data validation and normalization with the Schema component: - -- **Expect class** - Building validation schemas for arrays and objects -- **Configuration schemas** - Validating NEON configuration in DI extensions -- **Type coercion** - Automatic conversion of strings to integers, dates, etc. -- **Custom validators** - Adding your own validation rules - -Example: Claude knows how to create schemas like: - -```php -$schema = Expect::structure([ - 'name' => Expect::string()->required(), - 'age' => Expect::int()->min(0)->max(120), - 'email' => Expect::string(), - 'roles' => Expect::listOf('string')->default([]), -]); -``` - - -nette-tester ------------- - -Writing tests with Nette Tester: - -- **Test structure** - The `.phpt` format, `@testCase` annotation, file organization -- **Assertions** - All `Assert::*` methods: `same()`, `equal()`, `exception()`, `match()`, and more -- **Fixtures** - Setting up test data, mocking dependencies, database transactions -- **Running tests** - Command-line options, parallel execution, code coverage - -Example: Claude can generate proper test files: - -```php -/** @testCase */ -class UserServiceTest extends TestCase -{ - public function testCreateUser(): void - { - $service = new UserService($this->mockDatabase()); - $user = $service->create(['name' => 'John']); - Assert::same('John', $user->name); - } -} -``` - - -tracy-debugging ---------------- - -Working with the Tracy debugger: - -- **BlueScreen** - Reading exception details, stack traces, source highlights -- **Tracy Bar** - Interpreting embedded SQL query logs, timing, and memory usage at the bottom of every page -- **`dump()` workflow** - Inserting `dump($variable)` calls and reading the output back through curl or the browser -- **Production logs** - Investigating `log/exception-*.html` snapshots and `log/error.log` -- **Chrome MCP integration** - Pulling Tracy errors out of the browser console with `list_console_messages()` instead of taking screenshots - -Especially useful for 500 errors, blank pages, N+1 query issues, or whenever Claude fetches a local PHP URL and needs to interpret what came back. - - -nette-utils ------------ - -The utility classes that make PHP development easier: - -- **Arrays** - `Nette\Utils\Arrays` with `get()`, `getRef()`, `map()`, `flatten()`, and more -- **Strings** - Unicode-safe operations: `webalize()`, `truncate()`, `contains()`, `startsWith()` -- **Finder** - File system traversal with filtering by name, size, date -- **Image** - Resize, crop, sharpen with automatic format detection -- **Json** - Safe JSON encoding/decoding with proper error handling -- **Validators** - Email, URL, numeric validation helpers -- **DateTime** - Immutable date/time with Czech locale support - - -frontend-development --------------------- - -Integrating frontend tools with Nette: - -- **Vite** - Setting up Vite for modern JavaScript/TypeScript development, HMR configuration -- **Nette Assets** - Using the asset system for cache-busted URLs in production -- **Tailwind CSS** - Configuration for Tailwind with Latte templates, purging unused styles -- **ESLint** - Code quality integration via `@nette/eslint-plugin` -- **Build scripts** - npm/package.json scripts for development and production builds - -Claude can help configure `vite.config.js` to work with Nette's directory structure and generate proper asset references in Latte templates. - - -latte-templates ---------------- - -Everything about the Latte templating engine: - -- **Syntax** - Tags, filters, blocks, and inheritance -- **Security** - Auto-escaping, content-aware output -- **Custom filters** - Creating and registering your own filters -- **Template classes** - Using typed templates for better IDE support - - -neon-format ------------ - -The NEON configuration format: - -- **Syntax** - Mappings, sequences, entities, and multiline strings -- **Common patterns** - Service definitions, parameter references -- **Debugging** - Finding and fixing syntax errors - - -nette-upgrading ---------------- - -A lookup table of retired names: - -- **Renames** - `I`-prefixed interfaces, UPPER_CASE constants, moved classes -- **Silent survivals** - Where the old name still works, so the mistake never surfaces -- **Changed signatures** - Arguments that disappeared or became named - -Example: Claude knows that `Nette\Database\Context` still resolves as an alias, so code written from memory keeps running while quietly using a retired name. - - -Automatic Validation -==================== - -One of the most useful features is automatic validation, provided by the separately installed **nette-lint** plugin. After every file edit it checks for errors: - -| What | How It Works | -|------|--------------| -| **PHP** | Runs `php -l` to check syntax after every `.php` or `.phpt` edit | -| **Latte templates** | Runs `latte-lint` to check syntax after every `.latte` edit | -| **NEON files** | Validates NEON syntax after every `.neon` edit | -| **JSON / JSONC** | Validates syntax after every `.json` or `.jsonc` edit and reports the exact line; comments and trailing commas are tolerated in JSONC files like `tsconfig.json` or `.vscode/*.json` | -| **JavaScript / TypeScript** | Runs `eslint --fix` after every `.js`, `.ts`, `.mjs` or `.mts` edit (only if the project has an ESLint config) | - -If there's a syntax error in any of these files, the agent knows about it immediately and can suggest a fix. No more discovering errors in the browser. - -The checks run after the agent edits a file with its own editing tools. When it changes files through shell commands instead, no hook runs, so it's worth asking it to run your project's checks afterwards. - -The plugin looks for the project's tools and configs (`latte-lint`, `vendor/bin/neon-lint`, the ESLint config) upwards from the edited file, not just in the folder where the agent is running. So even if you keep several applications in one repository and start the agent from its root, each file is still checked with the right tool for its application. The search stops one level above the nearest `.git` folder, so tools from unrelated projects elsewhere on the disk are never picked up. - -Sometimes you don't want a file checked - for example `fixtures` with intentionally broken templates or code used in tests. You can skip specific paths by creating a `.nette-claude.json` file in your project, where each key is a hook name with a list of paths to ignore: - -```json -{ - "lint-php": { - "exclude": ["fixtures*"] - }, - "lint-latte": { - "exclude": ["tests/**/expected"] - } -} -``` - -Patterns work like in `.gitignore`: a name without a slash (such as `fixtures*`) matches at any depth, while a pattern with a slash is anchored to the folder containing the config file. - - -Plugin for Framework Contributors -================================= - -If you're contributing to Nette itself, there's an additional plugin with coding standards: - -```shell -/plugin install nette-dev@nette # Claude Code -codex plugin add nette-dev@nette # Codex -``` - -| Skill | What It Covers | -|-------|----------------| -| `php-coding-standards` | Nette's PHP coding style - indentation, naming, structure | -| `php-doc` | PHPDoc conventions - when to document, what format to use | -| `commit-messages` | How to write commit messages for Nette repositories | -| `phpstan-analysis` | How to resolve PHPStan errors - prefer refactoring over phpDoc annotations over ignoring - plus common Nette patterns and baseline management | - - -Automatic PHP Style Fixing -========================== - -For automatic code style fixing after every PHP file edit, install the optional php-fixer plugin. In Claude Code: - -```shell -/plugin install php-fixer@nette -/install-php-fixer -``` - -The second command installs [DressCode |https://github.com/dg/dresscode] globally together with PHPStan, from which DressCode takes the types of your code, or into the project when the project uses PHPStan itself, and offers to create a `dresscode.neon` for the project. In Codex, install the plugin with `codex plugin add php-fixer@nette` and then pick the `install-php-fixer` skill, which does the same. After that, every PHP file you edit will be automatically formatted: by your project's `dresscode.neon` (or `dresscode.php`) when there is one, so the same presets, rules and exceptions apply as in CI, and by the Nette coding standard otherwise. A project that has DressCode among its own dependencies is fixed with that copy, so its configuration may use extensions the global installation does not know. - -Because the fixer **rewrites your files**, it's important to keep it away from code you don't want reformatted - typically `fixtures` with intentionally crafted formatting. The fixer is configured only by `dresscode.neon`, not by `.nette-claude.json`, so list such paths in its `excludePaths`: - -```neon -presets: - - nette - -excludePaths: - - fixtures* -``` - -Folders such as `vendor`, `temp`, `log` and hidden ones are left alone even without a configuration, and so is a file of no PHP project, one with neither `composer.json` nor `dresscode.neon` above it, such as a scratch script Claude writes into its temporary directory. - -Claude learns from the hook what happened: when the file was rewritten, so it reads it again before the next edit, and which violations the fixer could not fix, so it fixes them by hand. The plugin also comes with the `php-auto-fixer` skill, which teaches Claude the one subtle pitfall of an automatic fixer: it removes unused `use` statements, so a `use` added in a separate edit before the code that references it gets stripped. Claude knows to add imports together with the code that uses them. - -Beyond the file just edited, the `dresscode` skill teaches Claude to work with DressCode itself: to check or fix the whole project, to read its report, to change `dresscode.neon`, to silence a rule or write a baseline, to set it up in CI, and to upgrade the code to a newer PHP version or library API. Claude asks the installed DressCode about its rules and your configuration instead of guessing, and leaves the decisions that are yours to you: a fix over the whole project, risky fixes that may change what the code does, switching a rule off, a baseline, and raising the PHP version in `composer.json`. - -If you used version 1 of the plugin, which formatted with `nette/coding-standard`, nothing stops working after the upgrade: the fixer keeps using it until DressCode is installed, and Claude offers to migrate. With your consent it installs DressCode the same way, creates `dresscode.neon` and shows what the new standard would change. - - -MCP Inspector Integration -========================= - -The plugins work even better with [MCP Inspector |mcp-inspector] - a tool that lets the agent see your actual application state. With MCP Inspector, Claude can: - -- Query your real database schema instead of guessing -- List your registered DI services and their configuration -- Read Tracy error logs for debugging -- Match URLs to presenters using your actual routes - -Install it with two commands: - -```shell -composer require --dev nette/mcp-inspector:@dev -vendor/bin/mcp-inspector init -``` - -Then restart Claude Code. Codex isn't registered by `init`, so add the server to its configuration yourself as described in [Other AI Tools |mcp-inspector#other-ai-tools]. See the [MCP Inspector documentation |mcp-inspector] for all the available tools. - - -Privacy -======= - -The plugins collect no data. They run locally and have no servers, telemetry or analytics. See the [privacy policy |privacy-policy] for details. - -{{repo: nette/agent-plugins}} -{{maintitle: Nette Plugins for Claude Code and Codex: AI Agent Skills}} -{{description: Give Claude Code and Codex deep knowledge of Nette. Instead of generic PHP advice, you get recommendations that follow Nette conventions.}} diff --git a/ai/en/getting-started.texy b/ai/en/getting-started.texy deleted file mode 100644 index 3fa8b4f4ae..0000000000 --- a/ai/en/getting-started.texy +++ /dev/null @@ -1,261 +0,0 @@ -Getting Started -*************** - -
- -Ready to try [vibe coding |@home] with Nette? This guide walks you through the complete setup: - -- Choosing and installing an AI tool -- Setting up MCP Inspector so AI can see your application -- Making your first AI-assisted changes - -The whole process takes about 10 minutes. Let's get started! - -
- - -Choosing Your AI Tool -===================== - -Nette AI tools work with any MCP-compatible AI assistant. We recommend **Claude Code** for the best experience - it has a dedicated Nette plugin with deep framework knowledge and automatic code validation. - -Other options include **Cursor**, **VS Code**, and other MCP-compatible tools. See [Other AI Tools |#other-ai-tools] at the end of this guide. - - -What You'll Need -================ - -Before we begin, make sure you have: - -- **A Nette project** - existing or new (`composer create-project nette/web-project`) -- **PHP 8.3+** - required for MCP Inspector -- **An AI tool** - we'll install Claude Code below (Claude Pro costs $20/month) - - -Installation on macOS and Linux -=============================== - -```shell -curl -fsSL https://claude.ai/install.sh | bash -``` - -On macOS, you can alternatively use Homebrew: - -```shell -brew install --cask claude-code -``` - -After installation, skip to [Starting Claude Code |#starting-claude-code]. - - -Installation on Windows via WSL -=============================== - -Claude Code requires a Unix environment. On Windows, use WSL: - -```shell -wsl --install -``` - -This installs Ubuntu. **Restart your computer** after installation. - -After restart, launch Ubuntu: - -```shell -ubuntu -``` - -The first launch will ask for a username and password - you'll need them for `sudo` later. - -Installing Claude Code in WSL in the Ubuntu terminal: - -```shell -curl -fsSL https://claude.ai/install.sh | bash -``` - -Your Windows drives are mounted under `/mnt/`: -- `C:\Users\Jan\Projects` → `/mnt/c/Users/Jan/Projects` -- `D:\Work` → `/mnt/d/Work` - -From Windows, access Linux files via `\\wsl$\Ubuntu\home\username` in Explorer. - - -Starting Claude Code -==================== - -Great, you have Claude Code installed! Let's start it up. - -Navigate to your project directory: - -```shell -# Windows (WSL) -cd /mnt/c/Users/Jan/Projects/my-app - -# macOS/Linux -cd ~/projects/my-app -``` - -Start Claude Code: - -```shell -claude -``` - -The first time you run it, Claude Code will ask you to authenticate. It will open a browser window where you can log in to your Anthropic account. After successful authentication, you'll see the `claude>` prompt and you're ready to go. - -You can also use Claude Code "on the web":https://claude.ai/code or via the "desktop app":https://claude.com/download, but local installation provides the best experience with direct file access. - - -Adding the Nette Plugin -======================= - -Now let's give Claude deep knowledge of Nette. First, add the Nette marketplace and enable auto-updating: - -```shell -/plugin marketplace add nette/agent-plugins -``` - -Then install the plugin: - -```shell -/plugin install nette@nette -``` - -That's it! The plugin is now active. It includes 11 specialized skills that automatically activate based on what you're working on. When you ask about forms, it knows about Nette Forms. When you ask about templates, it knows about Latte. - - -Setting Up MCP Inspector -======================== - -The final piece is MCP Inspector, which lets Claude see your actual application - your services, database schema, routes, and error logs. - -.[caution] -MCP Inspector is still in **early development and has no stable release yet**. Install the development version (`:@dev` below) and expect tool names and configuration to keep changing. - -Two commands do the whole job: - -```shell -composer require --dev nette/mcp-inspector:@dev -vendor/bin/mcp-inspector init -``` - -`init` generates `mcp-bootstrap.php` (the script through which the inspector builds your application's DI container), the inspector's configuration in `config/mcp-inspector.neon`, and registers the server for Claude Code in `.mcp.json`. It recognizes the usual shapes of `App\Bootstrap`; if yours is unusual, the generated file contains a `TODO` comment telling you what to complete. See [the MCP Inspector page |mcp-inspector#the-mcp-bootstrap-php-file] for the details. - -**Important:** After installing MCP Inspector, restart Claude Code (type `/exit` and run `claude` again) to activate the connection. - - -Testing Your Setup -================== - -Let's verify everything works. Try these prompts: - - -Test the Plugin Knowledge -------------------------- - -Type: - -``` -What's the recommended directory structure for a Nette application? -``` - -Claude should respond with detailed information about presenters, models, templates, and configuration - knowledge that comes from the `nette-architecture` skill. - - -Test MCP Inspector ------------------- - -Type: - -``` -What services do I have registered in my DI container? -``` - -If MCP Inspector is working, Claude will call `di_get_services()` and show you the actual services from your application. If you see a list of your real services, congratulations - everything is set up correctly! - - -Test Database Introspection ---------------------------- - -If your application uses a database, try: - -``` -What tables do I have? Show me the columns in the user table. -``` - - -Your First Real Task -==================== - -Now that everything is set up, let's do something useful. Try this prompt: - -``` -I need a simple ArticlePresenter with list and detail actions. -Generate the presenter, templates, and tell me what routes I need. -``` - -Watch as Claude generates a complete, working presenter following Nette conventions. It will: -- Create the presenter class with proper type hints -- Generate Latte templates for both actions -- Suggest the appropriate route configuration - -If you have MCP Inspector set up and an `article` table in your database, try: - -``` -Look at my article table and generate an ArticleRow entity with proper type hints. -``` - - -Other AI Tools -============== - -While we recommend Claude Code for the best Nette experience, MCP Inspector works with any MCP-compatible tool. - - -Cursor ------- - -Cursor is a popular AI-first code editor. To use MCP Inspector with Cursor: - -1. Install MCP Inspector as described in [Setting Up MCP Inspector |#setting-up-mcp-inspector]; when your project already has a `.cursor` directory, `init` writes `.cursor/mcp.json` for you -2. Otherwise create `.cursor/mcp.json` in your project: - -```json -{ - "mcpServers": { - "nette-inspector": { - "command": "php", - "args": ["vendor/bin/mcp-inspector"] - } - } -} -``` - -3. Restart Cursor - -Note: Cursor doesn't have the Nette-specific skills that the [Nette plugins |agent-plugins] provide, but MCP Inspector will still give it access to your application's services, database, routes, and logs. - - -VS Code -------- - -VS Code with GitHub Copilot supports MCP servers natively. When your project already has a `.vscode` directory, `init` writes `.vscode/mcp.json` for you; otherwise create it with a `servers` entry as described in [Other AI Tools |mcp-inspector#other-ai-tools]. - - -Other MCP Tools ---------------- - -Any tool supporting the Model Context Protocol can use MCP Inspector. See the [MCP Inspector page |mcp-inspector#other-ai-tools] for setup instructions. - - -What's Next -=========== - -You're now ready for AI-assisted Nette development! Here's where to go from here: - -- [MCP Inspector |mcp-inspector] - Learn about every introspection tool -- [Agent Plugins |agent-plugins] - Explore all the skills (Claude Code and Codex users) -- [Vibe Coding in Practice |vibe-coding] - Get the most out of your AI assistant - -{{maintitle: Vibe Coding with Nette: Getting Started with Claude Code}} -{{description: Set up vibe coding with Nette in about ten minutes: install Claude Code, add the Nette plugin, and connect MCP Inspector to your application.}} diff --git a/ai/en/guide.texy b/ai/en/guide.texy deleted file mode 100644 index 3ec981a90e..0000000000 --- a/ai/en/guide.texy +++ /dev/null @@ -1 +0,0 @@ -{{redirect: @home}} diff --git a/ai/en/mcp-inspector.texy b/ai/en/mcp-inspector.texy deleted file mode 100644 index 500d1fcfc2..0000000000 --- a/ai/en/mcp-inspector.texy +++ /dev/null @@ -1,328 +0,0 @@ -MCP Inspector -************* - -
- -"MCP Inspector":https://github.com/nette/mcp-inspector lets an AI assistant look directly into your Nette application: it sees which services are registered in the DI container, what your database tables look like, which route leads where and what Tracy logged last night. You will learn: - -- how the inspector works and what it can see -- how to install it in two commands -- what each tool does and how to try it from the terminal -- how to keep the AI on a short leash: read-only queries, masked secrets, a kill switch - -
- -Without the inspector, the AI guesses your application from patterns it picked up during training. With it, the AI asks your application and gets the truth: the real columns, the real service names, the real error. - -.[caution] -MCP Inspector is still in **early development and has no stable release yet**. Until the first release, install it with `composer require --dev nette/mcp-inspector:@dev` and expect tool names and configuration to keep changing. - - -How It Works -============ - -MCP Inspector is a server speaking the **Model Context Protocol (MCP)**, the standard through which AI tools such as Claude Code, Cursor or VS Code call external tools. Your editor starts the inspector as a background process, and whenever the AI needs something from your application, it calls one of the inspector's tools and gets the answer back. - -To answer, the inspector builds your application's DI container. It does that through a small script `mcp-bootstrap.php` in your project root, which returns your application's `Configurator` with all configs added; the inspector creates the container itself, in debug mode, in its own temp directory, so it never touches your web's cache. - -The container stays alive between calls, but every call checks whether your configuration changed. When you edit `services.neon`, the next tool call already sees the new services; no restart of the editor is needed. If a rebuild fails, say because of a typo in your config, the inspector keeps serving the last working container and adds a `_warning` field to the result so the AI tells you about the failure right away. - -Everything is **read-only by default**: the inspector reads your services, schema, routes and logs, but it cannot change your data or configuration and it never executes code from the AI. The single exception, running modifying SQL, is switched off unless you explicitly [enable it |#database-configuration]. - - -Installation -============ - -Two commands. The first adds the package as a development dependency, the second generates the files the inspector needs: - -```shell -composer require --dev nette/mcp-inspector:@dev -vendor/bin/mcp-inspector init -``` - -`init` creates three files and never overwrites an existing one: - -| File | Purpose -|------|------ -| `mcp-bootstrap.php` | returns your application's `Nette\Bootstrap\Configurator` (see below) -| `config/mcp-inspector.neon` | the inspector's configuration: what the AI may do -| `.mcp.json` | registers the `nette-inspector` server for Claude Code; `.cursor/mcp.json` and `.vscode/mcp.json` get the same entry when those directories exist - -Then restart your AI tool (in Claude Code type `/exit` and run `claude` again): MCP servers connect when the tool starts. - -Two options come in handy. When PHP does not run directly on your machine, pass the command the AI tool should use: `--php="ddev exec php"`. When the project is not the current directory, add `--project=PATH`. - -Is it working? Ask the AI: - -``` -What services do I have registered in the DI container? -``` - -If the answer lists the real services of your application, you are done. - - -The mcp-bootstrap.php File -========================== - -The inspector needs a `Configurator` with all configs added but *before* `createContainer()` is called, because it builds the container itself. `init` looks at your `App\Bootstrap` class and generates the file accordingly: - -- **Static `App\Bootstrap::boot(): Configurator`** (the classic Web Project): the file is simply `return App\Bootstrap::boot();` -- **Object `Bootstrap` with `bootWebApplication(): Container`** (Web Project since 2024): add a method that stops before creating the container and use it from both places: - -```php -public function bootWebApplication(): Nette\DI\Container -{ - return $this->bootConfigurator()->createContainer(); -} - -public function bootConfigurator(): Configurator -{ - $this->initializeEnvironment(); - $this->setupContainer(); - return $this->configurator; -} -``` - -`mcp-bootstrap.php` then reads `return (new App\Bootstrap)->bootConfigurator();`. - -- **Custom bootstrap** (a constructor with arguments, multi-tenant applications and similar): `init` writes a template with a `TODO` comment for you to complete. Keep the `new Configurator` call inside your `Bootstrap` class so Nette's `%appDir%` autodetection, which looks at the file creating the Configurator, keeps working. Environment variables set in `.mcp.json` are a handy way to pass parameters: - -```php -$blog = getenv('BLOG') === 'phpfashion' ? App\Blog::PhpFashion : App\Blog::LaTrine; -return (new App\Bootstrap($blog))->bootConsoleConfigurator(); -``` - -You do not need to switch on debug mode; the inspector does that itself, because the CLI never autodetects it and debug mode is what makes the live reload of configuration possible. - - -Tools -===== - -The tools are grouped by what they look at. Each group appears only when your application has the corresponding part: without `nette/database` there are no `db_*` tools, and the AI is not confused by tools that cannot work. - - -Application ------------ - -| Tool | What it does -|------|------ -| `app_get_info` | PHP and Nette versions, installed Nette packages, directories and the database driver - -The AI is told to call this first, so the code it writes matches the versions you actually use, PHP 8.3 attributes for example, or the Nette 3.3 way of doing things. - - -DI Container ------------- - -| Tool | What it does -|------|------ -| `di_get_services` | lists services with their types, tags, aliases and autowiring, optionally filtered by a substring of the name or type -| `di_get_service` | details of one service, including whether it has already been created -| `di_find_by_type` | services implementing a class or interface, and which of them autowiring picks -| `di_find_by_tag` | services carrying a tag, with the tag values -| `di_get_parameter_names` | parameter names, nested ones in dotted notation (`database.default.dsn`) -| `di_get_parameter` | the value of one parameter; secrets (`password`, `token`, `dsn`, …) are masked - -When you ask "what mailers do I have?", the AI calls `di_find_by_type("Nette\Mail\Mailer")` and sees exactly what your container holds. Only runtime data is available: the inspector knows the type, tags and aliases of a service, not the factory expression or the `setup` calls from your config. These tools require `nette/di` 3.2.7 or newer; parameters additionally need to be exported, and if your config has `di: export: parameters: no`, the tools tell you so. - - -Router ------- - -| Tool | What it does -|------|------ -| `router_get_routes` | all registered routes with masks, defaults and module prefixes -| `router_match_url` | which presenter and action handle a URL, with the parameters (e.g. `/article/123`) -| `router_generate_url` | the URL for a presenter and action, the way `{link}` does it (e.g. `Article:show` with `{"id": 5}`) - -The inspector runs without an HTTP request, so your application cannot tell its own address the way it does on the web. Tell it in the application's configuration (`nette/http` 3.4): - -```neon -http: - baseUrl: https://example.com/ -``` - -Without it, `router_generate_url` reports an error saying what to set, and relative URLs passed to `router_match_url` are matched against `http://localhost/`. - - -Database --------- - -| Tool | What it does -|------|------ -| `db_get_tables` | tables and views -| `db_get_columns` | columns of a table: types, nullability, defaults, primary and foreign keys -| `db_get_relationships` | foreign key relationships between all tables (belongsTo, hasMany) -| `db_get_indexes` | indexes of a table -| `db_query` | runs one SQL statement with values bound to `?` placeholders; read-only by default -| `db_explain_query` | runs `EXPLAIN` on a `SELECT` query - -This is the group that ends the guessing about your schema. "Generate an entity for the product table" becomes a call to `db_get_columns("product")` and an entity with the columns you really have. - -`db_query` lets the AI look at the data as well, for example to see what values a status column really holds. By default it accepts only `SELECT`-like statements (`SELECT`, `SHOW`, `EXPLAIN`, `DESCRIBE`, `WITH`, `VALUES`, `TABLE`, a single statement, no `INTO OUTFILE`) and runs them inside a read-only transaction on MySQL, PostgreSQL and SQLite, so the database itself rejects anything the validator would miss. Values of columns whose names suggest secrets are masked, and the number of rows is limited. - - -Tracy ------ - -| Tool | What it does -|------|------ -| `tracy_get_log` | the newest entries of a log by level (`exception` by default, `error`, `warning`, …), each with the name of its report -| `tracy_get_report` | an exception report as Tracy writes it for agents: the code around the exception, the stack trace with arguments and the environment - -Debugging changes shape with these two. Instead of copying a stack trace into the chat, you say "check the log and tell me what broke", and the AI reads the exception itself. The log directory is the one your application's Tracy logger writes to, nothing to configure. The markdown reports require Tracy 2.12 or newer; older HTML-only reports cannot be read. - - -Trying Tools From the Terminal -============================== - -You do not need an AI tool to see what a tool returns. The `call` command runs a tool exactly as the client would and prints the result as JSON: - -```shell -vendor/bin/mcp-inspector call app_get_info -vendor/bin/mcp-inspector call router_match_url '{"url": "/article/123"}' -vendor/bin/mcp-inspector call db_query '{"query": "SELECT * FROM product WHERE id = ?", "params": [1]}' -``` - -This is the fastest way to check your bootstrap and configuration, and to see what the AI will see. The options `--project`, `--bootstrap` and `--config` work here too. - - -Configuration -============= - -The inspector is itself a small Nette application, and `config/mcp-inspector.neon` is the configuration of its own DI container, with the usual `parameters:`, `services:` and one section per group of tools. Every section is optional; a missing file means the defaults. This is the file `init` generates: - -```neon -# Configuration of nette/mcp-inspector: a Nette DI config for the inspector's own container. -# The inspector reads it itself, do not add it to the application's configs. -# Every section is optional; missing keys use the defaults shown here. - -inspector: - # false keeps the inspector from starting at all - enabled: true - # tool names or patterns hidden from the agent, e.g. [db_*, tracy_get_log] - disableTools: [] - -database: - # true: only SELECT-like statements, run in a read-only transaction - # false: any statement, the agent can modify data - readOnly: true - # maximum number of rows returned by db_query - rowLimit: 100 -``` - -The inspector reads this file itself; do not add it to your application's configs. Changes take effect after the MCP server restarts, which the AI tool does together with its session. - - -Hiding Tools ------------- - -`disableTools` takes tool names or patterns with `*`. Do you not want the AI to read your data at all? Hide the whole database group: - -```neon -inspector: - disableTools: [db_*] -``` - - -Database Configuration ----------------------- - -The only real security question is whether the AI may modify data, and by default it may not. When you do want it to, say on a throwaway development database, switch the protection off: - -```neon -database: - readOnly: false - rowLimit: 500 -``` - -With `readOnly: false` any statement runs, `UPDATE`, `DELETE` and DDL included. AI tools such as Claude Code then ask you for confirmation before each `db_query`, because the tool no longer declares itself read-only. - - -Other AI Tools -============== - -MCP Inspector works with any tool that speaks MCP. `init` registers it for Claude Code in `.mcp.json`, and for Cursor and VS Code when it finds their `.cursor` or `.vscode` directories in your project. For any other tool, register a command that starts the server over standard input and output: - -```json -{ - "mcpServers": { - "nette-inspector": { - "type": "stdio", - "command": "php", - "args": ["vendor/bin/mcp-inspector"] - } - } -} -``` - -The command runs in your project root. Where it does not (some editors start servers elsewhere), add `"--project=/path/to/project"` to the arguments. Consult your AI tool's documentation for where the configuration file lives. - - -Security -======== - -The inspector exposes the DI graph, configuration and data of whatever application it is pointed at, so point it at development environments and development data only. There is no reliable way for a CLI process to tell that it runs on a production server, so the protection is layered instead: - -1. **Development dependency**: install it with `--dev`, and `composer install --no-dev` on the server never installs it. -2. **Safe defaults**: nothing modifies data, secrets are masked, no tool executes PHP code or writes files. -3. **A kill switch**: `inspector: enabled: false` in `config/mcp-inspector.neon` or the `MCP_INSPECTOR_DISABLED=1` environment variable makes the server refuse to start. - -Two more things happen quietly. Values under keys that look like secrets (`password`, `secret`, `token`, `apiKey`, `dsn`, …) come out as `***`, both in parameters and in query results. And results carrying data from your application, database rows and log entries, are marked as untrusted, so the AI knows not to follow instructions it might find in them; a user comment saying "ignore your previous instructions" stays a comment. - - -Custom Toolkits -=============== - -Your application has facts of its own that the AI would like to know: the pending orders, the feature flags, the tenants. Add a toolkit: a class implementing `Nette\McpInspector\Toolkit` whose public methods marked with `#[McpTool]` become tools. The docblock is the tool's description, so write it for the AI: what the tool returns and when to call it. - -```php -namespace App\Mcp; - -use Mcp\Capability\Attribute\McpTool; -use Mcp\Schema\ToolAnnotations; -use Nette\McpInspector\AppContainer; -use Nette\McpInspector\Toolkit; -use Nette\McpInspector\UntrustedData; - -class BlogToolkit implements Toolkit -{ - public function __construct( - private AppContainer $app, - ) {} - - public function isAvailable(): bool - { - return true; - } - - /** - * Get a blog post by ID. - * @param int $id Post ID - */ - #[UntrustedData] - #[McpTool(name: 'blog_get_post', title: 'Blog post', annotations: new ToolAnnotations(readOnlyHint: true))] - public function getPost(int $id): array - { - $post = $this->app->get()->getByType(BlogFacade::class)->getPost($id); - return $post ? ['id' => $post->id, 'title' => $post->title] : ['error' => 'not found']; - } -} -``` - -A few things to notice. The toolkit depends on `AppContainer`, whose `get()` returns the current container of your application, so config reloads are honoured; through it you reach any service. `isAvailable()` lets a toolkit step aside when the application lacks what it needs. The `#[UntrustedData]` attribute marks a tool whose result carries data from the application (posts, comments, user input), and the inspector then tells the AI not to follow instructions found in it. And `readOnlyHint: true` tells the AI tool that the call is safe to run without asking you each time. - -Register the toolkit as a service in the inspector's configuration, not in your application's: - -```neon -# config/mcp-inspector.neon -services: - - App\Mcp\BlogToolkit -``` - -The AI can now call `blog_get_post` like any built-in tool. Try it first from the terminal: `vendor/bin/mcp-inspector call blog_get_post '{"id": 1}'`. - -{{composer: nette/mcp-inspector}} -{{repo: nette/mcp-inspector}} -{{maintitle: Nette MCP Inspector: Let AI See Inside Your Application}} -{{description: MCP Inspector lets an AI assistant look directly into your Nette application: DI services, database tables, routes and Tracy logs, instead of guessing.}} diff --git a/ai/en/privacy-policy.texy b/ai/en/privacy-policy.texy deleted file mode 100644 index c7ed219f76..0000000000 --- a/ai/en/privacy-policy.texy +++ /dev/null @@ -1,65 +0,0 @@ -Privacy Policy for Nette Agent Plugins -************************************** - -
- -This policy covers the "Nette plugins":https://github.com/nette/agent-plugins for Claude Code and Codex: `nette`, `nette-lint`, `nette-dev` and `php-fixer`. The short version: **the plugins collect no data about you**. They have no servers, no telemetry, no analytics and no accounts, and they never send your code or any information about you over the network. - -
- -Last updated: September 24, 2026 - - -What the Plugins Are -==================== - -The plugins are open-source files that run entirely on your own computer: - -- **Skills** are plain text documents. Your AI tool reads them to learn Nette conventions. They contain no code that runs on their own. -- **Hooks** (in `nette-lint` and `php-fixer`) are small PHP scripts. Your AI tool starts them after it edits a file, and they check or reformat that file. - -The complete source code is public on "GitHub":https://github.com/nette/agent-plugins, so you can verify everything this page says. - - -What Data the Plugins Collect -============================= - -None. The plugins do not collect, store or transmit any personal data, source code, prompts, conversation content or usage statistics, and they do not share anything with Nette or with any third party. Since nothing is collected, nothing is retained. - - -What the Hooks Do on Your Computer -================================== - -When your AI tool edits a file, a hook does the following, all locally: - -- reads the edited file and, if present, the `.nette-claude.json` configuration from your project -- checks or fixes the file, either with the JSON parser bundled in the plugin or with a tool from your project or your system: `php -l`, `latte-lint`, `neon-lint`, DressCode or ESLint -- reports the result back to your AI tool, which is how the AI learns about a syntax error or a reformatted file - -The hooks change nothing but the edited file itself, and only when a fixer (DressCode or ESLint) reformats it. The tools they run may keep their usual local caches, such as the DressCode cache in your temporary directory. These tools belong to your project or system and behave according to your own configuration, including any extensions or plugins you have set up for them. - - -Third-Party Services -==================== - -A few things around the plugins do involve other services: - -- **Your AI tool.** Claude Code and Codex send your conversation, including skill text and hook results, to their providers. That processing is governed by the "Anthropic":https://www.anthropic.com/legal/privacy and "OpenAI":https://openai.com/policies/privacy-policy/ privacy policies, not by this one. -- **Installation and updates.** Your AI tool downloads the plugins from GitHub, which is governed by the "GitHub privacy statement":https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement. -- **Installing DressCode.** The `install-php-fixer` skill, which you have to start yourself, runs Composer to download DressCode from Packagist and GitHub. -- **ESLint.** The ESLint hook starts ESLint through `npx`. If your project does not have ESLint installed, `npx` may download it from the npm registry. This is an ordinary package download; none of your code is sent. - - -Changes to This Policy -====================== - -If the plugins ever start handling data, this page will be updated before that version is released, and the date at the top will change. The history of the page is public in the "documentation repository":https://github.com/nette/docs. - - -Contact -======= - -The plugins are maintained by David Grudl and the Nette Foundation. If you have a question about this policy or about the plugins, please open an issue at "github.com/nette/agent-plugins":https://github.com/nette/agent-plugins/issues. - -{{maintitle: Privacy Policy for Nette Agent Plugins}} -{{description: The Nette plugins for Claude Code and Codex collect no data. They run locally and have no servers, telemetry or analytics.}} diff --git a/ai/en/tips.texy b/ai/en/tips.texy deleted file mode 100644 index d348348e9a..0000000000 --- a/ai/en/tips.texy +++ /dev/null @@ -1 +0,0 @@ -{{redirect: vibe-coding}} diff --git a/ai/en/vibe-coding.texy b/ai/en/vibe-coding.texy deleted file mode 100644 index a79da50c08..0000000000 --- a/ai/en/vibe-coding.texy +++ /dev/null @@ -1,299 +0,0 @@ -Vibe Coding in Practice -*********************** - -
- -Vibe coding means describing what you want in plain language and letting the agent write the PHP. It is a powerful way to work, but like any tool you get better results when you know how to use it. This page collects practical advice from real-world experience with Nette. - -- How to write prompts that get better results -- Making the most of MCP Inspector -- Proven workflows for common tasks -- Mistakes to avoid - -
- - -Writing Better Prompts -====================== - - -The Art of Being Specific -------------------------- - -The single biggest improvement you can make is being specific. Compare these two prompts: - -**Vague prompt:** - -``` -Create a form -``` - -This gives the AI almost no context. What form? What fields? What validation? The AI has to make assumptions, and those assumptions might not match what you need. - -**Specific prompt:** - -``` -Create a ProductForm with: -- name: text field, required, max 100 characters -- price: float field, required, must be positive -- description: textarea, optional -- category: select from CategoryRow entities - -Use Bootstrap 5 rendering. The form should work for both creating new products and editing existing ones. -``` - -Now the AI knows exactly what you need and can generate code that works on the first try. - - -Point to Existing Patterns --------------------------- - -Your codebase already has patterns. Instead of explaining them, point the AI to examples: - -``` -Create an OrderPresenter. Follow the same patterns as ProductPresenter – -same structure, same way of handling forms, same template organization. -``` - -The AI will read ProductPresenter and replicate the patterns you're already using. - - -Let MCP Do the Heavy Lifting ----------------------------- - -If you have MCP Inspector installed (and you should!), don't explain your application - let the AI discover it: - -**Instead of:** - -``` -My product table has columns: id (int), name (varchar), price (decimal), -category_id (int, foreign key to category), created_at (datetime)... -``` - -**Just say:** - -``` -Generate an entity for the product table. -``` - -The AI will call `db_get_columns("product")` and see the actual schema. The generated entity will match your real database, including any columns you might have forgotten to mention. - - -Give Context About Your Goals ------------------------------ - -AI can't read your mind. If there's a reason behind your request, share it: - -``` -I need to optimize the product listing page. It's currently loading -all products at once, which is slow when there are thousands of items. -The page needs to support filtering by category and sorting by price or name. -``` - -This helps the AI suggest an appropriate solution (pagination, lazy loading, caching) rather than just blindly implementing what you asked for. - - -Working with MCP Inspector -========================== - -MCP Inspector is most powerful when you use it strategically. - - -Explore Before You Build ------------------------- - -Starting a new feature? Let the AI understand the context first: - -``` -I'm going to add order tracking. Before we start: -1. What services do I have related to orders? -2. What does my order table look like? -3. What routes handle order-related pages? -``` - -This gives the AI context about your existing code, so the new feature fits naturally. - - -Debug Smarter -------------- - -When something goes wrong, don't describe the error - let the AI see it: - -``` -Something broke. Check the Tracy log for the last exception -and tell me what went wrong. -``` - -The AI will call `tracy_get_log()` and `tracy_get_report()`, read the stack trace, and can often identify the problem faster than you could explain it. - - -Verify Before You Create ------------------------- - -Before adding new routes or links, verify what exists: - -``` -What presenter handles /admin/products/edit? I want to make sure -I'm not creating something that conflicts. -``` - - -Common Workflows -================ - -Here are proven approaches for common tasks. - - -Creating a Complete CRUD ------------------------- - -Don't ask for one piece at a time. Give the AI the full picture: - -``` -Create a complete CRUD for managing products: - -1. ProductPresenter with actions: list, add, edit, delete -2. ProductForm as a component (works for both add and edit) -3. Latte templates for all actions -4. Route suggestions - -Use the actual product table schema. Follow the patterns -in CategoryPresenter if it exists. -``` - - -Adding a New Feature --------------------- - -Break it down into phases that build on each other: - -``` -I need to add customer reviews to products. - -Phase 1 - Data layer: -- Look at the product table -- Suggest the review table schema -- Create ReviewRow entity - -Phase 2 - Business logic: -- Create ReviewService for CRUD operations -- Add methods to get reviews for a product - -Phase 3 - UI: -- Add review display to ProductPresenter:detail -- Create ReviewForm for submitting reviews -``` - - -Refactoring Existing Code -------------------------- - -Let the AI understand before it changes: - -``` -Analyze the OrderService class. What does each method do? -Are there any code smells or improvements you'd suggest? -``` - -Then: - -``` -The calculateTotal method is doing too much. Split it into -smaller methods while keeping the same public interface. -``` - - -Mistakes to Avoid -================= - - -Not Reviewing Generated Code ----------------------------- - -AI generates code quickly, but that doesn't mean every line is perfect. Always review: - -- **Database queries** - Are they efficient? Do they need indexes? -- **Security** - Is input validated? Are there authorization checks? -- **Edge cases** - What happens with empty data? Null values? - -AI is very good, but it's still an assistant. You're the developer responsible for the final code. - - -Ignoring Validation Feedback ----------------------------- - -If you're using the [Nette plugins for Claude Code or Codex |agent-plugins], they validate your PHP, Latte, NEON, JSON and JavaScript files automatically. When one reports an error, the AI knows about it. Instead of manually fixing the error, just say: - -``` -Fix the error you just created. -``` - -The AI will read the validation output and correct the mistake. - - -Forgetting Service Registration -------------------------------- - -When the AI creates a new service class, it sometimes forgets to register it in the DI container. If you get "Service not found" errors, ask: - -``` -What changes do I need in services.neon for the new OrderExportService? -``` - - -Asking for Too Much at Once ---------------------------- - -While AI can handle complex tasks, sometimes it helps to break them down: - -**Too ambitious:** - -``` -Build me a complete e-commerce system with product catalog, -shopping cart, checkout, payments, order tracking, and admin panel. -``` - -**Better approach:** - -``` -Let's build an e-commerce system step by step. Start with the product -catalog – I need to list, view, and admin products. -``` - - -When AI Excels (and When It Doesn't) -==================================== - - -AI is Great For ---------------- - -- **Boilerplate code** - Presenters, forms, entities, basic templates -- **Following patterns** - "Do it like X but for Y" -- **Understanding code** - "What does this method do?" -- **Generating tests** - Given implementation, create tests -- **Refactoring** - Improving code structure while keeping behavior - - -Consider Manual Coding For --------------------------- - -- **Complex business logic** - Domain rules that require careful thinking -- **Performance-critical code** - Algorithms that need optimization -- **Security-sensitive code** - Authentication, authorization, encryption -- **Novel solutions** - Things that don't follow existing patterns - -AI is a multiplier, not a replacement. It makes good developers faster, but it still needs a good developer guiding it. - - -Final Thoughts -============== - -The best way to learn AI-assisted development is to practice. Start with simple tasks, pay attention to what works, and gradually take on more complex projects. - -And remember: the AI is your assistant. You're still the developer. You make the decisions, you review the code, and you're responsible for the quality of the final product. - -Happy coding! - -{{maintitle: Vibe Coding in PHP with Nette: Prompts and Best Workflows}} -{{description: Practical advice for vibe coding in PHP with Nette: writing prompts that work, using MCP Inspector well, proven workflows, and mistakes to avoid.}} diff --git a/application/cs/@home.texy b/application/cs/@home.texy deleted file mode 100644 index 04526db5f2..0000000000 --- a/application/cs/@home.texy +++ /dev/null @@ -1,88 +0,0 @@ -Nette Application -***************** - -.[perex] -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 ---------- - -Knihovnu stáhnete a nainstalujete pomocí nástroje [Composer|best-practices:composer]: - -```shell -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.3+ 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 - - -Kompatibilita s PHP -------------------- - -| verze | kompatibilní s PHP -|-----------|------------------- -| Nette Application 3.3 | PHP 8.3 - 8.5 -| Nette Application 3.2 | PHP 8.1 - 8.5 -| 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 - -Platí pro poslední patch verze. - -{{maintitle: Nette Application: PHP framework pro webové aplikace}} -{{description: Jádro frameworku Nette: obousměrný router, presentery, znovupoužitelné komponenty se signály a bezpečné šablony Latte pro moderní webové aplikace.}} diff --git a/application/cs/@left-menu.texy b/application/cs/@left-menu.texy deleted file mode 100644 index 8848df94ca..0000000000 --- a/application/cs/@left-menu.texy +++ /dev/null @@ -1,24 +0,0 @@ -Nette Application -***************** -- [Úvod |@home] -- [Jak fungují aplikace? |how-it-works] -- [Bootstrapping] -- [Presentery |presenters] -- [Šablony |templates] -- [Adresářová struktura |directory-structure] -- [Routování |routing] -- [Vytváření odkazů URL |creating-links] -- [Interaktivní komponenty |components] -- [AJAX & snippety |ajax] -- [Multiplier |multiplier] -- [Konfigurace |configuration] -- [Upgrade |upgrading] - - -Další četba -*********** -- [Proč používat Nette? |www:10-reasons-why-nette] -- [Instalace |nette:installation] -- [Píšeme první aplikaci! |quickstart:] -- [Návody a postupy |best-practices:] -- [Řešení problémů |nette:troubleshooting] diff --git a/application/cs/@meta.texy b/application/cs/@meta.texy deleted file mode 100644 index 462d9add80..0000000000 --- a/application/cs/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokumentace}} diff --git a/application/cs/ajax.texy b/application/cs/ajax.texy deleted file mode 100644 index 5f0c89bf28..0000000000 --- a/application/cs/ajax.texy +++ /dev/null @@ -1,268 +0,0 @@ -AJAX & 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 -- nástroje pro debugování AJAXových požadavků - -
- - -AJAXový požadavek -================= - -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ě prohlížeče inicializujeme AJAXový požadavek pomocí funkce `fetch()`: - -```js -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -.then(response => response.json()) -.then(payload => { - // zpracování odpovědi -}); -``` - -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 -public function actionExport(): void -{ - $this->sendJson($this->model->getData()); -} -``` - -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 -{ - if ($this->isAjax()) { - $this->template->setFile('path/to/ajax.latte'); - } - // ... -} -``` - - -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, neboli výstřižky, umožňují 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 jako JSON payload s polem snippetů. 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. Pokud se pomocí `redrawControl()` žádný snippet neinvaliduje, Nette i na AJAXový požadavek vrátí celou stránku - snippety se posílají jen při invalidaci. - - -Naja ----- - -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 bundlery jako Webpack, Rollup, Vite, Parcel a dalšími): - -```shell -npm install naja -``` - -…nebo přímo vložte do šablony stránky: - -```latte - -``` - -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`: - -```latte -Go - -
- -
- -nebo - -
- -
-``` - - -Překreslení snippetů --------------------- - -Každý objekt třídy [Control |components] (včetně samotného Presenteru) eviduje, zda došlo ke změnám vyžadujícím jeho překreslení. K tomu slouží metoda `redrawControl()`: - -```php -public function handleLogin(string $user): void -{ - // po přihlášení je potřeba překreslit relevantní část - $this->redrawControl(); - // ... -} -``` - -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: - -```php -// invaliduje snippet 'header' -$this->redrawControl('header'); -``` - -Případnou invalidaci lze zrušit druhým parametrem `$redraw`: voláním `$this->redrawControl('header', redraw: false)` označíte snippet jako nevyžadující překreslení. Úplná signatura je `redrawControl(?string $snippet = null, bool $redraw = true)`. - - -Snippety v Latte ----------------- - -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} -

Hello ...

-{/snippet} -``` - -Snippet vytvoří v HTML stránce element `
` se speciálním vygenerovaným `id`. Při překreslení snippetu se pak aktualizuje obsah tohoto elementu. Proto je nutné, aby se při prvotním vykreslení stránky 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 -
-

Hello ...

-
-``` - - -Oblasti snippetů ----------------- - -Názvy snippetů mohou být také výrazy: - -```latte -{foreach $items as $id => $item} -
  • {$item}
  • -{/foreach} -``` - -Samo o sobě jde o nefunkční mezikrok: dynamický snippet vykreslený mimo statický `{snippet}` nebo `{snippetArea}` vyvolá `E_USER_WARNING` s hláškou *Dynamic snippets are allowed only inside static snippet/snippetArea.* Napravíme to níže. - -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}`: - -```latte -
      - {foreach $items as $id => $item} -
    • {$item}
    • - {/foreach} -
    -``` - -A necháme překreslit jak samotný snippet, tak i celou nadřazenou oblast: - -```php -$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. - -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 -{snippetArea include} - {include 'included.latte'} -{/snippetArea} -``` - -```latte -{* included.latte *} -{snippet item} - ... -{/snippet} -``` - -```php -$this->redrawControl('include'); -$this->redrawControl('item'); -``` - - -Snippety v komponentách ------------------------ - -Snippety můžete vytvářet i v [komponentách|components] a Nette je bude automaticky překreslovat. Ale platí tu určité omezení: při překreslování snippetů Nette volá metodu `render()` bez parametrů. Tedy nebude fungovat předávání parametrů v šabloně: - -```latte -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 actionDelete(int $id): void -{ - // ... - if ($this->isAjax()) { - $this->payload->message = 'Success'; - } -} -``` - - -Přesměrování ------------- - -Během AJAXového požadavku metody `redirect()` a `redirectUrl()` neposílají HTTP přesměrování. Místo toho zapíšou cílovou URL do payloadu (datový objekt odesílaný v odpovědi na AJAX), konkrétně do vlastnosti `payload.redirect`, a odešlou jej; samotné přesměrování pak provede klientská knihovna (Naja). - - -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 -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ě: - -```php -public function handleFoo(int $bar): void -{ -} -``` - - -Další četba -=========== - -- [Dynamické snippety |best-practices:dynamic-snippets] - -{{maintitle: AJAX a snippety v Nette: překreslení části stránky}} -{{description: Překreslete jen to, co se změnilo, bez načtení celé stránky: snippety v šablonách Latte, AJAXové požadavky s knihovnou Naja a předávání parametrů z prohlížeče.}} diff --git a/application/cs/bootstrapping.texy b/application/cs/bootstrapping.texy deleted file mode 100644 index b5af5d8fd5..0000000000 --- a/application/cs/bootstrapping.texy +++ /dev/null @@ -1,315 +0,0 @@ -Bootstrapping -************* - -
    - -Bootstrapping je proces inicializace prostředí aplikace, vytvoření kontejneru pro dependency injection (DI) a spuštění aplikace. Budeme probírat: - -- jak třída Bootstrap inicializuje prostředí -- jak jsou aplikace konfigurovány pomocí NEON souborů -- jak rozlišovat mezi produkčním a vývojářským režimem -- jak vytvořit a nakonfigurovat DI kontejner - -
    - - -Aplikace, ať už jde o ty webové nebo skripty spouštěné z příkazové řádky, začínají svůj běh nějakou formou inicializace prostředí. V dávných dobách to míval na starosti soubor s názvem třeba `include.inc.php`, který prvotní soubor inkludoval. V moderních Nette aplikacích jej nahradila třída `Bootstrap`, kterou jakožto součást aplikace najdete v souboru `app/Bootstrap.php`. Může vypadat kupříkladu takto: - -```php -namespace App; - -use Nette; -use Nette\Bootstrap\Configurator; - -class Bootstrap -{ - private Configurator $configurator; - private string $rootDir; - - public function __construct() - { - $this->rootDir = dirname(__DIR__); - // Konfigurátor je zodpovědný za nastavení prostředí aplikace a služeb. - $this->configurator = new Configurator; - // Nastaví adresář pro dočasné soubory generované Nette (např. zkompilované šablony) - $this->configurator->setTempDirectory($this->rootDir . '/temp'); - } - - public function bootWebApplication(): Nette\DI\Container - { - $this->initializeEnvironment(); - $this->setupContainer(); - return $this->configurator->createContainer(); - } - - private function initializeEnvironment(): void - { - // Nette je chytré a vývojový režim se zapíná automaticky, - // nebo jej můžete povolit pro konkrétní IP adresu odkomentováním následujícího řádku: - // $this->configurator->setDebugMode('secret@23.75.345.200'); - - // Aktivuje Tracy: ultimátní "švýcarský nůž" pro ladění. - $this->configurator->enableTracy($this->rootDir . '/log'); - - // RobotLoader: automaticky načítá všechny třídy ve zvoleném adresáři - $this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); - } - - private function setupContainer(): void - { - // Načte konfigurační soubory - $this->configurator->addConfig($this->rootDir . '/config/common.neon'); - } -} -``` - - -index.php -========= - -Prvotní soubor je v případě webových aplikací `index.php`, který se nachází ve [veřejném adresáři |directory-structure#Veřejný adresář www] `www/`. Ten si nechá od třídy Bootstrap inicializovat prostředí a vyrobit DI kontejner. Poté z něj získá službu `Application`, která spustí webovou aplikaci: - -```php -$bootstrap = new App\Bootstrap; -// Inicializace prostředí + vytvoření DI kontejneru -$container = $bootstrap->bootWebApplication(); -// DI kontejner vytvoří objekt Nette\Application\Application -$application = $container->getByType(Nette\Application\Application::class); -// Spuštění aplikace Nette a zpracování příchozího požadavku -$application->run(); -``` - -.[note] -Objekt `$application` během zpracování požadavku vyvolává [události |nette:glossary#události] - `onStartup`, `onRequest`, `onPresenter`, `onResponse`, `onShutdown` a `onError` (při nezachycené výjimce). Můžete k nim připojit handlery, což se hodí pro logování nebo monitoring napříč aplikací. - -Jak vidno, s nastavením prostředí a vytvořením dependency injection (DI) kontejneru pomáhá třída [api:Nette\Bootstrap\Configurator], kterou si nyní blíže představíme. - - -Vývojářský vs produkční režim -============================= - -Nette se chová různě podle toho, zda běží na vývojářském nebo produkčním serveru: - -🛠️ Vývojářský režim (Development): - - Zobrazuje Tracy debugbar s užitečnými informacemi (SQL dotazy, čas vykonání, použitá paměť) - - Při chybě zobrazí detailní chybovou stránku s voláním funkcí a obsahem proměnných - - Automaticky obnovuje cache při změně Latte šablon, úpravě konfiguračních souborů atd. - - -🚀 Produkční režim (Production): - - Nezobrazuje žádné ladicí informace, všechny chyby zapisuje do logu - - Při chybě zobrazí ErrorPresenter nebo obecnou stránku "Server Error" - - Cache se nikdy automaticky neobnovuje! - - Optimalizovaný pro rychlost a bezpečnost - - -Volba režimu se provádí autodetekcí, takže obvykle není potřeba nic konfigurovat nebo ručně přepínat: - -- vývojářský režim: na localhostu (IP adresa `127.0.0.1` nebo `::1`), pokud není přítomná proxy (tj. její HTTP hlavička) -- produkční režim: všude jinde - -Pokud chceme vývojářský režim povolit i v dalších případech, například programátorům přistupujícím z konkrétní IP adresy, použijeme `setDebugMode()`: - -```php -$this->configurator->setDebugMode('23.75.345.200'); // lze uvést i pole IP adres -``` - -Rozhodně doporučujeme kombinovat IP adresu s cookie. Do cookie `nette-debug` uložíme tajný token, např. `secret1234`, a tímto způsobem aktivujeme vývojářský režim pro programátory přistupující z konkrétní IP adresy a zároveň mající v cookie zmíněný token: - -```php -$this->configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Vývojářský režim můžeme také vypnout úplně, i pro localhost: - -```php -$this->configurator->setDebugMode(false); -``` - -Pozor, hodnota `true` zapne vývojářský režim natvrdo, což se nikdy nesmí stát na produkčním serveru. - -Autodetekci uvnitř zajišťuje statická metoda `Configurator::detectDebugMode()`, kterou můžete zavolat i sami, například pro detekci vývojářského režimu mimo konfigurátor. Přijímá volitelný whitelist IP adres nebo názvů počítačů a vrací, zda má aktuální požadavek běžet ve vývojářském režimu: - -```php -$debug = Nette\Bootstrap\Configurator::detectDebugMode('23.75.345.200'); -``` - - -Debugovací nástroj Tracy -======================== - -Pro snadné debugování ještě zapneme skvělý nástroj [Tracy |tracy:]. Ve vývojářském režimu chyby vizualizuje a v produkčním režimu chyby loguje do uvedeného adresáře: - -```php -$this->configurator->enableTracy($this->rootDir . '/log'); -``` - - -Dočasné soubory -=============== - -Nette využívá cache pro DI kontejner, RobotLoader, šablony atd. Proto je nutné nastavit cestu k adresáři, kam se bude cache ukládat: - -```php -$this->configurator->setTempDirectory($this->rootDir . '/temp'); -``` - -Na Linuxu nebo macOS nastavte adresářům `log/` a `temp/` [práva pro zápis |nette:troubleshooting#Nastavení práv adresářů]. - - -RobotLoader -=========== - -Zpravidla budeme chtít automaticky načítat třídy pomocí [RobotLoaderu |robot-loader:], musíme ho tedy nastartovat a necháme jej načítat třídy z adresáře, kde je umístěný `Bootstrap.php` (tj. `__DIR__`), a všech podadresářů: - -```php -$this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); -``` - -Alternativní přístup je nechat třídy načítat pouze přes [Composer |best-practices:composer] při dodržení PSR-4. - - -Timezone -======== - -Přes konfigurátor můžete nastavit výchozí časovou zónu. - -```php -$this->configurator->setTimeZone('Europe/Prague'); -``` - - -Konfigurace DI kontejneru -========================= - -Součástí bootovacího procesu je vytvoření DI kontejneru neboli továrny na objekty, což je srdce celé aplikace. Jde vlastně o PHP třídu, kterou vygeneruje Nette a uloží do adresáře s cache. Továrna vyrábí klíčové objekty aplikace a pomocí konfiguračních souborů jí instruujeme, jak je má vytvářet a nastavovat, čímž ovlivňujeme chování celé aplikace. - -Konfigurační soubory se obvykle zapisují ve formátu [NEON |neon:format]. V samostatné kapitole se dočtete, [co vše lze konfigurovat |nette:configuring]. - -.[tip] -Ve vývojářském režimu se kontejner automaticky aktualizuje při každé změně kódu nebo konfiguračních souborů. V produkčním režimu se vygeneruje jen jednou a změny se kvůli maximalizaci výkonu nekontrolují. - -Zatímco `createContainer()` kontejner vytvoří a vrátí jeho instanci, metoda `loadContainer()` vrátí jen název vygenerované třídy kontejneru, kterou si pak můžete vytvořit sami. To se hodí v pokročilých scénářích. - -Konfigurační soubory načteme pomocí `addConfig()`: - -```php -$this->configurator->addConfig($this->rootDir . '/config/common.neon'); -``` - -Pokud chceme přidat více konfiguračních souborů, můžeme funkci `addConfig()` zavolat vícekrát. - -```php -$configDir = $this->rootDir . '/config'; -$this->configurator->addConfig($configDir . '/common.neon'); -$this->configurator->addConfig($configDir . '/services.neon'); -if (PHP_SAPI === 'cli') { - $this->configurator->addConfig($configDir . '/cli.php'); -} -``` - -Název `cli.php` není překlep, konfigurace může být zapsaná také v PHP souboru, který ji vrátí jako pole. - -Také můžeme přidat další konfigurační soubory v [sekci `includes` |dependency-injection:configuration#Vkládání souborů]. - -Pokud se v konfiguračních souborech objeví prvky se stejnými klíči, budou přepsány, nebo v případě [polí sloučeny |dependency-injection:configuration#Slučování]. Později vkládaný soubor má vyšší prioritu než předchozí. Soubor, ve kterém je sekce `includes` uvedena, má vyšší prioritu než v něm inkludované soubory. - - -Statické parametry ------------------- - -Parametry používané v konfiguračních souborech můžeme definovat [v sekci `parameters` |dependency-injection:configuration#Parametry] a také je předávat (či přepisovat) metodou `addStaticParameters()` (má starší, dnes zavržený (deprecated) alias `addParameters()`). Důležité je, že různé hodnoty parametrů způsobí vygenerování dalších DI kontejnerů, tedy dalších tříd. - -```php -$this->configurator->addStaticParameters([ - 'projectId' => 23, -]); -``` - -Na parametr `projectId` se lze v konfiguraci odkázat obvyklým zápisem `%projectId%`. - - -Dynamické parametry -------------------- - -Do kontejneru můžeme přidat i dynamické parametry, jejichž různé hodnoty na rozdíl od statických parametrů nezpůsobí generování nových DI kontejnerů. - -```php -$this->configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Jednoduše tak můžeme přidat např. environmentální proměnné, na které se pak lze v konfiguraci odkázat zápisem `%env.variable%`. - -```php -$this->configurator->addDynamicParameters([ - 'env' => getenv(), -]); -``` - - -Výchozí parametry ------------------ - -V konfiguračních souborech můžete využít tyto parametry: - -- `%appDir%` je absolutní cesta k adresáři se souborem `Bootstrap.php` -- `%wwwDir%` je absolutní cesta k adresáři se vstupním souborem `index.php` -- `%tempDir%` je absolutní cesta k adresáři pro dočasné soubory -- `%vendorDir%` je absolutní cesta k adresáři, kam Composer instaluje knihovny -- `%rootDir%` je absolutní cesta ke kořenovému adresáři projektu -- `%baseUrl%` je absolutní URL ke kořenovému adresáři (dynamický parametr vyhodnocovaný za běhu; mimo HTTP požadavek se odvozuje z [http: baseUrl |http:configuration#Základní URL aplikace]) -- `%debugMode%` udává, zda je aplikace v debugovacím režimu -- `%consoleMode%` udává, zda request přišel přes příkazovou řádku - - -Importované služby ------------------- - -Nyní už jdeme hlouběji. Ačkoliv je smyslem DI kontejneru objekty vyrábět, výjimečně může vzniknout potřeba do kontejneru existující objekt vložit. Uděláme to tak, že službu definujeme s příznakem `imported: true`. - -```neon -services: - myservice: - type: App\Model\MyCustomService - imported: true -``` - -A v bootstrapu do kontejneru vložíme objekt: - -```php -$this->configurator->addServices([ - 'myservice' => new App\Model\MyCustomService('foobar'), -]); -``` - - -Odlišné prostředí -================= - -Nebojte se upravit třídu Bootstrap podle svých potřeb. Metodě `bootWebApplication()` můžete přidat parametry pro rozlišení webových projektů. Nebo můžeme doplnit další metody, například `bootTestEnvironment()`, která inicializuje prostředí pro jednotkové testy, `bootConsoleApplication()` pro skripty volané z příkazové řádky atd. - -```php -public function bootTestEnvironment(): Nette\DI\Container -{ - Tester\Environment::setup(); // inicializace Nette Testeru - $this->setupContainer(); - return $this->configurator->createContainer(); -} - -public function bootConsoleApplication(): Nette\DI\Container -{ - $this->configurator->setDebugMode(false); - $this->initializeEnvironment(); - $this->setupContainer(); - return $this->configurator->createContainer(); -} -``` - -{{maintitle: Bootstrapping aplikace v Nette: od index.php po DI kontejner}} -{{description: Jak se aplikace v Nette spouští: od index.php a třídy Configurator přes vývojářský režim, Tracy a dočasné soubory až po konfiguraci v NEON.}} diff --git a/application/cs/components.texy b/application/cs/components.texy deleted file mode 100644 index fb4be07928..0000000000 --- a/application/cs/components.texy +++ /dev/null @@ -1,507 +0,0 @@ -Interaktivní komponenty -*********************** - -
    - -Komponenty jsou samostatné znovupoužitelné objekty, které vkládáme do stránek. Mohou to být formuláře, datagridy, ankety, vlastně cokoliv, co má smysl používat opakovaně. Ukážeme si: - -- jak používat komponenty? -- jak je psát? -- co jsou to signály? - -
    - -Nette má v sobě vestavěný komponentový systém. Něco podobného mohou pamětníci znát z Delphi nebo ASP.NET Web Forms, na něčem vzdáleně podobném je postaven React nebo Vue.js. Nicméně ve světě PHP frameworků jde o unikátní záležitost. - -Přitom komponenty zásadním způsobem ovlivňují přístup k tvorbě aplikací. Můžete totiž stránky skládat z předpřipravených jednotek. Potřebujete v administraci datagrid? Najdete jej na [Componette |https://componette.org/search/component], repositáři open-source doplňků (tedy nejen komponent) pro Nette a jednoduše vložíte do presenteru. - -Do presenteru můžete začlenit libovolný počet komponent. A do některých komponent můžete vkládat další komponenty. Vzniká tak komponentový strom, jehož kořenem je presenter. - - -Tovární metody -============== - -Jak se do presenteru komponenty vkládají a následně používají? Obvykle pomocí továrních metod. - -Továrna na komponenty představuje elegantní způsob, jak komponenty vytvářet teprve ve chvíli, kdy jsou skutečně potřeba (lazy / on demand). Celé kouzlo spočívá v implementaci metody s názvem `createComponent()`, kde `` je název vytvářené komponenty; tato metoda komponentu vytvoří a vrátí. - -```php .{file:DefaultPresenter.php} -class DefaultPresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentPoll(): PollControl - { - $poll = new PollControl; - $poll->items = $this->items; - return $poll; - } -} -``` - -Díky tomu, že jsou všechny komponenty vytvářeny v samostatných metodách, získává kód na přehlednosti. - -.[note] -Názvy komponent začínají vždy malým písmenem, přestože se v názvu metody píší s velkým. - -Továrny nikdy nevoláme přímo, zavolají se samy ve chvíli, kdy komponentu poprvé použijeme. Díky tomu je komponenta vytvořena ve správný okamžik a pouze v případě, když je skutečně potřeba. Pokud komponentu nepoužijeme (třeba při AJAXovém požadavku, kdy se přenáší jen část stránky, nebo při cachování šablony), nevytvoří se vůbec a ušetříme výkon serveru. - -```php .{file:DefaultPresenter.php} -// přistoupíme ke komponentě a pokud to bylo poprvé, -// zavolá se createComponentPoll(), která ji vytvoří -$poll = $this->getComponent('poll'); -// alternativní syntax: $poll = $this['poll']; -``` - -V šabloně je možné vykreslit komponentu pomocí značky [{control} |#Vykreslení]. Není proto potřeba manuálně komponenty předávat do šablony. - -```latte -

    Hlasujte

    - -{control poll} -``` - -.[tip] -Pro dynamické vytváření proměnného počtu komponent použijte [Multiplier |multiplier]. - -Tovární metody `createComponent()` nefungují jen v presenterech. Stejným způsobem můžete komponentu vložit i do jiné komponenty a skládat je tak do sebe - hodí se to například pro samostatně vykreslovaný formulář uvnitř komponenty. - - -Hollywood style -=============== - -Komponenty běžně používají jednu svěží techniku, které rádi říkáme Hollywood style. Určitě znáte okřídlenou větu, kterou tak často slyší účastníci filmových konkurzů: "Nevolejte nám, my vám zavoláme". A právě o tu jde. - -V Nette totiž místo toho, abyste se museli neustále na něco ptát ("byl formulář odeslaný?", "bylo to validní?" nebo "stiskl uživatel tohle tlačítko?"), řeknete frameworku "až se to stane, zavolej tuhle metodu" a necháte další práci na něm. Pokud programujete v JavaScriptu, tento styl programování důvěrně znáte. Píšete funkce, které se volají, až nastane určitá událost. A jazyk jim předá příslušné parametry. - -Tohle zcela mění pohled na psaní aplikací. Čím víc úkolů můžete nechat na frameworku, tím méně máte práce vy. A tím méně toho můžete třeba opomenout. - - -Píšeme komponentu -================= - -Pod pojmem komponenta obvykle myslíme potomka třídy [api:Nette\Application\UI\Control]. (Přesnější by tedy bylo používat termín "controls", ale "kontroly" mají v češtině zcela jiný význam a spíš se ujaly "komponenty".) Samotný presenter [api:Nette\Application\UI\Presenter] je mimochodem také potomkem třídy `Control`. - -```php .{file:PollControl.php} -use Nette\Application\UI\Control; - -class PollControl extends Control -{ -} -``` - - -Vykreslení -========== - -Už víme, že k vykreslení komponenty se používá značka `{control componentName}`. Ta vlastně zavolá metodu `render()` komponenty, ve které se postaráme o vykreslení. K dispozici máme, úplně stejně jako v presenteru, [Latte šablonu|templates] v proměnné `$this->template`, do které předáme parametry. Na rozdíl od presenteru musíme uvést soubor se šablonou a nechat ji vykreslit: - -```php .{file:PollControl.php} -public function render(): void -{ - // vložíme do šablony nějaké parametry - $this->template->param = $value; - // a vykreslíme ji - $this->template->render(__DIR__ . '/poll.latte'); -} -``` - -Značka `{control}` umožňuje do metody `render()` předat parametry: - -```latte -{control poll, $id, $message} -``` - -```php .{file:PollControl.php} -public function render(int $id, string $message): void -{ - // ... -} -``` - -Někdy se může komponenta skládat z několika částí, které chceme vykreslovat odděleně. Pro každou z nich si vytvoříme vlastní vykreslovací metodu, zde v příkladu třeba `renderPaginator()`: - -```php .{file:PollControl.php} -public function renderPaginator(): void -{ - // ... -} -``` - -A v šabloně ji pak vyvoláme pomocí: - -```latte -{control poll:paginator} -``` - -Pro lepší pochopení je dobré vědět, jak se tato značka přeloží do PHP. - -```latte -{control poll} -{control poll:paginator, 123, 'hello'} -``` - -se přeloží jako: - -```php -$control->getComponent('poll')->render(); -$control->getComponent('poll')->renderPaginator(123, 'hello'); -``` - -Metoda `getComponent()` vrací komponentu `poll` a nad touto komponentou volá metodu `render()`, resp. `renderPaginator()`, pokud je ve značce za dvojtečkou uveden jiný způsob renderování. - -.[caution] -Pozor, pokud se v parametrech objeví **`=>`** mimo hranaté závorky, všechny parametry budou zabaleny do pole a předány jako první argument: - -```latte -{control poll, id: 123, message: 'hello'} -``` - -se přeloží jako: - -```php -$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']); -``` - -Vykreslení sub-komponenty: - -```latte -{control cartControl-someForm} -``` - -se přeloží jako: - -```php -$control->getComponent("cartControl-someForm")->render(); -``` - -Komponenty, stejně jako presentery, předávají do šablon několik užitečných proměnných automaticky: - -- `$basePath` je absolutní URL cesta ke kořenovému adresáři (např. `/eshop`) -- `$baseUrl` je absolutní URL ke kořenovému adresáři (např. `http://localhost/eshop`) -- `$user` je objekt [reprezentující uživatele |security:authentication] -- `$presenter` je aktuální presenter -- `$control` je aktuální komponenta -- `$flashes` je pole [zpráv |#Flash zprávy] zaslaných funkcí `flashMessage()` - - -Signál -====== - -Už víme, že navigace v Nette aplikaci spočívá v odkazování nebo přesměrování na dvojice `Presenter:action`. Ale co když jen chceme provést akci na **aktuální stránce**? Například změnit řazení sloupců v tabulce; smazat položku; přepnout světlý/tmavý režim; odeslat formulář; hlasovat v anketě; atd. - -Tomuto druhu požadavků se říká signály. A podobně jako akce vyvolávají metody `action()` nebo `render()`, signály volají metody `handle()`. Zatímco pojem akce (nebo view) souvisí čistě jen s presentery, signály se týkají všech komponent. A tedy i presenterů, protože `UI\Presenter` je potomkem `UI\Control`. - -```php -public function handleClick(int $x, int $y): void -{ - // ... zpracování signálu ... -} -``` - -Odkaz, který zavolá signál, vytvoříme obvyklým způsobem, tedy v šabloně atributem `n:href` nebo značkou `{link}`, v kódu metodou `link()`. Více v kapitole [Vytváření odkazů URL |creating-links#Odkazy na signál]. - -```latte -click here -``` - -Signál se vždy volá na aktuálním presenteru a akci, není možné jej vyvolat na jiném presenteru nebo jiné akci. - -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. - - -Snippety a AJAX -=============== - -Signály vám možná trošku připomínají AJAX: handlery, které se vyvolávají na aktuální stránce. A máte pravdu, signály se opravdu často volají pomocí AJAXu a následně přenášíme do prohlížeče pouze změněné části stránky. Neboli tzv. snippety. Více informací naleznete na [stránce věnované AJAXu |ajax]. - - -Flash zprávy -============ - -Komponenta má své vlastní úložiště flash zpráv nezávislé na presenteru. Jde o zprávy, které např. informují o výsledku operace. Důležitým rysem flash zpráv je to, že jsou v šabloně k dispozici i po přesměrování. I po zobrazení zůstanou živé ještě dalších 30 sekund - například pro případ, že by z důvodu chybného přenosu uživatel dal stránku obnovit - zpráva mu tedy hned nezmizí. - -Zasílání obstarává metoda [flashMessage |api:Nette\Application\UI\Control::flashMessage()]. Prvním parametrem je text zprávy (`string`, `Stringable`) nebo objekt `stdClass` reprezentující zprávu. Nepovinným druhým parametrem je její typ (error, warning, info apod.). Metoda `flashMessage()` vrací instanci flash zprávy jako objekt `stdClass`, ke kterému je možné přidávat další informace. - -```php -$this->flashMessage('Položka byla smazána.'); -$this->redirect(/* ... */); // a přesměrujeme -``` - -Šabloně jsou tyto zprávy k dispozici v proměnné `$flashes` jako objekty `stdClass`, které obsahují vlastnosti `message` (text zprávy), `type` (typ zprávy) a mohou obsahovat již zmíněné uživatelské informace. Vykreslíme je třeba takto: - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -Přesměrování po zpracování 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 akci -``` - -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 -====================== - -Persistentní parametry slouží k udržování stavu v komponentách mezi různými požadavky. Jejich hodnota zůstává stejná i po kliknutí na odkaz. Na rozdíl od dat v session se přenášejí v URL. A to zcela automaticky, včetně odkazů vytvořených v jiných komponentách na téže stránce. - -Máte např. komponentu pro stránkování obsahu. Takových komponent může být na stránce několik. A přejeme si, aby po kliknutí na odkaz zůstaly všechny komponenty na své aktuální stránce. Proto z čísla stránky (`page`) uděláme persistentní parametr. - -Vytvoření persistentního parametru je v Nette nesmírně jednoduché. Stačí vytvořit veřejnou property a označit ji atributem (dříve se používalo `/** @persistent */`): - -```php -use Nette\Application\Attributes\Persistent; // tento řádek je důležitý - -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; // musí být public -} -``` - -U property doporučujeme uvádět i datový typ (např. `int`) a můžete uvést i výchozí hodnotu. Hodnoty parametrů lze [validovat |#Validace persistentních parametrů]. - -Při vytváření odkazu lze persistentnímu parametru změnit hodnotu: - -```latte -next -``` - -Nebo jej lze *vyresetovat*, tj. odstranit z URL. Pak bude nabývat svou výchozí hodnotu: - -```latte -reset -``` - - -Persistentní komponenty -======================= - -Nejen parametry, ale také komponenty mohou být persistentní. Jejich persistentní parametry se pak přenáší i mezi různými akcemi presenteru nebo mezi více presentery. Persistentní komponenty značíme atributem u třídy presenteru. Třeba takto označíme komponenty `calendar` a `poll`: - -```php -use Nette\Application\Attributes\Persistent; - -#[Persistent('calendar', 'poll')] -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Podkomponenty uvnitř těchto komponent není třeba značit, stanou se persistentní také. - -Starší anotace `@persistent` stále funguje, ale je zastaralá a vyhazuje varování: - -```php -/** - * @persistent(calendar, poll) - */ -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Komponenty se závislostmi -========================= - -Jak vytvářet komponenty se závislostmi, aniž bychom si "zaneřádili" presentery, které je budou používat? Díky chytrým vlastnostem DI kontejneru v Nette lze stejně jako u používání klasických služeb nechat většinu práce na frameworku. - -Vezměme si jako příklad komponentu, která má závislost na službě `PollFacade`: - -```php -class PollControl extends Control -{ - public function __construct( - private int $id, // ID ankety, pro kterou vytváříme komponentu - private PollFacade $facade, - ) { - } - - public function handleVote(int $voteId): void - { - $this->facade->vote($this->id, $voteId); - // ... - } -} -``` - -Pokud bychom psali klasickou službu, nebylo by co řešit. O předání všech závislostí by se neviditelně postaral DI kontejner. Jenže s komponentami obvykle zacházíme tak, že jejich novou instanci vytváříme přímo v presenteru v [továrních metodách |#Tovární metody] `createComponent…()`. Ale předávat si všechny závislosti všech komponent do presenteru, abychom je pak předali komponentám, je těžkopádné. A toho napsaného kódu… - -Logickou otázkou je, proč prostě nezaregistrujeme komponentu jako klasickou službu, nepředáme ji do presenteru a poté v metodě `createComponent…()` nevracíme? Takový přístup je ale nevhodný, protože komponentu chceme mít možnost vytvářet klidně i vícekrát. - -Správným řešením je napsat pro komponentu továrnu, tedy třídu, která nám komponentu vytvoří: - -```php -class PollControlFactory -{ - public function __construct( - private PollFacade $facade, - ) { - } - - public function create(int $id): PollControl - { - return new PollControl($id, $this->facade); - } -} -``` - -Takhle továrnu zaregistrujeme do našeho kontejneru v konfiguraci: - -```neon -services: - - PollControlFactory -``` - -a nakonec ji použijeme v našem presenteru: - -```php -class PollPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private PollControlFactory $pollControlFactory, - ) { - } - - protected function createComponentPollControl(): PollControl - { - $pollId = 1; // můžeme si předat náš parametr - return $this->pollControlFactory->create($pollId); - } -} -``` - -Skvělé je, že Nette DI takovéhle jednoduché továrny umí [generovat |dependency-injection:factory], takže místo jejího celého kódu stačí napsat jenom její rozhraní: - -```php -interface PollControlFactory -{ - public function create(int $id): PollControl; -} -``` - -A to je vše. Nette vnitřně tento interface naimplementuje a předá do presenteru, kde jej už můžeme používat. Magicky nám právě do naší komponenty přidá i parametr `$id` a instanci třídy `PollFacade`. - - -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 |ajax#Snippety] má vykreslit při AJAXovém požadavku (snippety) -3) má schopnost ukládat svůj stav do URL (persistentní parametry) -4) má schopnost reagovat na uživatelské akce (signály) -5) vytváří hierarchickou strukturu (kde kořenem je presenter) - -Každou z těchto funkcí obstarává některá z tříd dědičné linie. Vykreslování (1 + 2) má na starosti [api:Nette\Application\UI\Control], začlenění do [životního cyklu |presenters#Životní cyklus presenteru] (3, 4) třída [api:Nette\Application\UI\Component] a vytváření hierarchické struktury (5) třídy [Container a Component |component-model:]. - -``` -Nette\ComponentModel\Component { IComponent } -| -+- Nette\ComponentModel\Container { IContainer } - | - +- Nette\Application\UI\Component { SignalReceiver, StatePersistent } - | - +- Nette\Application\UI\Control { Renderable } - | - +- Nette\Application\UI\Presenter { IPresenter } -``` - - -Životní cyklus komponenty -------------------------- - -[* lifecycle-component.svg *] *** *Životní cyklus komponenty* .<> - - -Validace persistentních parametrů ---------------------------------- - -Hodnoty [persistentních parametrů |#Persistentní parametry] přijatých z URL zapisuje do properties metoda `loadState()`. Ta také kontroluje, zda hodnota odpovídá datovému typu uvedenému u property; jinak odpoví chybou 404 a stránka se nezobrazí. - -Nikdy slepě nevěřte persistentním parametrům, protože mohou být snadno uživatelem přepsány v URL. Takto například ověříme, zda je číslo stránky `$this->page` větší než 0. Vhodnou cestou je přepsat zmíněnou metodu `loadState()`: - -```php -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; - - public function loadState(array $params): void - { - parent::loadState($params); // zde se nastaví $this->page - // následuje vlastní kontrola hodnoty: - if ($this->page < 1) { - $this->error(); - } - } -} -``` - -Opačný proces, tedy sesbírání hodnot z persistentních properties, má na starosti metoda `saveState()`. - - -Připojení k presenteru ----------------------- - -Ve chvíli, kdy se komponenta stane součástí hierarchie presenteru, zavolají se její callbacky uložené v poli `$onAnchor`. Od tohoto okamžiku má komponenta k dispozici presenter, může bezpečně vytvářet odkazy, číst persistentní parametry apod. - -```php -$control->onAnchor[] = function ($control): void { - // komponenta má nyní k dispozici presenter -}; -``` - - -Signály do hloubky ------------------- - -Signál způsobí znovunačtení stránky úplně stejně jako při původním požadavku (kromě případu, kdy je volán AJAXem) a vyvolá metodu `signalReceived($signal)`, jejíž výchozí implementace ve třídě `Nette\Application\UI\Component` se pokusí zavolat metodu složenou ze slov `handle`. Další zpracování je na daném objektu. Objekty, které dědí od `Component` (tzn. `Control` a `Presenter`), reagují tak, že se snaží zavolat metodu `handle` s příslušnými parametry. - -Jinými slovy: vezme se definice funkce `handle` a všechny parametry, které přišly s požadavkem, a k argumentům se podle jména dosadí parametry z URL a pokusí se danou metodu zavolat. Např. jako parametr `$id` se předá hodnota z parametru `id` v URL, jako `$something` se předá `something` z URL, atd. A pokud metoda neexistuje, metoda `signalReceived` vyvolá [výjimku |api:Nette\Application\UI\BadSignalException]. - -Kromě parametrů z URL bere signál v potaz i parametry odeslané v **POST těle požadavku**. To se hodí, protože signály se často volají přes JavaScript, kde je přirozené posílat data metodou POST. Pokud ale parametr stejného jména přijde jak z URL, tak z POST těla, má přednost hodnota **z URL**. Vyhněte se proto tomu, aby POST pole mělo stejný název jako parametr z URL nebo z routy, jinak by ho hodnota z URL tiše přepsala. Parametry signálu sdílejí společný prostor s parametry action metod a persistentními parametry, viz [Společný prostor parametrů |presenters#Společný prostor parametrů]. - -Signál může přijímat jakákoliv komponenta, presenter nebo objekt, který implementuje rozhraní `SignalReceiver` a je připojený do stromu komponent. - -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ím presenteru a akci s aktuálními parametry, parametry signálu se jen přidají. Navíc se přidává **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. - -Metoda [isSignalReceiver()|api:Nette\Application\UI\Presenter::isSignalReceiver()] ověří, zda je komponenta (první argument) příjemcem signálu (druhý argument). Druhý argument můžeme vynechat - pak zjišťuje, jestli je komponenta příjemcem jakéhokoliv signálu. Jako druhý parametr lze uvést `true` a tím ověřit, jestli je příjemcem nejen uvedená komponenta, ale také kterýkoliv její potomek. - -V kterékoliv fázi předcházející `handle` můžeme vykonat signál manuálně zavoláním metody [processSignal()|api:Nette\Application\UI\Presenter::processSignal()], která má na starosti vyřízení signálu - vezme komponentu, která se určila jako příjemce signálu (pokud není určen příjemce signálu, je to presenter samotný) a pošle jí signál. - -Příklad: - -```php -if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) { - $this->processSignal(); -} -``` - -Tím je signál provedený předčasně a už se nebude znovu volat. - -{{maintitle: Interaktivní komponenty v Nette: signály a AJAX}} -{{description: Formulář, datagrid nebo anketu napíšete jednou a použijete kdekoliv. Tovární metody, vykreslení, signály, AJAX, flash zprávy a persistentní parametry.}} diff --git a/application/cs/configuration.texy b/application/cs/configuration.texy deleted file mode 100644 index 1ce20ce3f3..0000000000 --- a/application/cs/configuration.texy +++ /dev/null @@ -1,189 +0,0 @@ -Konfigurace aplikací -******************** - -.[perex] -Přehled konfiguračních voleb pro Nette Aplikace. - - -Application -=========== - -```neon -application: - # zobrazit "Nette Application" panel v Tracy BlueScreen? - debugger: ... # (bool) zapne se, je-li k dispozici Tracy - - # v produkci error-presenter zpracovává výjimky vždy; - # tato volba jej zapne i ve vývojářském režimu - catchExceptions: ... # (bool) výchozí je false - tj. ve vývoji vypnuto, v produkci vždy zapnuto - - # název error-presenteru - 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: ... - - # chybné odkazy negenerují varování? - # má efekt pouze ve vývojářském režimu - silentLinks: ... # (bool) výchozí je false -``` - -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 -``` - -Rozdělení se hodí proto, že obě situace jsou zásadně odlišné. Výjimka `BadRequestException` (kódy 4xx) znamená, že aplikace je v pořádku a jen návštěvník požádal o něco, co neexistuje. Můžete proto použít plnohodnotný presenter, který zobrazí přátelskou zprávu v layoutu vašeho webu. Naproti tomu chyba 5xx znamená, že se v aplikaci něco rozbilo a nevíte co. Presenter pro 5xx proto udržujte co nejjednodušší, aby při jeho vykreslování už nemohlo nic dalšího selhat - ideálně by neměl sahat na databázi, layout ani na přihlášeného uživatele. - -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í chybu `E_USER_WARNING`. Nastavením na `true` dojde k potlačení této chybové hlášky. V produkčním prostředí se `E_USER_WARNING` vyvolá vždy. Toto chování můžeme také ovlivnit nastavením proměnné presenteru [$invalidLinkMode |creating-links#Neplatné odkazy]. - -[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ů ---------------------------------- - -Nette automaticky přidává presentery jako služby do DI kontejneru, což zásadně zrychlí jejich vytváření. Jak Nette presentery dohledává, lze konfigurovat: - -```neon -application: - # hledat presentery v Composer class map? - scanComposer: ... # (bool) výchozí je true - - # maska, které musí vyhovovat název třídy a souboru - scanFilter: ... # (string) výchozí je '*Presenter' - - # ve kterých adresářích hledat presentery? - scanDirs: # (string[]|false) výchozí je '%appDir%' - - %vendorDir%/mymodule -``` - -Adresáře uvedené v `scanDirs` nepřepisují výchozí hodnotu `%appDir%`, ale doplňují ji, `scanDirs` tedy bude obsahovat obě cesty `%appDir%` a `%vendorDir%/mymodule`. Pokud bychom chtěli výchozí adresář vynechat, použijeme [vykřičník |dependency-injection:configuration#Slučování], který hodnotu přepíše: - -```neon -application: - scanDirs!: - - %vendorDir%/mymodule -``` - -Skenování adresářů lze vypnout uvedením hodnoty `false`. Presentery pak nejsou registrované jako služby, takže je nelze upravovat v sekci [decorator |dependency-injection:configuration#Decorator] a jejich vytváření je pomalejší. Úplné potlačení automatické registrace proto nedoporučujeme, protože sníží výkon aplikace. - - -Šablony Latte -============= - -Tímto nastavením lze globálně ovlivnit chování Latte v komponentách a presenterech. - -```neon -latte: - # zobrazit Latte panel v Tracy Baru pro hlavní šablonu (true) nebo všechny komponenty (all)? - debugger: ... # (true|false|'all') zapne se s Tracy (jen v debug módu) - - # generuje šablony s hlavičkou declare(strict_types=1) - strictTypes: ... # (bool) výchozí je false - - # zapne režim [striktního parseru |latte:develop#striktní režim] - strictParsing: ... # (bool) výchozí je false - - # omezí platnost proměnných na tělo cyklu - scopedLoopVariables: ... # (bool) výchozí je false - - # odstraní odsazení vzniklé zanořením v párových značkách - dedent: ... # (bool) výchozí je false - - # aktivuje [kontrolu vygenerovaného kódu |latte:develop#Kontrola vygenerovaného kódu] - phpLinter: ... # (string) výchozí je null - - # nastaví locale - locale: cs_CZ # (string) výchozí je null - - # třída objektu $this->template - templateClass: App\MyTemplateClass # výchozí je Nette\Bridges\ApplicationLatte\DefaultTemplate -``` - -Nová [rozšíření |latte:extending-latte#Latte Extension] můžete přidávat pomocí: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - - -Routování -========= - -Základní nastavení: - -```neon -routing: - # zobrazit routovací panel v Tracy Bar? - debugger: ... # (bool) zapne se s Tracy (jen v debug módu) - - # serializuje router do DI kontejneru - cache: ... # (bool) výchozí je false -``` - -Routování obvykle definujeme ve třídě [RouterFactory |routing#Kolekce rout]. Alternativně lze routy definovat také v konfiguraci pomocí dvojic `maska: akce`, ale tento způsob nenabízí tak širokou variabilitu v nastavení: - -```neon -routing: - routes: - 'detail/': Admin:Home:default - '/': Front:Home:default -``` - - -Konstanty -========= - -Vytváření PHP konstant. - -```neon -constants: - Foobar: 'baz' -``` - -Po nastartování aplikace bude vytvořena konstanta `Foobar`. - -.[note] -Konstanty by neměly sloužit jako jakési globálně dostupné proměnné. Pro předávání hodnot do objektů využijte [dependency injection |dependency-injection:passing-dependencies]. - - -PHP -=== - -Nastavení direktiv PHP. Přehled všech direktiv naleznete na [php.net |https://www.php.net/manual/en/ini.list.php]. - -```neon -php: - date.timezone: Europe/Prague -``` - - -Služby DI -========= - -Tyto služby se přidávají do DI kontejneru: - -| Název | Typ | Popis -|---------------------------------------------------------- -| `application.application` | [api:Nette\Application\Application] | [spouštěč celé aplikace |how-it-works#Nette Application] -| `application.linkGenerator` | [api:Nette\Application\LinkGenerator] | [LinkGenerator |creating-links#LinkGenerator] -| `application.presenterFactory` | [api:Nette\Application\IPresenterFactory] | továrna na presentery -| `application.###` | [api:Nette\Application\UI\Presenter] | jednotlivé presentery -| `routing.router` | [api:Nette\Routing\Router] | směrovač -| `latte.latteFactory` | [api:Nette\Bridges\ApplicationLatte\LatteFactory] | továrna objektu `Latte\Engine` -| `latte.templateFactory` | [api:Nette\Application\UI\TemplateFactory] | továrna pro [`$this->template` |templates] - -{{maintitle: Konfigurace Nette Application: volby v NEON}} -{{description: Konfigurace Nette Application v NEON: error presenter, mapování presenterů, šablony Latte, routování, konstanty, nastavení PHP a služby DI.}} diff --git a/application/cs/creating-links.texy b/application/cs/creating-links.texy deleted file mode 100644 index 126a11240a..0000000000 --- a/application/cs/creating-links.texy +++ /dev/null @@ -1,322 +0,0 @@ -Vytváření odkazů URL -******************** - -
    - -Tvořit odkazy v Nette je jednoduché, jako ukazovat prstem. Stačí jen namířit a framework už za vás všechnu práci udělá. Ukážeme si: - -- jak vytvářet odkazy v šablonách i jinde -- jak odlišit odkaz na aktuální stránku -- co s neplatnými odkazy - -
    - - -Díky [obousměrnému routování |routing] nebudete nikdy muset do šablon či kódu zapisovat natvrdo URL adresy vaší aplikace, které se mohou později měnit, nebo je komplikovaně skládat. V odkazu stačí uvést presenter a akci, předat případné parametry a framework už URL vygeneruje sám. Vlastně je to velice podobné, jako když voláte funkci. To se vám bude líbit. - - -V šabloně presenteru -==================== - -Nejčastěji vytváříme odkazy v šablonách a skvělým pomocníkem je atribut `n:href`: - -```latte -detail -``` - -Všimněte si, že místo HTML atributu `href` jsme použili [n:atribut |latte:syntax#n:atributy] `n:href`. Jeho hodnotou pak není URL, jak by tomu bylo v případě atributu `href`, ale název presenteru a akce. - -Kliknutí na odkaz je, zjednodušeně řečeno, něco jako zavolání metody `ProductPresenter::renderShow()`. A pokud má ve své signatuře parametry, můžeme ji volat s argumenty: - -```latte -detail produktu -``` - -Je možné předávat i pojmenované parametry. Následující odkaz předává parametr `lang` s hodnotou `cs`: - -```latte -detail produktu -``` - -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 `...`: - -```latte -{var $args = [$product->id, lang => cs]} -detail produktu -``` - -V odkazech se také automaticky předávají tzv. [persistentní parametry |presenters#Persistentní parametry]. - -Atribut `n:href` je velmi šikovný pro HTML značky ``. Chceme-li odkaz vypsat jinde, například v textu, použijeme `{link}`: - -```latte -Adresa je: {link Home:default} -``` - - -V kódu -====== - -K vytvoření odkazu v presenteru slouží metoda `link()`: - -```php -$url = $this->link('Product:show', $product->id); -``` - -Parametry lze předat také pomocí pole, kde lze uvést i pojmenované parametry: - -```php -$url = $this->link('Product:show', [$product->id, 'lang' => 'cs']); -``` - -Odkazy lze vytvářet i bez presenteru, od toho je tu [#LinkGenerator] a jeho metoda `link()`. - -Někdy potřebujete odkaz vytvořit hned, ale skutečnou URL sestavit až později. K tomu slouží metoda `lazyLink()`, která vrací objekt `Nette\Application\UI\Link`. Výhodou je, že tento objekt můžete předat dál, například do šablony, a než se vykreslí, můžete ještě upravit jeho parametry metodou `setParameter()`. Samotná URL se sestaví až ve chvíli, kdy se objekt převede na řetězec: - -```php -$link = $this->lazyLink('Product:show', $id); -// ... -echo $link; // URL se vygeneruje až zde -``` - - -Odkazy na presenter -=================== - -Pokud je cílem odkazu presenter a akce, má tuto syntaxi: - -``` -[//] [[[[:]module:]presenter:]action | this] [#fragment] -``` - -Formát podporují všechny značky Latte a všechny metody presenteru, které s odkazy pracují, tedy `n:href`, `{link}`, `{plink}`, `link()`, `lazyLink()`, `isLinkCurrent()`, `redirect()`, `redirectPermanent()`, `forward()`, `canonicalize()` a také [#LinkGenerator]. Takže i když je v příkladech použit `n:href`, mohla by tam být kterákoliv z funkcí. - -Základním tvarem je tedy `Presenter:action`: - -```latte -úvodní stránka -``` - -Pokud odkazujeme na akci aktuálního presenteru, můžeme jeho název vynechat: - -```latte -úvodní stránka -``` - -Pokud je cílem akce `default`, můžeme ji vynechat, ale dvojtečka musí zůstat: - -```latte -úvodní stránka -``` - -Odkazy mohou také směřovat do jiných [modulů |directory-structure#Presentery a šablony]. Zde se odkazy rozlišují na relativní do zanořeného submodulu, nebo absolutní. Princip je analogický k cestám na disku, jen místo lomítek jsou dvojtečky. Předpokládejme, že aktuální presenter je součástí modulu `Front`, potom zapíšeme: - -```latte -odkaz na Front:Shop:Product:show -odkaz na Admin:Product:show -``` - -Speciálním případem je odkaz [na sebe sama |#Odkaz na aktuální stránku], kdy jako cíl uvedeme `this`. - -```latte -refresh -``` - -Odkazovat můžeme na určitou část stránky přes tzv. fragment za znakem mřížky `#`: - -```latte -odkaz na Home:default a fragment #main -``` - -.{data-version:3.3.0} -Fragment lze nastavit i dynamicky jako argument s klíčem `#`. Jeho hodnota se automaticky zakóduje a má přednost před fragmentem uvedeným v cíli: - -```php -$this->link('Home:default', ['#' => $fragment]); -``` - - -Absolutní cesty -=============== - -Odkazy generované pomocí `link()` nebo `n:href` jsou vždy absolutní cesty (tj. začínají znakem `/`), ale nikoliv absolutní URL s protokolem a doménou jako `https://domain`. - -Pro vygenerování absolutní URL přidejte na začátek dvě lomítka (např. `n:href="//Home:"`). Nebo lze přepnout presenter, aby generoval jen absolutní odkazy nastavením `$this->absoluteUrls = true`. - -V šabloně lze také použít filtr `|absoluteUrl`, který relativní cestu převede na absolutní. - - -Odkaz na aktuální stránku -========================= - -Cíl `this` vytvoří odkaz na aktuální stránku: - -```latte -refresh -``` - -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 parametr. - -Samozřejmě je možné parametry uvést přímo: - -```latte -refresh -``` - -Funkce `isLinkCurrent()` zjišťuje, zda je cíl odkazu shodný s aktuální stránkou. Toho lze využít například v šabloně k odlišení odkazů apod. - -Parametry jsou stejné jako u metody `link()`, navíc je však možné místo konkrétní akce uvést zástupný znak `*`, který znamená jakoukoliv akci daného presenteru. - -```latte -{if !isLinkCurrent('Admin:login')} - Přihlaste se -{/if} - -
  • - ... -
  • -``` - -V kombinaci s `n:href` v jednom elementu se dá použít zkrácená podoba: - -```latte -... -``` - -Zástupný znak `*` lze použít pouze místo akce, nikoliv presenteru. - -Pro zjištění, zda jsme v určitém modulu nebo jeho submodulu, použijeme metodu `isModuleCurrent(moduleName)`. - -```latte -
  • - ... -
  • -``` - - -Změna základu pro odkazy .{data-version:3.2.7} -============================================== - -Ve výchozím stavu se relativní odkazy odvíjejí od aktuálního presenteru. To lze změnit pomocí `{linkBase}`: - -```latte -{linkBase Admin:Dashboard} -detail produktu -``` - -Odkaz povede na `Admin:Dashboard:Product:show`. Ovlivněny jsou pouze relativní odkazy - absolutní odkazy začínající dvojtečkou a odkazy na aktuální presenter (`this`, `show`) zůstávají nezměněny. - -`{linkBase}` platí pro celou šablonu a je užitečné zejména v šablonách layoutu, kde zajistí konzistentní odkazy nezávisle na volajícím presenteru. -Značka musí být umístěna na začátku šablony, jinak vyhodí `CompileException`. - - -Odkazy na signál -================ - -Cílem odkazu nemusí být jen presenter a akce, ale také [signál |components#Signál] (volá metodu `handle()`). Pak je syntaxe následující: - -``` -[//] [sub-component:]signal! [#fragment] -``` - -Signál tedy odlišuje vykřičník: - -```latte -signal -``` - -Lze vytvořit i odkaz na signál subkomponenty (nebo sub-subkomponenty): - -```latte -signal -``` - - -Odkazy v komponentě -=================== - -Protože [komponenty|components] jsou samostatné znovupoužitelné celky, které by neměly mít žádné vazby na okolní presentery, fungují tu odkazy trošku jinak. Atribut Latte `n:href` a značka `{link}` i metody komponent jako je `link()` a další považují cíl odkazu **vždy za název signálu**. Proto není nutné ani uvádět vykřičník: - -```latte -signál, nikoliv akce -``` - -Pokud bychom chtěli v šabloně komponenty odkazovat na presentery, použijeme k tomu značku `{plink}`: - -```latte -úvod -``` - -nebo v kódu - -```php -$this->getPresenter()->link('Home:default') -``` - - -Aliasy .{data-version:3.2.3} -============================ - -Občas se může hodit přiřadit dvojici Presenter:akce snadno zapamatovatelný alias. Například úvodní stránku `Front:Home:default` pojmenovat jednoduše jako `home` nebo `Admin:Dashboard:default` jako `admin`. - -Aliasy se definují v [konfiguraci|configuration] pod klíčem `application › aliases`: - -```neon -application: - aliases: - home: Front:Home:default - admin: Admin:Dashboard:default - sign: Front:Sign:in -``` - -V odkazech se pak zapisují pomocí zavináče, například: - -```latte -administrace -``` - -Podporované jsou i ve všech metodách pracujících s odkazy, jako je `redirect()` a podobně. - - -Neplatné odkazy -=============== - -Může se stát, že vytvoříme neplatný odkaz - buď proto, že vede na neexistující presenter, nebo proto, že předává víc parametrů, než které cílová metoda přijímá ve své signatuře, nebo když pro cílovou akci nelze vygenerovat URL. Jak naložit s neplatnými odkazy, určuje property `$this->invalidLinkMode`, kterou nastavíte v presenteru. Ta může nabývat kombinace těchto hodnot (konstant): - -- `Presenter::InvalidLinkSilent` - tichý režim, jako URL se vrátí znak # -- `Presenter::InvalidLinkWarning` - vyhodí se varování E_USER_WARNING, které bude v produkčním režimu zalogováno, ale nezpůsobí přerušení běhu skriptu -- `Presenter::InvalidLinkTextual` - vizuální varování, vypíše chybu přímo do odkazu -- `Presenter::InvalidLinkException` - vyhodí se výjimka InvalidLinkException - -Výchozí nastavení je `InvalidLinkWarning` v produkčním režimu a `InvalidLinkWarning | InvalidLinkTextual` ve vývojovém. `InvalidLinkWarning` v produkčním prostředí nezpůsobí přerušení skriptu, ale varování bude zalogováno. Ve vývojovém prostředí ho zachytí [Tracy |tracy:] a zobrazí bluescreen. `InvalidLinkTextual` pracuje tak, že jako URL vrátí chybovou zprávu, která začíná znaky `#error:`. Aby takové odkazy byly na první pohled patrné, doplníme si do CSS: - -```css -a[href^="#error:"] { - background: red; - color: white; -} -``` - -Pokud nechceme, aby se ve vývojovém prostředí varování vypisovala, můžeme je potlačit přímo v [konfiguraci|configuration]. - -```neon -application: - silentLinks: true -``` - - -LinkGenerator -============= - -Jak vytvářet odkazy s podobným komfortem, jako má metoda `link()`, ale bez přítomnosti presenteru? Od toho je tu [api:Nette\Application\LinkGenerator]. - -LinkGenerator 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. - -Neplatné odkazy vždy vyhazují `Nette\Application\UI\InvalidLinkException`. - -{{maintitle: Odkazy v Nette: šablony, presentery a LinkGenerator}} -{{description: Odkazy na presentery, akce a signály pomocí n:href a link(), zvýraznění aktuální stránky v menu, odhalení neplatných odkazů a generování URL mimo presenter.}} diff --git a/application/cs/directory-structure.texy b/application/cs/directory-structure.texy deleted file mode 100644 index bf42818d93..0000000000 --- a/application/cs/directory-structure.texy +++ /dev/null @@ -1,529 +0,0 @@ -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 funkci rostlináře to nemá vliv. Adresář obsahuje: -- [Vstupní bod |bootstrapping#index.php] aplikace `index.php` -- Soubor `.htaccess` s pravidly pro mod_rewrite (u Apache) -- Statické soubory (CSS, JavaScript, obrázky) -- Uploadované soubory - -Pro správné zabezpečení aplikace je zásadní mít správně [nakonfigurovaný document-root |nette:troubleshooting#Jak změnit či odstranit z URL adresář www]. - -.[note] -Nikdy neumisťujte do tohoto adresáře složku `node_modules/` - obsahuje tisíce souborů, které mohou být spustitelné a neměly by být veřejně dostupné. - - -Aplikační adresář `app/` -======================== - -Toto je hlavní adresář s aplikačním kódem. Základní struktura: - -/--pre -app/ -├── Core/ ← infrastrukturní záležitosti -├── Model/ ← business logika -├── Presentation/ ← presentery a šablony -├── Tasks/ ← příkazové skripty -└── Bootstrap.php ← zaváděcí třída aplikace -\-- - -`Bootstrap.php` je [startovací třída aplikace|bootstrapping], která inicializuje prostředí, načítá konfiguraci a vytváří DI kontejner. - -Pojďme se nyní podívat na jednotlivé podadresáře podrobněji. - - -Presentery a šablony -==================== - -Prezentační část aplikace máme v adresáři `app/Presentation`. Alternativou je krátké `app/UI`. Je to místo pro všechny presentery, jejich šablony a případné pomocné třídy. - -Tuto vrstvu organizujeme podle domén. V komplexním projektu, který kombinuje e-shop, blog a API, by struktura vypadala takto: - -/--pre -app/Presentation/ -├── Shop/ ← e-shop frontend -│ ├── Product/ -│ ├── Cart/ -│ └── Order/ -├── Blog/ ← blog -│ ├── Home/ -│ └── Post/ -├── Admin/ ← administrace -│ ├── Dashboard/ -│ └── Products/ -└── Api/ ← API endpointy - └── V1/ -\-- - -Naopak u jednoduchého blogu bychom použili členění: - -/--pre -app/Presentation/ -├── Front/ ← frontend webu -│ ├── Home/ -│ └── Post/ -├── Admin/ ← administrace -│ ├── Dashboard/ -│ └── Posts/ -├── Error/ -└── Export/ ← RSS, sitemapy atd. -\-- - -Složky jako `Home/` nebo `Dashboard/` obsahují presentery a šablony. Složky jako `Front/`, `Admin/` nebo `Api/` nazýváme **moduly**. Technicky jde o běžné adresáře, které slouží k logickému členění aplikace. - -Každá složka s presenterem obsahuje stejně pojmenovaný presenter a jeho šablony. Například složka `Dashboard/` obsahuje: - -/--pre -Dashboard/ -├── DashboardPresenter.php ← presenter -└── default.latte ← šablona -\-- - -Tato adresářová struktura se odráží ve jmenných prostorech tříd. Například `DashboardPresenter` se nachází ve jmenném prostoru `App\Presentation\Admin\Dashboard` (viz [#mapování presenterů]): - -```php -namespace App\Presentation\Admin\Dashboard; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -Na presenter `Dashboard` uvnitř modulu `Admin` odkazujeme v aplikaci pomocí dvojtečkové notace jako na `Admin:Dashboard`. Na jeho akci `default` potom jako na `Admin:Dashboard:default`. V případě zanořených modulů používáme více dvojteček, například `Shop:Order:Detail:default`. - - -Flexibilní vývoj struktury --------------------------- - -Jednou z velkých výhod této struktury je, jak elegantně se přizpůsobuje rostoucím potřebám projektu. Jako příklad si vezměme část generující XML feedy. Na začátku máme jednoduchou podobu: - -/--pre -Export/ -├── ExportPresenter.php ← jeden presenter pro všechny exporty -├── sitemap.latte ← šablona pro sitemapu -└── feed.latte ← šablona pro RSS feed -\-- - -Časem přibudou 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ší přehlednost 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 složku `Presentation/`. 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 presenterů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í. Jejich ú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 nebo přes cron: skript v `bin/` vytvoří DI kontejner pomocí metody [bootConsoleApplication() |bootstrapping#Odlišné prostředí] a vytáhne si z něj potřebnou službu. 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`). Od Nette Application 3.3 je toto výchozí konvence, kterou není nutné nijak konfigurovat. Pokud používáte jinou strukturu nebo chcete mapování uvést explicitně, výchozímu nastavení odpovídá tento řá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 použitím zdvojené hvězdičky `**` (vyžaduje Nette Application 3.2.3): - -```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`, kdy se segment s hvězdičkou opakuje pro každou úroveň modulu 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ů. Pro výše uvedené příklady je tento zápis ekvivalentní s předchozím: - -```neon -application: - mapping: - *: [App\Presentation, *, **Presenter] - Api: [App\Api, '', *Presenter] -``` - -{{maintitle: Adresářová struktura projektu v Nette: jak uspořádat kód}} -{{description: Přehledná a škálovatelná adresářová struktura projektů v Nette: kam patří presentery, šablony, model a infrastrukturní kód a jak funguje mapování presenterů.}} diff --git a/application/cs/how-it-works.texy b/application/cs/how-it-works.texy deleted file mode 100644 index 4a52c576b3..0000000000 --- a/application/cs/how-it-works.texy +++ /dev/null @@ -1,203 +0,0 @@ -Jak fungují aplikace? -********************* - -
    - -Právě čtete základní listinu dokumentace Nette. Dozvíte se celý princip fungování webových aplikací. Pěkně od A do Z, od chvíle zrození až do posledního vydechnutí PHP skriptu. Po přečtení budete vědět: - -- jak to celé funguje -- co je to Bootstrap, Presenter a DI kontejner -- jak vypadá adresářová struktura - -
    - - -Adresářová struktura -==================== - -Otevřete si příklad skeletonu webové aplikace zvané [WebProject|https://github.com/nette/web-project] a při čtení se můžete dívat na soubory, o kterých je řeč. - -Adresářová struktura vypadá nějak takto: - -/--pre -web-project/ -├── app/ ← adresář s aplikací -│ ├── 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 -├── assets/ ← zdroje (SCSS, TypeScript, zdrojové obrázky) -├── bin/ ← skripty spouštěné z příkazové řádky -├── config/ ← konfigurační soubory -│ ├── common.neon -│ └── services.neon -├── log/ ← logované chyby -├── temp/ ← dočasné soubory, cache, … -├── vendor/ ← knihovny instalované Composerem -│ ├── ... -│ └── autoload.php ← autoloading všech nainstalovaných balíčků -├── www/ ← veřejný adresář neboli document-root projektu -│ ├── assets/ ← zkompilované statické soubory (CSS, JS, obrázky, …) -│ ├── .htaccess ← pravidla mod_rewrite -│ └── index.php ← prvotní soubor, kterým se aplikace spouští -└── .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, 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 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 odstranit z URL adresář www] tak, aby document-root mířil do tohoto adresáře. - -WebProject si můžete také rovnou stáhnout včetně Nette a to pomocí [Composeru |best-practices:composer]: - -```shell -composer create-project nette/web-project -``` - -Na Linuxu nebo macOS nastavte adresářům `log/` a `temp/` [práva pro zápis |nette:troubleshooting#Nastavení práv adresářů]. - -Aplikace WebProject je připravená ke spuštění, není třeba vůbec nic konfigurovat a můžete ji rovnou zobrazit v prohlížeči přístupem ke složce `www/`. - - -HTTP požadavek -============== - -Vše začíná ve chvíli, kdy uživatel v prohlížeči otevře stránku. Tedy když prohlížeč zaklepe na server s HTTP požadavkem. Požadavek míří na jediný PHP soubor, který se nachází ve veřejném adresáři `www/`, a tím je `index.php`. Dejme tomu, že jde o požadavek na adresu `https://example.com/product/123`. Díky vhodnému [nastavení serveru |nette:troubleshooting#Jak nastavit server pro hezká URL] se i tohle URL mapuje na soubor `index.php` a ten se vykoná. - -Jeho úkolem je: - -1) inicializovat prostředí -2) získat továrnu -3) spustit Nette aplikaci, která vyřídí požadavek - -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á [chytrou autodetekci |bootstrapping#Vývojářský vs produkční režim]: pokud web spouštíte na localhost, běží v režimu vývojářském. Nemusíte tak nic konfigurovat a aplikace je rovnou připravena jak pro vývoj, tak ostré nasazení. Tyhle kroky se provádějí a jsou podrobně rozepsané v kapitole o [třídě Bootstrap|bootstrapping]. - -Třetím bodem (ano, druhý jsme přeskočili, ale vrátíme se k němu) je spuštění aplikace. Vyřizování HTTP požadavků má v Nette na starosti třída `Nette\Application\Application` (dále `Application`), takže když říkáme spustit aplikaci, myslíme tím konkrétně zavolání metody s příznačným názvem `run()` na objektu této třídy. - -Nette je mentor, který vás vede k psaní čistých aplikací podle osvědčených metodik. A jedna z těch naprosto nejosvědčenějších se nazývá **dependency injection**, zkráceně DI. V tuto chvíli vás nechceme zatěžovat vysvětlováním DI, od toho je tu [samostatná kapitola|dependency-injection:introduction], podstatný je důsledek, že klíčové objekty nám bude obvykle vytvářet továrna na objekty, které se říká **DI kontejner** (zkráceně DIC). Ano, to je ta továrna, o které byla před chvíli řeč. A vyrobí nám i objekt `Application`, proto potřebujeme nejprve kontejner. Získáme jej pomocí třídy `Configurator` a necháme jej vyrobit objekt `Application`, zavoláme na něm metodu `run()` a tím se spustí Nette aplikace. Přesně tohle se děje v souboru [index.php |bootstrapping#index.php]. - - -Nette Application -================= - -Třída Application má jediný úkol: odpovědět na HTTP požadavek. - -Aplikace psané v Nette se člení do spousty tzv. presenterů (v jiných frameworcích se můžete setkat s termínem controller, jde o totéž), což jsou třídy, z nichž každá představuje nějakou konkrétní stránku webu: např. homepage; produkt v e-shopu; přihlašovací formulář; sitemap feed atd. Aplikace může mít od jednoho po tisíce presenterů. - -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/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. 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. - -Presenter může vypadat třeba takto: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ProductRepository $repository, - ) { - } - - public function renderShow(int $id): void - { - // získáme data z modelu a předáme do šablony - $this->template->product = $this->repository->getProduct($id); - } -} -``` - -Vyřizování požadavku přebírá presenter. A úkol zní jasně: proveď akci `show` s `id: 123`. Což v řeči presenterů znamená, že se zavolá metoda `renderShow()` a v parametru `$id` dostane `123`. - -Presenter může obsluhovat více akcí, tedy mít více metod `render()`. Ale doporučujeme navrhovat presentery s jednou nebo co nejméně akcemi. - -Takže se zavolala metoda `renderShow(123)`, jejíž kód je sice smyšlený příklad, ale můžete na něm vidět, jak se předávají data do šablony, tedy zápisem do `$this->template`. - -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í sám. V případě akce `show` jednoduše zkusí načíst šablonu `show.latte` v adresáři s třídou `ProductPresenter`. Taktéž se pokusí dohledat layout v souboru `@layout.latte` (podrobněji o [dohledávání šablon |templates#Hledání šablon]). - -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 *] - -Pro jistotu, zkusme si zrekapitulovat celý proces s trošku jinou URL: - -1) URL bude `https://example.com` -2) bootujeme aplikaci, vytvoří se kontejner a spustí se `Application::run()` -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ř. `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. - - -Šablony -======= - -Když už přišla řeč na šablony, v Nette se používá šablonovací systém [Latte |latte:]. Proto taky ty koncovky `.latte` u šablon. Latte se používá jednak proto, že jde o nejlépe zabezpečený šablonovací systém pro PHP, a zároveň také systém nejintuitivnější. Nemusíte se učit mnoho nového, vystačíte si se znalostí PHP a několika značek. Všechno se dozvíte [v dokumentaci |templates]. - -V šabloně se [vytvářejí odkazy |creating-links] na další presentery a akce takto: - -```latte -detail produktu -``` - -Prostě místo reálného URL napíšete známý pár `Presenter:action` a uvedete případné parametry. Trik je v tom `n:href`, které říká, že tento atribut zpracuje Nette. A vygeneruje: - -```latte -detail produktu -``` - -Generování URL má na starosti už dříve zmíněný router. Routery v Nette jsou totiž výjimečné tím, že dokáží provádět nejen transformace z URL na dvojici presenter:action, ale také obráceně, tedy z názvu presenteru + akce + parametrů vygenerovat URL. Díky tomu v Nette můžete úplně změnit tvary URL v celé hotové aplikaci, aniž byste změnili jediný znak v šabloně nebo presenteru. Jen tím, že upravíte router. Také díky tomu funguje tzv. kanonizace, což je další unikátní vlastnost Nette, která přispívá k lepšímu SEO (optimalizaci nalezitelnosti na internetu) tím, že automaticky zabraňuje existenci duplicitního obsahu na různých URL. Hodně programátorů to považuje za ohromující. - - -Interaktivní komponenty -======================= - -O presenterech vám musíme prozradit ještě jednu věc: mají v sobě zabudovaný komponentový systém. Něco podobného mohou pamětníci znát z Delphi nebo ASP.NET Web Forms, na něčem vzdáleně podobném je postaven React nebo Vue.js. Ve světě PHP frameworků jde o naprosto unikátní záležitost. - -Komponenty jsou samostatné znovupoužitelné celky, které vkládáme do stránek (tedy presenterů). Mohou to být [formuláře |forms:in-presenter], [datagridy |https://componette.org/contributte/datagrid/], menu, hlasovací ankety, vlastně cokoliv, co má smysl používat opakovaně. Můžeme vytvářet vlastní komponenty nebo používat některé z [ohromné nabídky |https://componette.org] open source komponent. - -Komponenty zásadním způsobem ovlivňují přístup k tvorbě aplikací. Otevřou vám nové možnosti skládání stránek z předpřipravených jednotek. A navíc mají něco společného s [Hollywoodem |components#Hollywood style]. - - -DI kontejner a konfigurace -========================== - -DI kontejner neboli továrna na objekty je srdce celé aplikace. - -Nemějte obavy, není to žádný magický black box, jak by se třeba mohlo z předchozích řádků zdát. Vlastně je to jedna docela nudná PHP třída, kterou vygeneruje Nette a uloží do adresáře s cache. Má spoustu metod pojmenovaných jako `createServiceAbcd()` a každá z nich umí vyrobit a vrátit nějaký objekt. Ano, je tam i metoda `createServiceApplication__application()`, která vyrobí `Nette\Application\Application`, který jsme potřebovali v souboru `index.php` pro spuštění aplikace. A jsou tam metody vyrábějící jednotlivé presentery. A tak dále. - -Objektům, které DI kontejner vytváří, se z nějakého důvodu říká služby. - -Na této třídě je opravdu speciální to, že ji neprogramujete vy, ale framework. On skutečně vygeneruje PHP kód a uloží ho na disk. Vy jen dáváte instrukce, jaké objekty má umět kontejner vyrábět a jak přesně. A tyhle instrukce jsou zapsané v [konfiguračních souborech |bootstrapping#Konfigurace DI kontejneru], pro které se používá formát [NEON|neon:format] a tedy mají i příponu `.neon`. - -Konfigurační soubory slouží čistě k instruování DI kontejneru. Takže když například v sekci [session |http:configuration#Session] uvedete volbu `expiration: 14 days`, tak DI kontejner při vytváření objektu `Nette\Http\Session` reprezentujícího session zavolá jeho metodu `setExpiration('14 days')` a tím se konfigurace stane realitou. - -Je tu pro vás připravená celá kapitola popisující, co vše lze [konfigurovat |nette:configuring] a jak [definovat vlastní služby |dependency-injection:services]. - -Jakmile do vytváření služeb trošku proniknete, narazíte na slovo [autowiring |dependency-injection:autowiring]. To je vychytávka, která vám neuvěřitelným způsobem zjednoduší život. Umí automaticky předávat objekty tam, kde je potřebujete (třeba v konstruktorech vašich tříd), aniž byste museli cokoliv dělat. Zjistíte, že DI kontejner v Nette je malý zázrak. - - -Kam dál? -======== - -Prošli jsme si základní principy aplikací v Nette. Zatím velmi povrchně, ale brzy proniknete do hloubky a časem vytvoříte báječné webové aplikace. Kam pokračovat dál? Vyzkoušeli jste si už tutoriál [Píšeme první aplikaci|quickstart:]? - -Kromě výše popsaného disponuje Nette celým arzenálem [užitečných tříd|utils:], [databázovou vrstvou|database:], atd. Zkuste si schválně jen tak proklikat dokumentaci. Nebo [blog|https://blog.nette.org]. Objevíte spoustu zajímavého. - -Ať vám framework přináší spoustu radosti 💙 - -{{maintitle: Jak fungují aplikace v Nette: od požadavku k odpovědi}} -{{description: Základní kapitola dokumentace Nette: krok za krokem sledujete cestu webové aplikace od HTTP požadavku přes router a presenter až po vykreslení šablony.}} diff --git a/application/cs/multiplier.texy b/application/cs/multiplier.texy deleted file mode 100644 index c1553453bb..0000000000 --- a/application/cs/multiplier.texy +++ /dev/null @@ -1,66 +0,0 @@ -Multiplier: dynamické komponenty -******************************** - -.[perex] -Nástroj na dynamickou tvorbu interaktivních komponent. - -Vyjděme od typického příkladu: mějme seznam zboží v eshopu, přičemž u každého budeme chtít vypsat formulář pro přidání zboží do košíku. Jednou z možných variant je obalit celý výpis do jednoho formuláře. Mnohem pohodlnější způsob nám však nabízí [api:Nette\Application\UI\Multiplier]. - -Multiplier umožňuje pohodlně definovat továrničku pro více komponent. Funguje na principu vnořených komponent - každá komponenta dědící od [api:Nette\ComponentModel\Container] může obsahovat další komponenty. - -.[tip] -Viz kapitola o [komponentovém modelu |components#Komponenty do hloubky] v dokumentaci či [přednáška od Honzy Tvrdíka|https://www.youtube.com/watch?v=8y3LLexWu-I]. - -Podstatou Multiplieru je, že vystupuje v pozici rodiče, který si své potomky dokáže vytvářet dynamicky pomocí callbacku předaného v konstruktoru. Viz příklad: - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function () { - $form = new Nette\Application\UI\Form; - $form->addInteger('count', 'Počet zboží:') - ->setRequired(); - $form->addSubmit('send', 'Přidat do košíku'); - return $form; - }); -} -``` - -Nyní můžeme v šabloně jednoduše u každého zboží nechat vykreslit formulář - a každý bude skutečně unikátní komponentou. - -```latte -{foreach $items as $item} -

    {$item->title}

    - {$item->description} - - {control "shopForm-$item->id"} -{/foreach} -``` - -Argument předaný ve značce `{control}` je ve formátu, který říká: - -1. získej komponentu `shopForm` -2. a z ní získej potomka `$item->id` - -Při prvním volání bodu **1.** `shopForm` ještě neexistuje, takže se zavolá jeho továrna `createComponentShopForm`. Na získané komponentě (instanci Multiplieru) je pak zavolána továrna konkrétního formuláře - což je anonymní funkce, kterou jsme Multiplieru v konstruktoru předali. - -V další iteraci foreache již metoda `createComponentShopForm` volána nebude (komponenta existuje), ale protože hledáme jejího jiného potomka (`$item->id` bude v každé iteraci jiné), znovu bude zavolána anonymní funkce a vrátí nám nový formulář. - -Jediné, co zbývá, je zajistit, aby nám formulář do košíku přidal skutečně to zboží, které má - aktuálně je formulář u každého zboží úplně totožný. Pomůže nám vlastnost Multiplieru (a obecně každé továrny na komponentu v Nette Frameworku), a to ta, že každá továrna dostává jako svůj první argument název tvořené komponenty. Továrna Multiplieru navíc jako druhý argument dostává samotnou instanci Multiplieru. V našem případě bude prvním argumentem `$item->id`, což je přesně ten údaj, který potřebujeme. Stačí tedy lehce upravit tvorbu formuláře: - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function (string $itemId) { - $form = new Nette\Application\UI\Form; - $form->addInteger('count', 'Počet zboží:') - ->setRequired(); - $form->addHidden('itemId', $itemId); - $form->addSubmit('send', 'Přidat do košíku'); - return $form; - }); -} -``` - -{{maintitle: Multiplier v Nette: dynamické vytváření komponent}} -{{description: Potřebujete u každého produktu formulář Přidat do košíku? Multiplier vytvoří tolik samostatných komponent, kolik je třeba, takže každá položka má tu svou.}} diff --git a/application/cs/presenters.texy b/application/cs/presenters.texy deleted file mode 100644 index 2af57b46df..0000000000 --- a/application/cs/presenters.texy +++ /dev/null @@ -1,598 +0,0 @@ -Presentery -********** - -
    - -Seznámíme se s tím, jak se v Nette píší presentery a šablony. Po přečtení budete vědět: - -- jak funguje presenter -- co jsou persistentní parametry -- jak se kreslí šablony - -
    - -[Už víme |how-it-works#Nette Application], že presenter je třída, která představuje nějakou konkrétní stránku webové aplikace, např. homepage; produkt v e-shopu; přihlašovací formulář; sitemap feed atd. Aplikace může mít od jednoho po tisíce presenterů. V jiných frameworcích se jim také říká controllery. - -Obvykle se pod pojmem presenter myslí potomek třídy [api:Nette\Application\UI\Presenter], který je vhodný pro generování webových rozhraní a kterému se budeme věnovat ve zbytku této kapitoly. V obecném smyslu je presenter jakýkoliv objekt implementující rozhraní [api:Nette\Application\IPresenter]. - - -Životní cyklus presenteru -========================= - -Úkolem presenteru je vyřídit požadavek a vrátit odpověď (což může být HTML stránka, obrázek, přesměrování atd.). - -Tedy na počátku je mu předán požadavek. Není to přímo HTTP požadavek, ale objekt [api:Nette\Application\Request], do kterého byl HTTP požadavek přetransformován za pomoci routeru. S tímto objektem obvykle nepřijdeme do styku, neboť presenter zpracování požadavku chytře deleguje do dalších metod, které si teď ukážeme. - -[* lifecycle.svg *] *** *Životní cyklus presenteru* .<> - -Obrázek představuje seznam metod, které se postupně od shora dolů volají, pokud existují. Žádná z nich existovat nemusí, můžeme mít úplně prázdný presenter bez jediné metody a postavit na něm jednoduchý statický web. - - -`__construct()` ---------------- - -Konstruktor nepatří tak úplně do životního cyklu presenteru, protože se volá v okamžiku vytváření objektu. Ale uvádíme jej kvůli důležitosti. Konstruktor (společně s [metodou inject|best-practices:inject-method-attribute]) slouží k předávání závislostí. - -Presenter by neměl obstarávat byznys logiku aplikace, zapisovat a číst z databáze, provádět výpočty atd. Od toho jsou třídy z vrstvy, kterou označujeme jako model. Například třída `ArticleRepository` může mít na starosti načítání a ukládání článků. Aby s ní mohl presenter pracovat, nechá si ji [předat pomocí dependency injection |dependency-injection:passing-dependencies]: - - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articles, - ) { - } -} -``` - - -`startup()` ------------ - -Ihned po obdržení požadavku se zavolá metoda `startup()`. Můžete ji využít k inicializaci properties, ověření uživatelských oprávnění atd. Metoda musí vždy volat předka `parent::startup()`. - - -`action(args...)` .{toc: action()} --------------------------------------------------- - -Obdoba metody `render()`. Zatímco `render()` je určená k tomu, aby připravila data pro konkrétní šablonu, která se následně vykreslí, tak v `action()` se zpracovává požadavek bez návaznosti na vykreslování šablony. Například se zpracují data, přihlásí či odhlásí uživatel a podobně a poté se [přesměruje jinam |#Přesměrování]. - -Důležité je, že `action()` se volá dříve než `render()`, takže v ní můžeme případně změnit další běh dějin, tj. změnit šablonu, která se bude kreslit, a také metodu `render()`, která se bude volat. A to pomocí `setView('jineView')`. - -.{data-version:3.2.3} -Můžete dokonce přepnout na zcela jinou akci metodou `switch('jináAkce')`. Přeruší běh aktuální metody a místo ní spustí metody `action()` a `render()` nové akce (a vypne automatickou [kanonizaci|#Kanonizace]). Samotný požadavek pokračuje dál, ukončí se jen právě běžící metoda. - -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()} --------------------------------------------------- - -Metoda zpracovává tzv. signály, se kterými se seznámíme v kapitole věnované [komponentám |components#Signál]. Je totiž určena zejména pro komponenty a zpracování AJAXových požadavků. - -Metodě se předávají parametry z požadavku, jako v případě `action()`, včetně typové kontroly. - - -`beforeRender()` ----------------- - -Metoda `beforeRender`, jak už název napovídá, se volá před každou metodou `render()`. Používá se pro společnou konfiguraci šablony, předání proměnných pro layout a podobně. - - -`render(args...)` .{toc: render()} ----------------------------------------------- - -Místo, kde připravujeme šablonu k následnému vykreslení, předáváme jí data atd. - -Metodě se předávají parametry z požadavku, jako v případě `action()`, včetně typové kontroly. - -```php -public function renderShow(int $id): void -{ - // získáme data z modelu a předáme do šablony - $this->template->article = $this->articles->getById($id); -} -``` - - -`afterRender()` ---------------- - -Metoda `afterRender`, jak název opět napovídá, se volá za každou metodou `render()`. Používá se spíš výjimečně. - - -`shutdown()` ------------- - -Volá se na konci životního cyklu presenteru. - - -Události --------- - -Kromě metod `startup()`, `beforeRender()` a `shutdown()`, které se volají jako součást životního cyklu presenteru, lze definovat ještě další funkce, které se mají automaticky zavolat. Presenter definuje tzv. [události |nette:glossary#události], jejichž handlery přidáte do polí `$onStartup`, `$onRender` a `$onShutdown`. - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct() - { - $this->onStartup[] = function () { - // ... - }; - } -} -``` - -Handlery v poli `$onStartup` se volají těsně před metodou `startup()`, dále `$onRender` mezi `beforeRender()` a `render()` a nakonec `$onShutdown` těsně před `shutdown()`. - - -**Dobrá rada, než půjdeme dál**. Presenter, jak vidno, může obsluhovat více akcí/view, tedy mít více metod `render()`. Ale doporučujeme navrhovat presentery s jednou nebo co nejméně akcemi. - - -Odeslání odpovědi -================= - -Odpovědí presenteru je zpravidla [vykreslení šablony s HTML stránkou|templates], ale může jí být také odeslání souboru, JSON nebo třeba přesměrování na jinou stránku. - -Kdykoliv během životního cyklu můžeme některou z následujících metod odeslat odpověď a zároveň tak ukončit presenter: - -- `redirect()`, `redirectPermanent()`, `redirectUrl()` a `forward()` [přesměrují |#Přesměrování] -- `error()` ukončí presenter [kvůli chybě |#Chyba 404 a spol] -- `sendJson($data)` presenter ukončí a [odešle data |#Odeslání JSON] ve formátu JSON -- `sendTemplate()` presenter ukončí a ihned [vykreslí šablonu |templates] -- `sendResponse($response)` presenter ukončí a odešle [vlastní odpověď |#Odpovědi] -- `terminate()` presenter ukončí bez odpovědi - -Každá z těchto metod okamžitě ukončí činnost presenteru vyhozením tzv. tiché ukončovací výjimky `Nette\Application\AbortException`. - -Pokud žádnou z těchto metod nezavoláte, presenter automaticky přistoupí k vykreslení šablony. Proč? Protože v 99 % případů chceme vykreslit šablonu, tudíž presenter tohle chování bere jako výchozí a chce nám ulehčit práci. - - -Vytváření odkazů -================ - -Presenter disponuje metodou `link()`, pomocí které lze vytvářet URL odkazy na další presentery. Prvním parametrem je cílový presenter a akce, následují předávané argumenty, které mohou být uvedeny jako pole: - -```php -$url = $this->link('Product:show', $id); - -$url = $this->link('Product:show', [$id, 'lang' => 'cs']); -``` - -V šabloně se vytvářejí odkazy na další presentery a akce tímto způsobem: - -```latte -detail produktu -``` - -Prostě místo reálného URL napíšete známý pár `Presenter:action` a uvedete případné parametry. Trik je v tom `n:href`, které říká, že tento atribut zpracuje Latte a vygeneruje reálné URL. V Nette tak vůbec nemusíte uvažovat nad URL, jen nad presentery a akcemi. - -Více informací najdete v kapitole [Vytváření odkazů URL|creating-links]. - - -Přesměrování -============ - -K přechodu na jiný presenter slouží metody `redirect()` a `forward()`, které mají velmi podobnou syntaxi jako metoda [link() |#Vytváření odkazů]. - -Metoda `forward()` přejde na nový presenter okamžitě bez HTTP přesměrování: - -```php -$this->forward('Product:show'); -``` - -Příklad tzv. dočasného přesměrování s HTTP kódem 302 (nebo 303, je-li metoda aktuálního požadavku POST): - -```php -$this->redirect('Product:show', $id); -``` - -Permanentní přesměrování s HTTP kódem 301 docílíte takto: - -```php -$this->redirectPermanent('Product:show', $id); -``` - -Na jinou URL mimo aplikaci lze přesměrovat metodou `redirectUrl()`. Jako druhý parametr lze uvést HTTP kód, výchozí je 302 (nebo 303, je-li metoda aktuálního požadavku POST): - -```php -$this->redirectUrl('https://nette.org'); -``` - -Přesměrování okamžitě ukončí činnost presenteru vyhozením tzv. tiché ukončovací výjimky `Nette\Application\AbortException`. - -Před přesměrováním lze odeslat [flash message |#Flash zprávy], tedy zprávy, které budou po přesměrování zobrazeny v šabloně. - - -Flash zprávy -============ - -Jde o zprávy obvykle informující o výsledku nějaké operace. Důležitým rysem flash zpráv je to, že jsou v šabloně k dispozici i po přesměrování. I po zobrazení zůstanou živé ještě dalších 30 sekund - například pro případ, že by z důvodu chybného přenosu uživatel dal stránku obnovit - zpráva mu tedy hned nezmizí. - -Stačí zavolat metodu [flashMessage() |api:Nette\Application\UI\Control::flashMessage()] a o předání do šablony se postará presenter. Prvním parametrem je text zprávy a nepovinným druhým parametrem její typ (error, warning, info apod.). Metoda `flashMessage()` vrací instanci flash zprávy, ke které je možné přidávat další informace. - -```php -$this->flashMessage('Položka byla smazána.'); -$this->redirect(/* ... */); // a přesměrujeme -``` - -Šabloně jsou tyto zprávy k dispozici v proměnné `$flashes` jako objekty `stdClass`, které obsahují vlastnosti `message` (text zprávy), `type` (typ zprávy) a mohou obsahovat již zmíněné uživatelské informace. Vykreslíme je třeba takto: - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -Chyba 404 a spol. -================= - -Pokud nelze splnit požadavek, třeba z důvodu, že článek, který chceme zobrazit, v databázi neexistuje, vyhodíme chybu 404 metodou `error(string $message = '', int $httpCode = 404)`. - -```php -public function renderShow(int $id): void -{ - $article = $this->articles->getById($id); - if (!$article) { - $this->error(); - } - // ... -} -``` - -HTTP kód chyby lze předat jako druhý parametr, výchozí je 404. Metoda funguje tak, že vyhodí výjimku `Nette\Application\BadRequestException`, načež `Application` předá řízení error-presenteru. Což je presenter, jehož úkolem je zobrazit stránku informující o nastalé chybě. Nastavení error-presenteru se provádí v [konfiguraci application|configuration]. - - -Odeslání JSON -============= - -Metoda `sendJson($data)` převede předaná data do formátu JSON, odešle je jako HTTP odpověď a ukončí běh presenteru. Příklad: - -```php -public function actionData(): void -{ - $data = ['hello' => 'nette']; - $this->sendJson($data); -} -``` - - -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 -====================== - -Persistentní parametry slouží k udržování stavu mezi různými požadavky. Jejich hodnota zůstává stejná i po kliknutí na odkaz. Na rozdíl od dat v session se přenášejí v URL. A to zcela automaticky, není tedy potřeba je explicitně uvádět v `link()` nebo `n:href`. - -Příklad použití? Máte multijazyčnou aplikaci. Aktuální jazyk je parametr, který musí být neustále součástí URL. Ale bylo by neskutečně únavné ho v každém odkazu uvádět. Tak z něj uděláte persistentní parametr `lang` a bude se přenášet sám. Paráda! - -Vytvoření persistentního parametru je v Nette nesmírně jednoduché. Stačí vytvořit veřejnou property a označit ji atributem (dříve se používalo `/** @persistent */`): - -```php -use Nette\Application\Attributes\Persistent; // tento řádek je důležitý - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; // musí být public -} -``` - -Pokud bude `$this->lang` mít hodnotu například `'en'`, tak i odkazy vytvořené pomocí `link()` nebo `n:href` budou obsahovat parametr `lang=en`. A po kliknutí na odkaz bude opět `$this->lang = 'en'`. - -U property doporučujeme uvádět i datový typ (např. `string`) a můžete uvést i výchozí hodnotu. Hodnoty parametrů lze [validovat |#Validace parametrů]. - -Persistentní parametry se standardně přenášejí mezi všemi akcemi daného presenteru. Aby se přenášely i mezi více presentery, je potřeba je definovat buď: - -- ve společném předkovi, od kterého presentery dědí -- v traitě, kterou presentery použijí: - -```php -trait LanguageAware -{ - #[Persistent] - public string $lang; -} - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - use LanguageAware; -} -``` - -Při vytváření odkazu lze persistentnímu parametru změnit hodnotu: - -```latte -detail v češtině -``` - -Nebo jej lze *vyresetovat*, tj. odstranit z URL. Pak bude nabývat svou výchozí hodnotu: - -```latte -klikni -``` - - -Společný prostor parametrů -========================== - -Parametry požadavku, [persistentní parametry |#Persistentní parametry] i parametry metod `action`, `render` a `handle` (signálů) sdílejí jeden společný prostor, kde je každý z nich identifikován svým jménem. Pokud se stejné jméno objeví ve více z nich, odkazují na jednu a tutéž hodnotu. - -Toho se často využívá. Například persistentní parametr `lang` a argument `$lang` action nebo signálové metody jsou jedno a totéž - aktuální hodnotu persistentního parametru si přečtete prostě tím, že ho uvedete v signatuře metody: - -```php -#[Persistent] -public string $lang; - -public function handleSearch(string $query, string $lang): void -{ - // $lang obsahuje aktuální hodnotu persistentního parametru lang -} -``` - -Protože je tento prostor sdílený, dbejte na to, aby názvy parametrů byly jedinečné, pokud nechcete, aby cíleně sdílely hodnotu. To platí i pro signály, které navíc čtou parametry z POST těla požadavku, viz [Signály do hloubky |components#Signály do hloubky]. - - -Interaktivní komponenty -======================= - -Presentery v sobě mají zabudovaný komponentový systém. Komponenty jsou samostatné znovupoužitelné celky, které vkládáme do presenterů. Mohou to být [formuláře |forms:in-presenter], datagridy, menu, vlastně cokoliv, co má smysl používat opakovaně. - -Jak se do presenteru komponenty vkládají a následně používají? To se dozvíte v kapitole [Komponenty |components]. Dokonce zjistíte, co mají společného s Hollywoodem. - -A kde mohu získat komponenty? Na stránce [Componette |https://componette.org/search/component] najdete open-source komponenty a také řadu dalších doplňků pro Nette, které sem umístili dobrovolníci z komunity okolo frameworku. - - -Jdeme do hloubky -================ - -.[tip] -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. - - -Validace parametrů ------------------- - -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 hodnota odpovídá datovému typu uvedenému u property; jinak odpoví chybou 404 a stránka se nezobrazí. - -Nikdy slepě nevěřte parametrům, protože mohou být snadno uživatelem přepsány v URL. Takto například ověříme, zda je jazyk `$this->lang` mezi podporovanými. Vhodnou cestou je přepsat zmíněnou metodu `loadState()`: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; - - public function loadState(array $params): void - { - parent::loadState($params); // zde se nastaví $this->lang - // následuje vlastní kontrola hodnoty: - if (!in_array($this->lang, ['en', 'cs'])) { - $this->error(); - } - } -} -``` - - -Uložení a obnovení požadavku ----------------------------- - -Požadavek, který vyřizuje presenter, je objekt [api:Nette\Application\Request] a vrací ho metoda presenteru `getRequest()`. - -Aktuální požadavek lze uložit do session nebo naopak z ní obnovit a nechat jej presenter znovu vykonat. To se hodí například v situaci, když uživatel vyplňuje formulář a vyprší mu přihlášení. Aby o data nepřišel, před přesměrováním na přihlašovací stránku aktuální požadavek uložíme do session pomocí `$reqId = $this->storeRequest()`, které vrátí jeho identifikátor v podobě krátkého řetězce a ten předáme jako parametr přihlašovacímu presenteru. - -Po přihlášení zavoláme metodu `$this->restoreRequest($reqId)`, která požadavek vyzvedne ze session. POST požadavky na něj forwarduje, ostatní (GET) přesměruje na URL požadavku. 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. - -Podívejte se na návod [Jak se vrátit k dřívější stránce |best-practices:restore-request]. - - -Kanonizace ----------- - -Presentery mají jednu opravdu skvělou vlastnost, která přispívá k lepšímu SEO (optimalizaci nalezitelnosti na internetu). Automaticky zabraňují existenci duplicitního obsahu na různých URL. Pokud k určitému cíli vede více URL adres, např. `/index` a `/index?page=1`, framework určí jednu z nich za primární (kanonickou) a ostatní na ni přesměruje pomocí HTTP kódu 301. Díky tomu vám vyhledávače stránky neindexují dvakrát a nerozmělní jejich page rank. - -Tomuto procesu se říká kanonizace. Kanonická URL je ta, kterou vygeneruje [router|routing], zpravidla tedy první odpovídající routa v kolekci. - -Kanonizace je ve výchozím nastavení zapnutá a lze ji vypnout přes `$this->autoCanonicalize = false`. - -K přesměrování nedojde při AJAXovém nebo POST požadavku, protože by došlo ke ztrátě dat nebo by to nemělo přidanou hodnotu z hlediska SEO. - -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 -{ - $realSlug = $this->facade->getSlugForId($id); - // přesměruje, pokud se $slug liší od $realSlug - $this->canonicalize('Product:show', [$id, $realSlug]); -} -``` - -Kompletní vzor, který kombinuje routovací filtry s `canonicalize()` pro generování SEO-friendly URL, najdete v návodu [Hezké URL se slugem |best-practices:pretty-urls]. - - -Odpovědi --------- - -Odpověď, kterou vrací presenter, je objekt implementující rozhraní [api:Nette\Application\Response]. K dispozici je řada připravených odpovědí: - -- [api:Nette\Application\Responses\CallbackResponse] - odešle callback -- [api:Nette\Application\Responses\FileResponse] - odešle soubor -- [api:Nette\Application\Responses\ForwardResponse] - forward() -- [api:Nette\Application\Responses\JsonResponse] - odešle JSON -- [api:Nette\Application\Responses\RedirectResponse] - přesměrování -- [api:Nette\Application\Responses\TextResponse] - odešle text -- [api:Nette\Application\Responses\VoidResponse] - prázdná odpověď - -Odpovědi se odesílají metodou `sendResponse()`: - -```php -use Nette\Application\Responses; - -// Prostý text -$this->sendResponse(new Responses\TextResponse('Hello Nette!')); - -// Odešle soubor -$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf')); - -// Odpovědí bude callback -$callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) { - if ($httpResponse->getHeader('Content-Type') === 'text/html') { - echo '

    Hello

    '; - } -}; -$this->sendResponse(new Responses\CallbackResponse($callback)); -``` - -Můžete si napsat i vlastní odpověď. Stačí implementovat rozhraní `Nette\Application\Response`, které má jedinou metodu `send()` přijímající HTTP požadavek a odpověď. To se hodí například při streamování dat, která nechcete držet v paměti: - -```php -class CsvResponse implements Nette\Application\Response -{ - public function __construct( - private string $fileName, - private iterable $rows, - ) { - } - - public function send(Nette\Http\IRequest $request, Nette\Http\IResponse $response): void - { - $response->setContentType('text/csv', 'utf-8'); - $response->sendAsFile($this->fileName); - - $handle = fopen('php://output', 'w'); - foreach ($this->rows as $row) { - fputcsv($handle, $row); - } - - fclose($handle); - } -} -``` - -V presenteru ji pak odešlete obvyklým způsobem: `$this->sendResponse(new CsvResponse('export.csv', $rows));` - - -HTTP kešování -------------- - -Metoda `lastModified()` umožňuje snadno využít HTTP kešování. Předáte jí datum a čas poslední změny obsahu (jako timestamp, řetězec nebo objekt `DateTimeInterface`) a volitelně ETag validátor (krátký řetězec identifikující aktuální verzi obsahu, například jeho hash) a čas expirace. Pokud už prohlížeč aktuální verzi má, presenter odešle odpověď `304 Not Modified` a ukončí činnost, takže se stránka zbytečně negeneruje ani nepřenáší: - -```php -public function renderArticle(int $id): void -{ - $article = $this->articles->getById($id); - $this->lastModified($article->updatedAt); - // ... -} -``` - - -Doplnění šablony .{data-version:3.3.0} --------------------------------------- - -Když presenter vykresluje šablonu, metoda `sendTemplate()` těsně před vykreslením zavolá `completeTemplate()`. Ta doplní proměnné označené atributem `#[TemplateVariable]` a dohledá soubor šablony (výchozí proměnné nastaví už `TemplateFactory` při vytvoření šablony). Tuto chráněnou metodu můžete přepsat a přidat do šablony proměnné společné pro všechny pohledy nebo nastavit jiný soubor: - -```php -protected function completeTemplate(Nette\Application\UI\Template $template): void -{ - parent::completeTemplate($template); - $template->siteName = 'My App'; -} -``` - - -Omezení přístupu pomocí `#[Requires]` .{data-version:3.2.3} ------------------------------------------------------------ - -Atribut `#[Requires]` poskytuje pokročilé možnosti pro omezení přístupu k presenterům a jejich metodám. Lze jej použít k určení HTTP metod, vyžadování AJAXového požadavku, omezení na stejný původ (same origin) i k povolení přístupu 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 tato 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')]` - -.[note] -Shoda stejného původu (same origin) se od verze 3.3 ověřuje pomocí prohlížečové hlavičky `Sec-Fetch-Site` (dříve pomocí SameSite cookie), což je spolehlivější a kontroluje přesnou shodu schématu, domény i portu. - -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.3): - -```php -#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])] -class MyPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Od verze 3.1.13 se ověření provádí v `checkHttpMethod()`, která zjišťuje, zda je metoda specifikovaná v požadavku obsažena v poli `$presenter->allowedMethods`. Od verze 3.2.3 je však tento postup zastaralý ve prospěch `#[Requires]`. Metodu můžete přepsat 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ý podle pravidel CORS (Cross-Origin Resource Sharing). Pokud metodu povolíte, ale neimplementujete správnou odpověď, může to vést k nekonzistencím a potenciálním bezpečnostním problémům. - - -Označení zastaralých akcí .{data-version:3.2.3} ------------------------------------------------ - -Atribut `#[Deprecated]` slouží k označení akcí, signálů nebo celých presenterů, které jsou zastaralé a měly by být v budoucnu odstraněny. Při generování odkazů na takto označené části aplikace Nette vyhodí varování, které vývojáře upozorní. - -Atribut lze aplikovat jak na celou třídu presenteru, tak na jednotlivé metody `action()`, `render()` a `handle()`. - - -Další četba -=========== - -- [Metody a atributy inject |best-practices:inject-method-attribute] -- [Skládání presenterů z trait |best-practices:presenter-traits] -- [Předání nastavení do presenterů |best-practices:passing-settings-to-presenters] -- [Jak se vrátit k dřívější stránce |best-practices:restore-request] - -{{maintitle: Presentery v Nette: životní cyklus, akce a parametry}} -{{description: Jak presenter v Nette zpracuje požadavek, od životního cyklu a vykreslení šablony přes přesměrování a chybové stránky po JSON a persistentní parametry.}} diff --git a/application/cs/routing.texy b/application/cs/routing.texy deleted file mode 100644 index 37a1602524..0000000000 --- a/application/cs/routing.texy +++ /dev/null @@ -1,751 +0,0 @@ -Routování -********* - -
    - -Router má na starosti vše okolo URL adres, abyste už nad nimi nemuseli přemýšlet. Ukážeme si: - -- jak nastavit router, aby URL byly podle představ -- povíme si o SEO a přesměrování -- a ukážeme si, jak napsat vlastní router - -
    - - -Lidštější URL (nebo taky cool či pretty URL) jsou použitelnější, zapamatovatelnější a pozitivně přispívají k SEO. Nette na to myslí a vychází vývojářům plně vstříc. Můžete si pro svou aplikaci navrhnout přesně takovou strukturu URL adres, jakou budete chtít. Můžete ji navrhnout dokonce až ve chvíli, když už je aplikace hotová, protože se to obejde bez zásahů do kódu či šablon. Definuje se totiž elegantním způsobem na jednom [jediném místě |#Začlenění do aplikace], v routeru, a není tak roztroušen ve formě anotací ve všech presenterech. - -Router v Nette je mimořádný tím, že je **obousměrný.** Umí jak dekódovat URL v HTTP požadavku, tak i odkazy vytvářet. Hraje tedy zásadní roli v [Nette Application |how-it-works#Nette Application], protože jednak rozhoduje o tom, který presenter a akce budou vykonávat aktuální požadavek, ale také se využívá pro [generování URL |creating-links] v šabloně atd. - -Ovšem router není omezen jen na tohle využití, můžete jej použít v aplikacích, kde se vůbec presentery nepoužívají, pro REST API, atd. Více v části [#samostatné použití]. - - -Kolekce rout -============ - -Nejpříjemnější způsob, jak definovat podobu URL adres v aplikaci, nabízí třída [api:Nette\Application\Routers\RouteList]. Definici tvoří pomocí jednoduchého API seznam tzv. rout, tedy masek URL adres a k nim přidružených presenterů a akcí. Routy nemusíme nijak pojmenovávat. - -```php -$router = new Nette\Application\Routers\RouteList; -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('article/', 'Article:view'); -// ... -``` - -Ukázka říká, že pokud v prohlížeči otevřeme `https://domain.com/rss.xml`, zobrazí se presenter `Feed` s akcí `rss`, pokud `https://domain.com/article/12`, zobrazí se presenter `Article` s akcí `view` atd. V případě nenalezení vhodné routy reaguje Nette Application vyhozením výjimky [BadRequestException |api:Nette\Application\BadRequestException], která se uživateli zobrazí jako chybová stránka 404 Not Found. - - -Pořadí rout ------------ - -Zcela **klíčové je pořadí**, v jakém jsou jednotlivé routy uvedeny, protože se vyhodnocují postupně odshora dolů. Platí pravidlo, že routy deklarujeme **od specifických po obecné**: - -```php -// ŠPATNĚ: 'rss.xml' zachytí první routa a chápe tento řetězec jako -$router->addRoute('', 'Article:view'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// DOBŘE -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('', 'Article:view'); -``` - -Routy se vyhodnocují odshora dolů také při generování odkazů: - -```php -// ŠPATNĚ: odkaz na 'Feed:rss' vygeneruje jako 'admin/feed/rss' -$router->addRoute('admin//', 'Admin:default'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// DOBŘE -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('admin//', 'Admin:default'); -``` - -Nebudeme před vámi tajit, že správné sestavení rout vyžaduje jistou dovednost. Než do ní proniknete, bude vám užitečným pomocníkem [routovací panel |#Ladění routeru]. - - -Maska a parametry ------------------ - -Maska popisuje relativní cestu od kořenového adresáře webu. Nejjednodušší maskou je statická URL: - -```php -$router->addRoute('products', 'Products:default'); -``` - -Často masky obsahují tzv. **parametry**. Ty jsou uvedeny ve špičatých závorkách (např. ``) a jsou předány do cílového presenteru, například metodě `renderShow(int $year)` nebo do persistentního parametru `$year`: - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -Ukázka říká, že pokud v prohlížeči otevřeme `https://example.com/chronicle/2020`, zobrazí se presenter `History` s akcí `show` a parametrem `year: 2020`. - -Parametrům můžeme určit výchozí hodnotu přímo v masce a tím se stanou volitelné: - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -Routa bude nyní akceptovat i URL `https://example.com/chronicle/`, které opět zobrazí `History:show` s parametrem `year: 2020`. - -Parametrem může být samozřejmě i jméno presenteru a akce. Třeba takto: - -```php -$router->addRoute('/', 'Home:default'); -``` - -Uvedená routa akceptuje např. URL ve tvaru `/article/edit` nebo také `/catalog/list` a chápe je jako presentery a akce `Article:edit` a `Catalog:list`. - -Zároveň dává parametrům `presenter` a `action` výchozí hodnoty `Home` a `default` a jsou tedy také volitelné. Takže routa akceptuje i URL ve tvaru `/article` a chápe ji jako `Article:default`. Nebo obráceně, odkaz na `Product:default` vygeneruje cestu `/product`, odkaz na výchozí `Home:default` cestu `/`. - -Maska může popisovat nejen relativní cestu od kořenového adresáře webu, ale také absolutní cestu, pokud začíná lomítkem, nebo dokonce celé absolutní URL, začíná-li dvěma lomítky: - -```php -// relativně k document rootu -$router->addRoute('/', /* ... */); - -// absolutní cesta (relativní k doméně) -$router->addRoute('//', /* ... */); - -// absolutní URL včetně domény (relativní k schématu) -$router->addRoute('//.example.com//', /* ... */); - -// absolutní URL včetně schématu -$router->addRoute('https://.example.com//', /* ... */); -``` - - -Validační výrazy ----------------- - -Pro každý parametr lze stanovit validační podmínku pomocí [regulárního výrazu|https://www.php.net/manual/en/reference.pcre.pattern.syntax.php]. Například parametru `id` určíme, že může nabývat pouze číslic pomocí reguláru `\d+`: - -```php -$router->addRoute('/[/]', /* ... */); -``` - -Výchozím regulárním výrazem pro všechny parametry je `[^/]+`, tj. vše kromě lomítka. Pokud má parametr přijímat i lomítka, uvedeme výraz `.+`: - -```php -// akceptuje https://example.com/a/b/c, path bude 'a/b/c' -$router->addRoute('', /* ... */); -``` - - -Volitelné sekvence ------------------- - -V masce lze označovat volitelné části pomocí hranatých závorek. Volitelná může být libovolná část masky, mohou se v ní nacházet i parametry: - -```php -$router->addRoute('[/]', /* ... */); - -// Akceptuje cesty: -// /cs/download => lang => cs, name => download -// /download => lang => null, name => download -``` - -Když je parametr součástí volitelné sekvence, stává se pochopitelně také volitelným. Pokud nemá uvedenou výchozí hodnotu, tak bude null. - -Volitelné části mohou být i v doméně: - -```php -$router->addRoute('//[.]example.com//', /* ... */); -``` - -Sekvence je možné libovolně zanořovat a kombinovat: - -```php -$router->addRoute( - '[[-]/][/page-]', - 'Home:default', -); - -// Akceptuje cesty: -// /cs/hello -// /en-us/hello -// /hello -// /hello/page-12 -``` - -Při generování URL se usiluje o nejkratší variantu, takže všechno, co lze vynechat, se vynechá. Proto třeba routa `index[.html]` generuje cestu `/index`. Obrátit chování je možné uvedením vykřičníku za levou hranatou závorkou: - -```php -// akceptuje /hello i /hello.html, generuje /hello -$router->addRoute('[.html]', /* ... */); - -// akceptuje /hello i /hello.html, generuje /hello.html -$router->addRoute('[!.html]', /* ... */); -``` - -Volitelné parametry (tj. parametry mající výchozí hodnotu) bez hranatých závorek se chovají v podstatě tak, jako by byly uzávorkovány následujícím způsobem: - -```php -$router->addRoute('//', /* ... */); - -// odpovídá tomuto: -$router->addRoute('[/[/[]]]', /* ... */); -``` - -Pokud bychom chtěli ovlivnit chování koncového lomítka, aby se např. místo `/home/` generovalo jen `/home`, lze toho docílit takto: - -```php -$router->addRoute('[[/[/]]]', /* ... */); -``` - - -Zástupné znaky --------------- - -V masce absolutního URL můžeme použít následující zástupné znaky a vyhnout se tak např. nutnosti zapisovat do masky doménu, která se může lišit ve vývojovém a produkčním prostředí: - -- `%tld%` = top level domain, např. `com` nebo `org` -- `%sld%` = second level domain, např. `example` -- `%domain%` = doména bez subdomén, např. `example.com` -- `%host%` = celý host, např. `www.example.com` -- `%basePath%` = cesta ke kořenovému adresáři - -```php -$router->addRoute('//www.%domain%/%basePath%//', /* ... */); -$router->addRoute('//www.%sld%.%tld%/%basePath%//', /* ... */); -``` - - -Rozšířený zápis ---------------- - -Cíl routy obvykle zapisovaný ve tvaru `Presenter:action` může být také zapsán pomocí pole, které definuje jednotlivé parametry a jejich výchozí hodnoty: - -```php -$router->addRoute('/[/]', [ - 'presenter' => 'Home', - 'action' => 'default', -]); -``` - -Pro detailnější specifikaci lze použít ještě rozšířenější formu, kde kromě výchozích hodnot můžeme nastavit i další vlastnosti parametrů, jako třeba validační regulární výraz (viz parametr `id`): - -```php -use Nette\Routing\Route; - -$router->addRoute('/[/]', [ - 'presenter' => [ - Route::Value => 'Home', - ], - 'action' => [ - Route::Value => 'default', - ], - 'id' => [ - Route::Pattern => '\d+', - ], -]); -``` - -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. - -Toho se využívá pro **pevné parametry** - když chceme dát konkrétní stránce krátkou, zapamatovatelnou URL. Například aby `/obchodni-podminky` vždy otevřelo `Article:view` s `id: 123`: - -```php -$router->addRoute('obchodni-podminky', [ - 'presenter' => 'Article', - 'action' => 'view', - 'id' => 123, -]); -``` - - -Filtry a překlady ------------------ - -Zdrojové kódy aplikace píšeme v angličtině, ale pokud má mít web české URL, pak jednoduché routování typu: - -```php -$router->addRoute('/', 'Home:default'); -``` - -bude generovat anglické URL, jako třeba `/product/123` nebo `/cart`. Pokud chceme mít presentery a akce v URL reprezentované českými slovy (např. `/produkt/123` nebo `/kosik`), můžeme využít překladového slovníku. Pro jeho zápis už potřebujeme "upovídanější" variantu druhého parametru: - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterTable => [ - // řetězec v URL => presenter - 'produkt' => 'Product', - 'kosik' => 'Cart', - 'katalog' => 'Catalog', - ], - ], - 'action' => [ - Route::Value => 'default', - Route::FilterTable => [ - 'seznam' => 'list', - ], - ], -]); -``` - -Více klíčů překladového slovníku může vést na tentýž presenter. Tím se k němu vytvoří různé aliasy. Za kanonickou variantu (tedy tu, která bude ve vygenerovaném URL) se považuje poslední klíč. - -Překladovou tabulku lze tímto způsobem použít na jakýkoliv parametr, přičemž pokud překlad neexistuje, bere se původní hodnota. Tohle chování můžeme změnit doplněním `Route::FilterStrict => true` a routa pak odmítne URL, pokud hodnota není ve slovníku. - -Kromě překladového slovníku v podobě pole lze nasadit i vlastní překladové funkce. - -```php -use Nette\Routing\Route; - -$router->addRoute('//', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterIn => function (string $s): string { /* ... */ }, - Route::FilterOut => function (string $s): string { /* ... */ }, - ], - 'action' => 'default', - 'id' => null, -]); -``` - -Funkce `Route::FilterIn` převádí mezi parametrem v URL a řetězcem, který se poté předá do presenteru, funkce `FilterOut` zajišťuje převod opačným směrem. - -Parametry `presenter`, `action` a `module` už mají předdefinované filtry, které převádějí mezi stylem PascalCase, resp. camelCase, a kebab-case používaným v URL. Výchozí hodnota parametrů se zapisuje v té podobě, v jaké se předává do aplikace (PascalCase u presenteru a modulu, camelCase u akce), takže třeba v případě presenteru píšeme ``, nikoliv ``. - - -Obecné filtry -------------- - -Vedle filtrů určených pro konkrétní parametry můžeme definovat též obecné filtry, které obdrží asociativní pole všech parametrů, mohou je jakkoliv upravit a poté vrátí. Obecné filtry definujeme pod prázdným klíčem. - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => 'Home', - 'action' => 'default', - '' => [ - Route::FilterIn => function (array $params): array { /* ... */ }, - Route::FilterOut => function (array $params): array { /* ... */ }, - ], -]); -``` - -Obecné filtry dávají možnost upravit chování routy naprosto jakýmkoliv způsobem. Můžeme je použít třeba pro modifikaci parametrů na základě jiných parametrů. Například přeložení `` a `` na základě aktuální hodnoty parametru ``. - -Pokud má parametr definovaný vlastní filtr a současně existuje obecný filtr, provede se vlastní `FilterIn` před obecným a naopak obecný `FilterOut` před vlastním. Tedy uvnitř obecného filtru jsou hodnoty parametrů `presenter`, resp. `action`, zapsané ve stylu PascalCase, resp. camelCase. - -Praktické využití těchto filtrů, tedy generování SEO-friendly URL typu `/clanek/123-jak-upect-chleba` bez zásahu do šablon, najdete v návodu [Hezké URL se slugem |best-practices:pretty-urls]. - - -Jednosměrky OneWay ------------------- - -Jednosměrné routy se používají pro zachování funkčnosti starých URL, které už aplikace negeneruje, ale stále přijímá. Označíme je příznakem `OneWay`: - -```php -// staré URL /product-info?id=123 -$router->addRoute('product-info', 'Product:detail', oneWay: true); -// nové URL /product/123 -$router->addRoute('product/', 'Product:detail'); -``` - -Při přístupu na starou URL presenter automaticky přesměruje na nové URL, takže vám tyto stránky vyhledávače nezaindexují dvakrát (viz [#SEO a kanonizace]). - - -Dynamické routování s callbacky -------------------------------- - -Dynamické routování s callbacky vám umožňuje přiřadit routám přímo funkce (callbacky), které se vykonají, když je daná cesta navštívena. Tato flexibilní funkčnost vám umožní rychle a efektivně vytvářet různé koncové body (endpoints) pro vaši aplikaci: - -```php -$router->addRoute('test', function () { - echo 'jste na adrese /test'; -}); -``` - -Můžete také definovat v masce parametry, které se automaticky předají do vašeho callbacku: - -```php -$router->addRoute('', function (string $lang) { - echo match ($lang) { - 'cs' => 'Vítejte na české verzi našeho webu!', - 'en' => 'Welcome to the English version of our website!', - }; -}); -``` - -Kromě parametrů z masky může callback přijmout také služby z DI kontejneru. Ty se předají podle typu parametru. Navíc parametr `$presenter` dostane instanci [MicroPresenteru |api:NetteModule\MicroPresenter], který routu zpracovává: - -```php -$router->addRoute('', function (string $lang, Nette\Http\Request $httpRequest, NetteModule\MicroPresenter $presenter) { - // ... -}); -``` - - -Moduly ------- - -Pokud máme více rout, které spadají do společného [modulu |directory-structure#Presentery a šablony], využijeme `withModule()`. Uvedený modul se automaticky předřadí presenteru u každé routy ve skupině a z URL úplně zmizí: - -```php -$router = new RouteList; -$router->withModule('Forum') // následující routy jsou součástí modulu Forum - ->addRoute('rss', 'Feed:rss') // presenter bude Forum:Feed - ->addRoute('/') - - ->withModule('Admin') // následující routy jsou součástí modulu Forum:Admin - ->addRoute('sign:in', 'Sign:in'); -``` - -Alternativou je parametr `module`, který rovněž nastaví pevný modul a drží ho mimo URL: - -```php -// URL manage/dashboard/default se mapuje na presenter Admin:Dashboard -$router->addRoute('manage//', [ - 'module' => 'Admin', -]); -``` - -Název presenteru je úplný teprve i se svým modulem, např. `Front:Admin:ProductList`. Kdykoli se takový celý název ocitne v parametru URL, router jej zakóduje dvěma jednoduchými pravidly: každá dvojtečka `:` (oddělovač modulů) se stane **tečkou** a každý předěl mezi slovy v PascalCase názvu se stane **pomlčkou**. Takže `Front:Admin:ProductList` se v URL zapíše jako `front.admin.product-list` a stejně se i dekóduje zpět. Právě proto modulární aplikace bez některého z výše uvedených nástrojů tvoří URL plné teček. - -`withModule()` i parametr `module` se tomu vyhnou právě tím, že známý prefix modulu z názvu presenteru odříznou dřív, než se dostane do URL: modul je konstanta, takže ho není potřeba nijak kódovat. - -Někdy chceme, aby se modul sám měnil a objevoval se v URL, a tak sáhneme po `` přímo v masce. Pozor ale na jednu zásadní věc: **`` pohltí celou modulovou cestu** - vše až po poslední dvojtečku v názvu presenteru. U presenteru `Shop:Admin:Product` je to modul `Shop:Admin` a presenter `Product`, a protože se dvojtečky mění na tečky, dostaneme: - - -Subdomény ---------- - -Kolekce rout můžeme členit podle subdomén: - -```php -$router = new RouteList; -$router->withDomain('example.com') - ->addRoute('rss', 'Feed:rss') - ->addRoute('/'); -``` - -V názvu domény lze použít i [#zástupné znaky]: - -```php -$router = new RouteList; -$router->withDomain('example.%tld%') - // ... -``` - - -Prefix cesty ------------- - -Kolekce rout můžeme členit podle cesty v URL: - -```php -$router = new RouteList; -$router->withPath('eshop') - ->addRoute('rss', 'Feed:rss') // chytá URL /eshop/rss - ->addRoute('/'); // chytá URL /eshop// -``` - - -Kombinace ---------- - -Výše uvedené členění můžeme vzájemně kombinovat: - -```php -$router = (new RouteList) - ->withDomain('admin.example.com') - ->withModule('Admin') - ->addRoute(/* ... */) - ->addRoute(/* ... */) - ->end() - ->withModule('Images') - ->addRoute(/* ... */) - ->end() - ->end() - ->withDomain('example.com') - ->withPath('export') - ->addRoute(/* ... */) - // ... -``` - - -Query parametry ---------------- - -Masky mohou také obsahovat query parametry (parametry za otazníkem v URL). Těm nelze definovat validační výraz, ale lze změnit název, pod kterým se předají do presenteru: - -```php -// query parametr 'cat' chceme v aplikaci použít pod názvem 'categoryId' -$router->addRoute('product ? id= & cat=', /* ... */); -``` - - -Foo parametry -------------- - -Nyní už jdeme hlouběji. Foo parametry jsou v podstatě nepojmenované parametry, které umožňují matchovat regulární výraz. Příkladem je routa akceptující `/index`, `/index.html`, `/index.htm` a `/index.php`: - -```php -$router->addRoute('index', /* ... */); -``` - -Lze také explicitně definovat řetězec, který bude použit při generování URL. Řetězec musí být umístěn přímo za otazníkem. Následující routa je podobná předchozí, ale generuje `/index.html` namísto `/index`, protože řetězec `.html` je nastaven jako generovací hodnota: - -```php -$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\Core\RouterFactory::createRouter()`: - -```php -namespace App\Core; - -use Nette\Application\Routers\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute(/* ... */); - return $router; - } -} -``` - -Do [konfigurace |dependency-injection:services] pak zapíšeme: - -```neon -services: - - 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]: - -```php -public static function createRouter(Nette\Database\Connection $db): RouteList -{ - // ... -} -``` - - -SimpleRouter -============ - -Mnohem jednodušším routerem než kolekce rout je [SimpleRouter |api:Nette\Application\Routers\SimpleRouter]. Použijeme jej tehdy, pokud nemáme zvláštní nároky na tvar URL, pokud není k dispozici `mod_rewrite` (nebo jeho alternativy) nebo pokud zatím nechceme řešit hezké URL. - -Generuje adresy zhruba v tomto tvaru: - -``` -http://example.com/?presenter=Product&action=detail&id=123 -``` - -Parametrem konstruktoru SimpleRouteru je výchozí presenter a akce, na které se má směřovat, pokud otevřeme stránku bez parametrů, např. `http://example.com/`. - -```php -// výchozím presenterem bude 'Home' a akce 'default' -$router = new Nette\Application\Routers\SimpleRouter('Home:default'); -``` - -Doporučujeme SimpleRouter přímo definovat v [konfiguraci |dependency-injection:services]: - -```neon -services: - - Nette\Application\Routers\SimpleRouter('Home:default') -``` - - -SEO a kanonizace -================ - -Framework přispívá k SEO (optimalizaci nalezitelnosti na internetu) tím, že zabraňuje duplicitě obsahu na různých URL. Pokud k určitému cíli vede více adres, např. `/index` a `/index.html`, framework první z nich určí za primární (kanonickou) a ostatní na ni přesměruje pomocí HTTP kódu 301. Díky tomu vám vyhledávače stránky nezaindexují dvakrát a nerozmělní jejich page rank. - -Tomuto procesu se říká kanonizace. Kanonická URL je ta, kterou vygeneruje router, tj. první vyhovující routa v kolekci bez příznaku OneWay. Proto v kolekci uvádíme **primární routy jako první**. - -Kanonizaci provádí presenter, více v kapitole [kanonizace |presenters#Kanonizace]. - - -HTTPS -===== - -Abychom mohli používat HTTPS protokol, je nutné ho povolit na hostingu a správně si nakonfigurovat server. - -Přesměrování celého webu na HTTPS je nutné nastavit na úrovni serveru, například pomocí souboru .htaccess v kořenovém adresáři naší aplikace, a to s HTTP kódem 301. Nastavení se může lišit podle hostingu a vypadá cca takto: - -``` - - RewriteEngine On - ... - RewriteCond %{HTTPS} off - RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] - ... - -``` - -Router generuje URL se stejným protokolem, s jakým byla stránka načtena, takže nic víc není potřeba nastavovat. - -Pokud ale výjimečně potřebujeme, aby různé routy běžely pod různými protokoly, uvedeme ho v masce routy: - -```php -// Bude generovat adresu s HTTP -$router->addRoute('http://%host%//', /* ... */); - -// Bude generovat adresu s HTTPs -$router->addRoute('https://%host%//', /* ... */); -``` - - -Ladění routeru -============== - -Routovací panel zobrazující se v [Tracy Baru |tracy:] je užitečným pomocníkem, který zobrazuje seznam rout a také parametrů, které router získal z URL. - -Zelený pruh se symbolem ✓ představuje routu, která zpracovala aktuální URL, modrou barvou a symbolem ≈ jsou označené routy, které by také URL zpracovaly, kdyby je zelená nepředběhla. Dále vidíme aktuální presenter a akci. - -[* routing-debugger.webp *] - -Zároveň pokud dojde k neočekávanému přesměrování kvůli [kanonizaci |#SEO a kanonizace], je užitečné se podívat do panelu v liště *redirect*, kde zjistíte, jak router URL původně pochopil a proč přesměroval. - -.[note] -Při ladění routeru doporučujeme otevřít v prohlížeči Developer Tools (Ctrl+Shift+I nebo Cmd+Option+I) a v panelu Network vypnout cache, aby se do ní neukládala přesměrování. - - -Výkonnost -========= - -Počet rout má vliv na rychlost routeru. Jejich počet by rozhodně neměl přesáhnout několik desítek. Pokud má váš web příliš komplikovanou strukturu URL, můžete si napsat na míru [#vlastní router]. - -Pokud router nemá žádné závislosti, například na databázi, a jeho továrna nepřijímá žádné argumenty, můžeme jeho sestavenou podobu serializovat přímo do DI kontejneru a tím aplikaci mírně zrychlit. - -```neon -routing: - cache: true -``` - - -Vlastní router -============== - -Následující řádky jsou určeny pro velmi pokročilé uživatele. Můžete si vytvořit router vlastní a zcela přirozeně ho začlenit do kolekce rout. Router je implementací rozhraní [api:Nette\Routing\Router] se dvěma metodami: - -```php -use Nette\Http\IRequest as HttpRequest; -use Nette\Http\UrlScript; - -class MyRouter implements Nette\Routing\Router -{ - public function match(HttpRequest $httpRequest): ?array - { - // ... - } - - public function constructUrl(array $params, UrlScript $refUrl): ?string - { - // ... - } -} -``` - -Metoda `match` zpracuje aktuální požadavek [$httpRequest |http:request], ze kterého lze získat nejen URL, ale i hlavičky atd., do pole obsahujícího název presenteru a jeho parametry. Pokud požadavek zpracovat neumí, vrátí null. Při zpracování požadavku musíme vrátit minimálně presenter; akce je volitelná a pokud ji neuvedeme, nastaví se na `default`. Název presenteru je úplný a obsahuje i případné moduly: - -```php -[ - 'presenter' => 'Front:Home', - 'action' => 'default', -] -``` - -Metoda `constructUrl` naopak sestaví z pole parametrů výsledné absolutní URL. K tomu může využít informace z parametru [`$refUrl`|api:Nette\Http\UrlScript], což je aktuální URL. - -Do kolekce rout ho přidáte pomocí `add()`: - -```php -$router = new Nette\Application\Routers\RouteList; -$router->add($myRouter); -$router->addRoute(/* ... */); -// ... -``` - - -Samostatné použití -================== - -Samostatným použitím myslíme využití schopností routeru v aplikaci, která nevyužívá Nette Application a presentery. Platí pro něj téměř vše, co jsme si v této kapitole ukázali, s těmito odlišnostmi: - -- pro kolekce rout používáme třídu [api:Nette\Routing\RouteList] -- jako simple router třídu [api:Nette\Routing\SimpleRouter] -- protože neexistuje dvojice `Presenter:action`, používáme [#rozšířený zápis] - -Takže opět si vytvoříme metodu, která nám sestaví router, např.: - -```php -namespace App\Core; - -use Nette\Routing\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute('rss.xml', [ - 'controller' => 'RssFeedController', - ]); - $router->addRoute('article/', [ - 'controller' => 'ArticleController', - ]); - // ... - return $router; - } -} -``` - -Pokud používáte DI kontejner, což doporučujeme, opět metodu přidáme do konfigurace a poté router společně s HTTP požadavkem získáme z kontejneru: - -```php -$router = $container->getByType(Nette\Routing\Router::class); -$httpRequest = $container->getByType(Nette\Http\IRequest::class); -``` - -Anebo objekty přímo vyrobíme: - -```php -$router = App\Core\RouterFactory::createRouter(); -$httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); -``` - -Teď už zbývá pustit router k práci: - -```php -$params = $router->match($httpRequest); -if ($params === null) { - // nebyla nalezena vyhovující routa, odešleme chybu 404 - exit; -} - -// zpracujeme získané parametry -$controller = $params['controller']; -// ... -``` - -A obráceně použijeme router k sestavení odkazu: - -```php -$params = ['controller' => 'ArticleController', 'id' => 123]; -$url = $router->constructUrl($params, $httpRequest->getUrl()); -``` - - -{{composer: nette/routing}} -{{repo: nette/routing}} -{{api: https://api.nette.org/routing/}} -{{maintitle: Routování v PHP s Nette: hezké URL a SEO}} -{{description: Všechny URL definujete na jednom místě a kdykoliv je změníte bez zásahu do šablon. Masky rout, automatická kanonizace, vlastní router i samostatné použití.}} diff --git a/application/cs/templates.texy b/application/cs/templates.texy deleted file mode 100644 index e2bee7f9c8..0000000000 --- a/application/cs/templates.texy +++ /dev/null @@ -1,422 +0,0 @@ -Šablony -******* - -.[perex] -Nette používá šablonovací systém [Latte |latte:]. Jednak proto, že jde o nejlépe zabezpečený šablonovací systém pro PHP, a zároveň také systém nejintuitivnější. Nemusíte se učit mnoho nového, vystačíte si se znalostí PHP a několika značek. - -Je obvyklé, že stránka se složí ze šablony layoutu + šablony dané akce. Takhle třeba může vypadat šablona layoutu, všimněte si bloků `{block}` a značky `{include}`: - -```latte - - - - {block title}My App{/block} - - -
    ...
    - {include content} -
    ...
    - - -``` - -A tohle bude šablona akce: - -```latte -{block title}Homepage{/block} - -{block content} -

    Homepage

    -... -{/block} -``` - -Ta definuje blok `content`, který se vloží na místo `{include content}` v layoutu, a také předefinuje blok `title`, kterým přepíše `{block title}` v layoutu. Zkuste si představit výsledek. - - -Hledání šablon --------------- - -Nemusíte v presenterech uvádět, jaká šablona se má vykreslit, framework cestu odvodí sám a ušetří vám psaní. - -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`: - -/--pre -app/ -└── Presentation/ - └── 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`, uložte ji buď do souboru `..latte`, nebo `/.latte`: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── Home/ - │ └── default.latte ← 1. varianta - └── Home.default.latte ← 2. varianta -\-- - -Adresář `templates` může být umístěn také o úroveň výš, tj. na stejné úrovni, jako je adresář s třídami presenterů. - -Pokud se šablona nenajde, presenter odpoví [chybou 404 - page not found |presenters#Chyba 404 a spol]. - -View změníte pomocí `$this->setView('jineView')`. Také lze přímo určit soubor se šablonou pomocí `$this->template->setFile('/path/to/template.latte')`. - -.[note] -Soubory, kde se dohledávají šablony, lze změnit překrytím metody [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], která vrací pole možných názvů souborů. - - -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. - -Název layoutu lze změnit pomocí `$this->setLayout('layoutAdmin')` a pak se bude očekávat v souboru `@layoutAdmin.latte`. Také lze přímo určit soubor se šablonou layoutu pomocí `$this->setLayout('/path/to/template.latte')`. - -Pomocí `$this->setLayout(false)` nebo značky `{layout none}` uvnitř šablony se dohledávání layoutu vypne. - -.[note] -Soubory, kde se dohledávají šablony layoutu, lze změnit překrytím metody [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], která vrací pole možných názvů souborů. - - -Proměnné v šabloně ------------------- - -Proměnné do šablony předáváme zápisem do `$this->template`. V šabloně jsou pak dostupné jako lokální proměnné: - -```php -$this->template->article = $this->articles->getById($id); -``` - -Pokud chcete, aby se hodnota určité property automaticky předala do šablony jako proměnná, označte ji atributem `#[TemplateVariable]` a viditelností public: .{data-version:3.2.9} - -```php -use Nette\Application\Attributes\TemplateVariable; - -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - #[TemplateVariable] - public string $siteName = 'Můj blog'; -} -``` - -Pokud do šablony vložíte proměnnou se stejným názvem, `#[TemplateVariable]` ji nepřepíše. - - -Výchozí proměnné ----------------- - -Presentery a komponenty předávají do šablon několik užitečných proměnných automaticky: - -- `$basePath` je absolutní URL cesta ke kořenovému adresáři (např. `/eshop`) -- `$baseUrl` je absolutní URL ke kořenovému adresáři (např. `http://localhost/eshop`) -- `$user` je objekt [reprezentující uživatele |security:authentication] -- `$presenter` je aktuální presenter -- `$control` je aktuální komponenta nebo presenter -- `$flashes` je pole [zpráv |presenters#Flash zprávy] zaslaných funkcí `flashMessage()` - -Pokud používáte vlastní třídu šablony, tyto proměnné se předají, pokud pro ně vytvoříte property. - - -Typově bezpečné šablony ------------------------ - -Při vývoji robustních aplikací je užitečné explicitně nadefinovat, jaké proměnné šablona očekává a jakého jsou typu. Získáte tak typovou kontrolu v PHP, chytré našeptávání v IDE a schopnost statické analýzy odhalovat chyby. - -Jak takový výčet nadefinovat? Jednoduše v podobě třídy s properties reprezentujícími proměnné šablony. Pojmenujeme ji podobně jako presenter, jen s `Template` na konci: - -```php -/** - * @property-read ArticleTemplate $template - */ -class ArticlePresenter extends Nette\Application\UI\Presenter -{ -} - -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - public Model\Article $article; - public Nette\Security\User $user; - - // a další proměnné -} -``` - -Objekt `$this->template` v presenteru bude nyní instancí třídy `ArticleTemplate`. PHP tak bude při zápisu kontrolovat deklarované typy. - -Nette volí třídu šablony automaticky. Nejprve hledá třídu s názvem `Template`, např. `ArticleEditTemplate` pro akci `edit`, a teprve pokud neexistuje, použije `Template`. - -Anotace `@property-read` slouží IDE a statické analýze, díky ní bude fungovat našeptávání, viz "PhpStorm and code completion for $this⁠-⁠>⁠template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template. - -[* phpstorm-completion.webp *] - -Našeptávání můžete využít i přímo v šablonách. Stačí do PhpStorm nainstalovat plugin pro Latte a uvést na začátek šablony název třídy parametrů šablony, více v kapitole [Latte: typový systém |latte:type-system]: - -```latte -{templateType App\Presentation\Article\ArticleTemplate} -... -``` - -Totéž platí i pro komponenty. Stačí dodržet jmennou konvenci a pro komponentu např. `FifteenControl` vytvořit třídu parametrů `FifteenTemplate`. - -Pokud potřebujete použít jinou třídu parametrů, využijte metodu `createTemplate()`: - -```php -public function renderDefault(): void -{ - $template = $this->createTemplate(SpecialTemplate::class); - $template->foo = 123; - // ... - $this->sendTemplate($template); -} -``` - -.{data-version:3.3.0} -Pokud potřebujete ovlivnit, jak se šablona před vykreslením dokončuje - například doplnit proměnné společné všem akcím - můžete v presenteru přepsat metodu `completeTemplate()`. Volá se těsně před vykreslením šablony: - -```php -protected function completeTemplate(Nette\Application\UI\Template $template): void -{ - parent::completeTemplate($template); - $template->siteName = 'Můj blog'; -} -``` - - -Vytváření odkazů ----------------- - -V šabloně se vytvářejí odkazy na další presentery a akce tímto způsobem: - -```latte -detail produktu -``` - -Atribut `n:href` je velmi šikovný pro HTML značky ``. Chceme-li odkaz vypsat jinde, například v textu, použijeme `{link}`: - -```latte -Adresa je: {link Home:default} -``` - -Více informací najdete v kapitole [Vytváření odkazů URL|creating-links]. - - -Vlastní filtry, značky apod. ----------------------------- - -Šablonovací systém Latte lze rozšířit o vlastní filtry, funkce, značky a další prvky. K dispozici jsou tři způsoby, jak to udělat, od nejrychlejších ad-hoc řešení až po architektonický přístup pro celou aplikaci. - -**Ad-hoc v metodách presenteru** - -Nejrychlejší způsob je přidat filtr nebo funkci přímo v kódu presenteru či komponenty. V presenteru je k tomu vhodná metoda `beforeRender()` nebo `render()`: - -```php -protected function beforeRender(): void -{ - // přidání filtru - $this->template->addFilter('money', fn($val) => number_format($val, 2) . ' Kč'); - - // přidání funkce - $this->template->addFunction('isWeekend', fn($date) => $date->format('N') >= 6); -} -``` - -V šabloně pak: - -```latte -

    Cena: {$price|money}

    - -{if isWeekend($now)} ... {/if} -``` - -Pro složitější logiku můžete konfigurovat přímo objekt `Latte\Engine`: - -```php -protected function beforeRender(): void -{ - $latte = $this->template->getLatte(); - $latte->setFeature(Latte\Feature::MigrationWarnings); -} -``` - -**Pomocí atributů** - -Elegantní způsob je definovat filtry a funkce jako metody přímo ve [třídě parametrů šablony|#Typově bezpečné šablony] presenteru nebo komponenty a označit je atributy: - -```php -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - #[Latte\Attributes\TemplateFilter] - public function money(float $val): string - { - return number_format($val, 2) . ' Kč'; - } - - #[Latte\Attributes\TemplateFunction] - public function isWeekend(DateTimeInterface $date): bool - { - return $date->format('N') >= 6; - } -} -``` - -Latte automaticky rozpozná a zaregistruje metody označené těmito atributy. Název filtru nebo funkce v šabloně odpovídá názvu metody. Tyto metody musí být public. - -**Globálně pomocí Extension** - -Předchozí způsoby jsou vhodné pro filtry a funkce, které potřebujete jen v konkrétním presenteru nebo komponentě, nikoliv v celé aplikaci. Pro celou aplikaci je nejvhodnější vytvořit si [extension |latte:extending-latte#Latte Extension]. Jde o třídu, která centralizuje všechna rozšíření Latte pro celý projekt. Kusý příklad: - -```php -namespace App\Presentation\Accessory; - -final class LatteExtension extends Latte\Extension -{ - public function __construct( - private App\Model\Facade $facade, - private Nette\Security\User $user, - // ... - ) { - } - - public function getFilters(): array - { - return [ - 'timeAgoInWords' => $this->filterTimeAgoInWords(...), - 'money' => $this->filterMoney(...), - // ... - ]; - } - - public function getFunctions(): array - { - return [ - 'canEditArticle' => - fn($article) => $this->facade->canEditArticle($article, $this->user->getId()), - // ... - ]; - } - - private function filterTimeAgoInWords(DateTimeInterface $time): string - { - // ... - } - - // ... -} -``` - -Extension zaregistrujeme pomocí [konfigurace |configuration#Šablony Latte]: - -```neon -latte: - extensions: - - App\Presentation\Accessory\LatteExtension -``` - -Výhodou extension je, že lze využít dependency injection, mít přístup k modelové vrstvě aplikace a všechna rozšíření mít přehledně na jednom místě. Extension umožňuje definovat i vlastní značky, providery, průchody pro Latte kompilátor a další. - - -Nastavení všech šablon ----------------------- - -Služba `TemplateFactory`, která vytváří všechny šablony, nabízí veřejné pole callbacků `$onCreate`. Ty se zavolají při vytvoření každé šablony, takže filtry, funkce nebo proměnné pro všechny šablony v aplikaci můžete nastavit z jednoho místa. Každý callback dostane nově vytvořenou šablonu. Službu `TemplateFactory` si [necháte předat |dependency-injection:passing-dependencies] a její callbacky zaregistrujete např. při startu aplikace: - -```php -$templateFactory->onCreate[] = function (Nette\Bridges\ApplicationLatte\Template $template): void { - $template->addFilter('money', fn($val) => number_format($val, 2) . ' Kč'); -}; -``` - - -Překládání ----------- - -Pokud programujete vícejazyčnou aplikaci, budete nejspíš potřebovat některé texty v šabloně vypsat v různých jazycích. Nette Framework k tomuto účelu definuje rozhraní pro překlad [api:Nette\Localization\Translator], které má jedinou metodu `translate()`. Ta přijímá zprávu `$message`, což zpravidla bývá řetězec, a libovolné další parametry. Úkolem je vrátit přeložený řetězec. V Nette není žádná výchozí implementace, můžete si vybrat podle svých potřeb z několika hotových řešení, která najdete na [Componette |https://componette.org/search/localization]. V jejich dokumentaci se dozvíte, jak translator konfigurovat. - -Šablonám lze nastavit překladač, který si [necháme předat |dependency-injection:passing-dependencies], metodou `setTranslator()`: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator); -} -``` - -Translator je alternativně možné nastavit pomocí [konfigurace |configuration#Šablony Latte]: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -Poté lze překladač používat například jako filtr `|translate`, a to včetně doplňujících parametrů, které se předají metodě `translate()` (viz `foo, bar`): - -```latte -
    {='Košík'|translate} -{$item|translate} -{$item|translate, foo, bar} -``` - -Nebo jako podtržítkovou značku: - -```latte -{_'Košík'} -{_$item} -{_$item, foo, bar} -``` - -Pro překlad úseku šablony existuje párová značka `{translate}` (od Latte 2.11, dříve se používala značka `{_}`): - -```latte -{translate}Objednávka{/translate} -{translate foo, bar}Objednávka{/translate} -``` - -Translator se standardně volá za běhu při vykreslování šablony. Latte verze 3 ovšem umí všechny statické texty překládat už během kompilace šablony. Tím se ušetří výkon, protože každý řetězec se přeloží jen jednou a výsledný překlad se zapíše do zkompilované podoby. V adresáři s cache tak vznikne více zkompilovaných verzí šablony, jedna pro každý jazyk. K tomu stačí pouze uvést jazyk jako druhý parametr: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator, $lang); -} -``` - -Statickým textem je myšleno třeba `{_'hello'}` nebo `{translate}hello{/translate}`. Nestatické texty, jako třeba `{_$foo}`, se nadále budou překládat za běhu. - -{{maintitle: Šablony v Nette: jak presentery používají Latte}} -{{description: Jak presentery v Nette pracují se šablonami Latte: hledání šablon a layoutu, proměnné, typově bezpečné šablony, odkazy, vlastní filtry a tagy, překlady.}} diff --git a/application/cs/upgrading.texy b/application/cs/upgrading.texy deleted file mode 100644 index c9b57d0f6c..0000000000 --- a/application/cs/upgrading.texy +++ /dev/null @@ -1,50 +0,0 @@ -Upgrade -******* - - -Upgrade na verzi 3.0 -==================== - -Nette 3.0 doplňuje typehinty k parametrům a návratovým hodnotám metod. Pokud takovou metodu přepisujete ve třídě zděděné z Nette (například v presenteru nebo komponentě), musíte doplnit stejné typehinty, jinak PHP vyhodí chybu "Declaration must be compatible". - -Rozhraní `Nette\Application\IRouter` bylo změněno. Metoda `match()` nyní vrací a `constructUrl()` přijímá pole parametrů namísto objektu `Nette\Application\Request`. - -Nette nyní kontroluje, zda je každý signál odeslán ze stejného originu (tj. ze stejné domény a subdomény). Tato same-origin policy je kritický bezpečnostní mechanismus, který pomáhá omezit možné vektory útoku. Pokud chcete povolit další původy, přidejte k metodě obsluhující signál anotaci `@crossOrigin`: - -```php -/** - * @crossOrigin - */ -public function handleXy(): void -{ -} -``` - -To platí také pro odesílání formulářů. Pokud chcete povolit odeslání z jiného původu, postupujte takto: - -```php -$form = new Nette\Application\UI\Form; -$form->allowCrossOrigin(); -``` - -Konstruktor `Nette\ComponentModel\Component` nebyl roky používán a byl odstraněn ve verzi 3.0. Je to BC break: pokud voláte rodičovský konstruktor v komponentě nebo presenteru zděděném od `Nette\Application\UI\Presenter`, musíte volání odstranit. - - -Upgrade na verzi 2.4 -==================== - -- `Route` i `SimpleRouter` nyní generují v URL stejné HTTP/HTTPS schéma, s jakým se přistupuje ke stránce. Routu, která vyžaduje určitý protokol, lze zapsat se schématem, např. `Route('http://domain.cz/')`. -- U parametrů typu bool použitých v render/action metodách (tj. s výchozí hodnotou true nebo false) a u persistentních parametrů se nyní rozlišuje mezi false a null. Pokud parametr v URL není uveden, jeho hodnota je nyní `null` (dříve `false`). -- Třída, kterou vrací `Presenter::getReflection()`, již není potomkem `Nette\Reflection\ClassType` a rovněž `getReflection()->getMethod()` není potomkem `Nette\Reflection\Method`. -- Flag `SECURED` a `Route::$defaultFlags` jsou zastaralé. - - -Upgrade na verzi 2.3 -==================== - -- routy a názvy presenterů jsou **case-sensitive**. Nette vás upozorní, pokud v názvu presenteru použijete špatnou velikost písmen; masku routy z výkonnostních důvodů nekontroluje, ověřte si ji tedy ručně. -- `Route::addStyle()` a `Route::setStyleProperty()` jsou zastaralé a nyní vyvolají `E_USER_DEPRECATED`. -- přípona šablony `.phtml` a stará syntaxe odkazů se již nepodporují. - -{{maintitle: Upgrade Nette Application: změny v jednotlivých verzích}} -{{description: Co se změnilo v Nette Application 3.0, 2.4 a 2.3 a jak upravit kód: typehinty, rozhraní routeru, same-origin kontrola signálů a další.}} diff --git a/application/de/@home.texy b/application/de/@home.texy deleted file mode 100644 index 65ca4bdcc0..0000000000 --- a/application/de/@home.texy +++ /dev/null @@ -1,85 +0,0 @@ -Nette Application -***************** - -.[perex] -Nette Application ist der Kern des Nette Frameworks und bietet leistungsstarke Werkzeuge für die Erstellung moderner Webanwendungen. Es bietet eine Reihe außergewöhnlicher Eigenschaften, die die Entwicklung erheblich vereinfachen und die Sicherheit sowie die Wartbarkeit des Codes verbessern. - - -Installation ------------- - -Laden Sie die Bibliothek mit [Composer|best-practices:composer] herunter und installieren Sie sie: - -```shell -composer require nette/application -``` - - -Warum Nette Application wählen? -------------------------------- - -Nette war schon immer ein Vorreiter bei Webtechnologien. - -**Bidirektionaler Router:** Nette verfügt über ein fortschrittliches Routing-System, das in seiner Bidirektionalität einzigartig ist - es übersetzt nicht nur URLs in Aktionen der Anwendung, sondern kann URLs auch umgekehrt erzeugen. Das bedeutet: -- Sie können die URL-Struktur der gesamten Anwendung jederzeit ändern, ohne die Templates anpassen zu müssen -- URLs werden automatisch kanonisiert, was das SEO verbessert -- Das Routing ist an einer einzigen Stelle definiert, nicht über Annotationen verstreut - -**Komponenten und Signale:** Das eingebaute Komponentensystem, inspiriert von Delphi und React.js, ist unter den PHP-Frameworks einzigartig: -- Es ermöglicht, wiederverwendbare UI-Elemente zu erstellen -- Es unterstützt die hierarchische Zusammensetzung von Komponenten -- Es bietet eine elegante Behandlung von AJAX-Requests mithilfe von Signalen -- Umfangreiche Bibliothek fertiger Komponenten auf [Componette](https://componette.org) - -**AJAX und Snippets:** Nette führte 2009 eine revolutionäre Art der Arbeit mit AJAX ein, lange vor ähnlichen Lösungen wie Hotwire für Ruby on Rails oder Symfony UX Turbo: -- Snippets erlauben es, nur Teile der Seite zu aktualisieren, ohne JavaScript schreiben zu müssen -- Automatische Integration mit dem Komponentensystem -- Intelligente Invalidierung von Seitenabschnitten -- Minimale Datenübertragung - -**Intuitive [Latte|latte:]-Templates:** Das sicherste Templating-System für PHP mit fortschrittlichen Funktionen: -- Automatischer XSS-Schutz durch kontextsensitives Escaping -- Erweiterbar um eigene Filter, Funktionen und Tags -- Template-Vererbung und Snippets für AJAX -- Ausgezeichnete Unterstützung für PHP 8.x samt Typsystem - -**Dependency Injection:** Nette nutzt Dependency Injection voll aus: -- Automatische Übergabe von Abhängigkeiten (Autowiring) -- Konfiguration im übersichtlichen NEON-Format -- Unterstützung für Komponenten-Factories - - -Hauptvorteile -------------- - -- **Sicherheit**: Automatischer Schutz vor [Sicherheitslücken|nette:vulnerability-protection] wie XSS, CSRF usw. -- **Produktivität**: Weniger Schreibarbeit, mehr Funktionen dank durchdachtem Design -- **Debugging**: [Tracy Debugger|tracy:] mit Routing-Panel -- **Leistung**: Intelligenter Cache, Lazy Loading von Komponenten -- **Flexibilität**: Einfache Änderung der URLs auch nach Fertigstellung der Anwendung -- **Komponenten**: Einzigartiges System wiederverwendbarer UI-Elemente -- **Modern**: Volle Unterstützung für PHP 8.3+ und das Typsystem - - -Erste Schritte --------------- - -1. [Wie funktionieren Anwendungen? |how-it-works] - Verständnis der grundlegenden Architektur -2. [Presenter |presenters] - Arbeit mit Presentern und Aktionen -3. [Templates |templates] - Templates in Latte erstellen -4. [Routing |routing] - URL-Adressen konfigurieren -5. [Interaktive Komponenten |components] - Das Komponentensystem verwenden - - -PHP-Kompatibilität ------------------- - -| Version | kompatibel mit PHP -|-----------------------|------------------- -| Nette Application 3.3 | PHP 8.3 - 8.5 -| Nette Application 3.2 | PHP 8.1 - 8.5 -| 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 - -Gilt für die jeweils letzten Patch-Versionen. diff --git a/application/de/@left-menu.texy b/application/de/@left-menu.texy deleted file mode 100644 index c9bd5699cf..0000000000 --- a/application/de/@left-menu.texy +++ /dev/null @@ -1,24 +0,0 @@ -Nette Application -***************** -- [Übersicht |@home] -- [Wie funktionieren Anwendungen? |how-it-works] -- [Bootstrapping|bootstrapping] -- [Presenter|presenters] -- [Templates|templates] -- [Verzeichnisstruktur |directory-structure] -- [Routing|routing] -- [URL-Links erstellen |creating-links] -- [Interaktive Komponenten |components] -- [AJAX & Snippets |ajax] -- [Multiplier |multiplier] -- [Konfiguration|configuration] -- [Upgrade|upgrading] - - -Weiterführende Lektüre -********************** -- [Warum Nette verwenden?|www:10-reasons-why-nette] -- [Installation |nette:installation] -- [Erstellen Sie Ihre erste Anwendung! |quickstart:] -- [Best Practices |best-practices:] -- [Fehlerbehebung |nette:troubleshooting] diff --git a/application/de/@meta.texy b/application/de/@meta.texy deleted file mode 100644 index b3b806b2ca..0000000000 --- a/application/de/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokumentation}} diff --git a/application/de/ajax.texy b/application/de/ajax.texy deleted file mode 100644 index 00e066bda7..0000000000 --- a/application/de/ajax.texy +++ /dev/null @@ -1,265 +0,0 @@ -AJAX & Snippets -*************** - -
    - -Im Zeitalter moderner Webanwendungen, in dem die Funktionalität oft zwischen Server und Browser aufgeteilt ist, ist AJAX das unverzichtbare Bindeglied. Welche Möglichkeiten bietet uns das Nette Framework in diesem Bereich? -- Senden von Teilen des Templates, sogenannten Snippets -- Übergabe von Variablen zwischen PHP und JavaScript -- Werkzeuge zum Debuggen von AJAX-Requests - -
    - - -AJAX-Request -============ - -Ein AJAX-Request unterscheidet sich grundsätzlich nicht von einem klassischen HTTP-Request. Es wird ein Presenter mit bestimmten Parametern aufgerufen. Es liegt am Presenter, wie er auf den Request antwortet - er kann Daten im JSON-Format zurückgeben, ein Stück HTML-Code senden, ein XML-Dokument usw. - -Auf der Browserseite starten wir einen AJAX-Request mit der Funktion `fetch()`: - -```js -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -.then(response => response.json()) -.then(payload => { - // die Antwort verarbeiten -}); -``` - -Auf der Serverseite wird ein AJAX-Request an der Methode `$httpRequest->isAjax()` des Services erkannt, der [den HTTP-Request kapselt |http:request]. Zur Erkennung dient der HTTP-Header `X-Requested-With`, es ist daher wesentlich, ihn mitzusenden. Innerhalb des Presenters können Sie die Methode `$this->isAjax()` verwenden. - -Wenn Sie Daten im JSON-Format senden wollen, verwenden Sie die Methode [`sendJson()` |presenters#Eine Antwort senden]. Die Methode beendet zugleich die Tätigkeit des Presenters. - -```php -public function actionExport(): void -{ - $this->sendJson($this->model->getData()); -} -``` - -Wenn Sie mit einem speziellen, für AJAX gedachten Template antworten wollen, geht das so: - -```php -public function handleClick($param): void -{ - if ($this->isAjax()) { - $this->template->setFile('path/to/ajax.latte'); - } - // ... -} -``` - - -Snippets -======== - -Das mächtigste Werkzeug, das Nette für die Verbindung von Server und Client bietet, sind Snippets. Mit ihnen verwandeln Sie eine gewöhnliche Anwendung mit minimalem Aufwand und wenigen Zeilen Code in eine AJAX-Anwendung. Wie das alles funktioniert, zeigt das Beispiel Fifteen, dessen Code Sie auf [GitHub |https://github.com/nette-examples/fifteen] finden. - -Snippets erlauben es, nur Teile der Seite zu aktualisieren, statt die ganze Seite neu zu laden. Das ist nicht nur schneller und effizienter, sondern bietet auch ein angenehmeres Benutzererlebnis. Snippets erinnern Sie vielleicht an Hotwire für Ruby on Rails oder Symfony UX Turbo. Interessanterweise hat Nette die Snippets 14 Jahre früher eingeführt. - -Wie funktionieren Snippets? Beim ersten Laden der Seite (einem Nicht-AJAX-Request) wird die gesamte Seite samt allen Snippets geladen. Wenn der Benutzer mit der Seite interagiert (z. B. auf eine Schaltfläche klickt, ein Formular absendet usw.), wird statt des Neuladens der ganzen Seite ein AJAX-Request ausgelöst. Der Code im Presenter führt die Aktion aus und entscheidet, welche Snippets aktualisiert werden müssen. Nette rendert diese Snippets und sendet sie als JSON-Payload, das ein Array mit den Snippets enthält. Der Code im Browser fügt die empfangenen Snippets anschließend wieder in die Seite ein. Es wird also nur der Code der geänderten Snippets übertragen, was Bandbreite spart und das Laden gegenüber der Übertragung des gesamten Seiteninhalts beschleunigt. Wird kein Snippet mit `redrawControl()` invalidiert, gibt Nette auch bei einem AJAX-Request die gesamte Seite zurück - Snippets werden nur dann gesendet, wenn etwas invalidiert wurde. - - -Naja ----- - -Für die Arbeit mit Snippets auf der Browserseite dient die [Bibliothek Naja |https://naja.js.org]. [Installieren Sie sie |https://naja.js.org/#/guide/01-install-setup-naja] als Node.js-Paket (für die Verwendung mit Bundlern wie Webpack, Rollup, Vite, Parcel und anderen): - -```shell -npm install naja -``` - -…oder binden Sie sie direkt in das Template der Seite ein: - -```latte - -``` - -Zuerst müssen Sie die Bibliothek [initialisieren |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization]: - -```js -naja.initialize(); -``` - -Um aus einem gewöhnlichen Link (Signal) oder dem Absenden eines Formulars einen AJAX-Request zu machen, genügt es, den betreffenden Link, das Formular oder die Schaltfläche mit der Klasse `ajax` zu kennzeichnen: - -```latte -Los - -
    - -
    - -oder - -
    - -
    -``` - - -Snippets neu zeichnen ---------------------- - -Jedes Objekt der Klasse [Control |components] (einschließlich des Presenters selbst) merkt sich, ob Änderungen eingetreten sind, die ein Neuzeichnen erfordern. Dazu dient die Methode `redrawControl()`: - -```php -public function handleLogin(string $user): void -{ - // nach dem Login muss der betreffende Teil neu gezeichnet werden - $this->redrawControl(); - // ... -} -``` - -Nette erlaubt eine noch feinere Steuerung dessen, was neu gezeichnet werden soll. Die Methode kann als Argument den Namen des Snippets entgegennehmen. Es lässt sich also auf der Ebene von Template-Teilen invalidieren (das heißt: ein Neuzeichnen erzwingen). Wird die gesamte Komponente invalidiert, wird auch jedes Snippet darin neu gezeichnet: - -```php -// invalidiert das Snippet 'header' -$this->redrawControl('header'); -``` - -Eine anstehende Invalidierung lässt sich über den zweiten Parameter `$redraw` auch aufheben: Der Aufruf `$this->redrawControl('header', redraw: false)` markiert das Snippet als nicht neu zu zeichnen. Die vollständige Signatur lautet `redrawControl(?string $snippet = null, bool $redraw = true)`. - - -Snippets in Latte ------------------ - -Die Verwendung von Snippets in Latte ist ausgesprochen einfach. Um einen Teil des Templates als Snippet zu definieren, umschließen Sie ihn einfach mit den Tags `{snippet}` und `{/snippet}`: - -```latte -{snippet header} -

    Hallo ...

    -{/snippet} -``` - -Das Snippet erzeugt in der HTML-Seite ein Element `
    ` mit einer speziell generierten `id`. Beim Neuzeichnen des Snippets wird der Inhalt dieses Elements aktualisiert. Deshalb ist es nötig, dass beim ersten Rendern der Seite auch alle Snippets gerendert werden, selbst wenn sie anfangs leer sein sollten. - -Ein Snippet lässt sich mit einem n:Attribut auch mit einem anderen Element als `
    ` erzeugen: - -```latte -
    -

    Hallo ...

    -
    -``` - - -Snippet-Bereiche ----------------- - -Namen von Snippets können auch Ausdrücke sein: - -```latte -{foreach $items as $id => $item} -
  • {$item}
  • -{/foreach} -``` - -Für sich genommen ist das ein nicht funktionsfähiger Zwischenschritt: Wird ein dynamisches Snippet außerhalb eines statischen `{snippet}` oder `{snippetArea}` gerendert, löst es ein `E_USER_WARNING` mit der Meldung *Dynamic snippets are allowed only inside static snippet/snippetArea.* aus. Das beheben wir gleich. - -So entstehen mehrere Snippets wie `item-0`, `item-1` usw. Würden wir ein dynamisches Snippet (z. B. `item-1`) direkt invalidieren, würde nichts neu gezeichnet. Der Grund ist, dass Snippets wirklich als Ausschnitte funktionieren und nur sie selbst direkt gerendert werden. Im Template gibt es jedoch technisch gesehen gar kein Snippet namens `item-1`. Es entsteht erst, wenn der Code rund um das Snippet, also die foreach-Schleife, ausgeführt wird. Deshalb kennzeichnen wir den Teil des Templates, der ausgeführt werden muss, mit dem Tag `{snippetArea}`: - -```latte -
      - {foreach $items as $id => $item} -
    • {$item}
    • - {/foreach} -
    -``` - -Und wir fordern das Neuzeichnen sowohl des einzelnen Snippets als auch des gesamten übergeordneten Bereichs an: - -```php -$this->redrawControl('itemsContainer'); -$this->redrawControl('item-1'); -``` - -Zugleich ist es ratsam, dafür zu sorgen, dass das Array `$items` nur die Einträge enthält, die neu gezeichnet werden sollen. - -Binden wir mit dem Tag `{include}` ein weiteres Template mit Snippets in das Haupttemplate ein, ist es nötig, das eingebundene Template erneut in eine `snippetArea` zu hüllen und diese zusammen mit dem Snippet zu invalidieren: - -```latte -{snippetArea include} - {include 'included.latte'} -{/snippetArea} -``` - -```latte -{* included.latte *} -{snippet item} - ... -{/snippet} -``` - -```php -$this->redrawControl('include'); -$this->redrawControl('item'); -``` - - -Snippets in Komponenten ------------------------ - -Snippets können Sie auch in [Komponenten|components] erstellen, und Nette zeichnet sie automatisch neu. Es gibt jedoch eine Einschränkung: Zum Neuzeichnen von Snippets ruft Nette die Methode `render()` ohne Parameter auf. Die Übergabe von Parametern im Template funktioniert daher nicht: - -```latte -OK -{control productGrid} - -funktioniert nicht: -{control productGrid, $arg, $arg} -{control productGrid:paginator} -``` - - -Eigene Daten senden -------------------- - -Zusammen mit den Snippets können Sie dem Client beliebige weitere Daten senden. Schreiben Sie sie einfach in das Objekt `payload`: - -```php -public function actionDelete(int $id): void -{ - // ... - if ($this->isAjax()) { - $this->payload->message = 'Erfolg'; - } -} -``` - - -Weiterleitung -------------- - -Während eines AJAX-Requests senden die Methoden `redirect()` und `redirectUrl()` keine HTTP-Weiterleitung. Stattdessen schreiben sie die Ziel-URL in das Payload (das im AJAX-Response gesendete Datenobjekt), konkret in dessen Eigenschaft `payload.redirect`, und senden es; die eigentliche Weiterleitung führt dann die clientseitige Bibliothek (Naja) aus. - - -Parameter übergeben -=================== - -Wenn wir einer Komponente über einen AJAX-Request Parameter senden, seien es Signal- oder persistente Parameter, müssen wir im Request ihren globalen Namen angeben, der auch den Namen der Komponente enthält. Den vollständigen Parameternamen liefert die Methode `getParameterId()`. - -```js -let url = new URL({link //foo!}); -url.searchParams.set({$control->getParameterId('bar')}, bar); - -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -``` - -Und die handle-Methode mit den entsprechenden Parametern in der Komponente: - -```php -public function handleFoo(int $bar): void -{ -} -``` - - -Weiterführende Lektüre -====================== - -- [Dynamische Snippets |best-practices:dynamic-snippets] diff --git a/application/de/bootstrapping.texy b/application/de/bootstrapping.texy deleted file mode 100644 index dabfea3a5b..0000000000 --- a/application/de/bootstrapping.texy +++ /dev/null @@ -1,312 +0,0 @@ -Bootstrapping -************* - -
    - -Bootstrapping ist der Prozess der Initialisierung der Anwendungsumgebung, der Erzeugung des Dependency-Injection-Containers (DI) und des Startens der Anwendung. Wir besprechen: - -- wie die Klasse Bootstrap die Umgebung initialisiert -- wie Anwendungen mit NEON-Dateien konfiguriert werden -- wie man Produktions- und Entwicklungsmodus unterscheidet -- wie man den DI-Container erzeugt und konfiguriert - -
    - - -Anwendungen, seien es Web-Anwendungen oder von der Kommandozeile ausgeführte Skripte, beginnen ihre Ausführung mit einer Form der Initialisierung der Umgebung. Früher kümmerte sich darum eine Datei mit einem Namen wie `include.inc.php`, die von der Startdatei eingebunden wurde. In modernen Nette-Anwendungen wurde sie durch die Klasse `Bootstrap` ersetzt, die als Teil der Anwendung in der Datei `app/Bootstrap.php` zu finden ist. Sie kann zum Beispiel so aussehen: - -```php -namespace App; - -use Nette; -use Nette\Bootstrap\Configurator; - -class Bootstrap -{ - private Configurator $configurator; - private string $rootDir; - - public function __construct() - { - $this->rootDir = dirname(__DIR__); - // Der Configurator ist für das Einrichten der Anwendungsumgebung und der Services zuständig. - $this->configurator = new Configurator; - // Verzeichnis für von Nette erzeugte temporäre Dateien festlegen (z. B. kompilierte Templates) - $this->configurator->setTempDirectory($this->rootDir . '/temp'); - } - - public function bootWebApplication(): Nette\DI\Container - { - $this->initializeEnvironment(); - $this->setupContainer(); - return $this->configurator->createContainer(); - } - - private function initializeEnvironment(): void - { - // Nette ist schlau, und der Entwicklungsmodus schaltet sich automatisch ein, - // oder Sie aktivieren ihn für eine bestimmte IP-Adresse, indem Sie die folgende Zeile einkommentieren: - // $this->configurator->setDebugMode('secret@23.75.345.200'); - - // Aktiviert Tracy: das ultimative "Schweizer Taschenmesser" zum Debuggen. - $this->configurator->enableTracy($this->rootDir . '/log'); - - // RobotLoader: lädt automatisch alle Klassen im gewählten Verzeichnis - $this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); - } - - private function setupContainer(): void - { - // Konfigurationsdateien laden - $this->configurator->addConfig($this->rootDir . '/config/common.neon'); - } -} -``` - - -index.php -========= - -Bei Webanwendungen ist die Startdatei `index.php`, die im [öffentlichen Verzeichnis |directory-structure#Öffentliches Verzeichnis www/] `www/` liegt. Sie lässt die Klasse Bootstrap die Umgebung initialisieren und den DI-Container erzeugen. Anschließend holt sie sich aus dem Container den Service `Application`, der die Webanwendung startet: - -```php -$bootstrap = new App\Bootstrap; -// Umgebung initialisieren + DI-Container erzeugen -$container = $bootstrap->bootWebApplication(); -// Der DI-Container erzeugt ein Objekt Nette\Application\Application -$application = $container->getByType(Nette\Application\Application::class); -// Die Nette-Anwendung starten und den eingehenden Request verarbeiten -$application->run(); -``` - -.[note] -Das Objekt `$application` löst während der Verarbeitung des Requests [Events |nette:glossary#Events] aus - `onStartup`, `onRequest`, `onPresenter`, `onResponse`, `onShutdown` und `onError` (bei einer unbehandelten Exception). Sie können daran Handler hängen, was sich für Logging oder anwendungsweites Monitoring anbietet. - -Wie Sie sehen, hilft die Klasse [api:Nette\Bootstrap\Configurator] beim Einrichten der Umgebung und beim Erzeugen des Dependency-Injection-Containers (DI). Stellen wir sie nun genauer vor. - - -Entwicklungs- vs. Produktionsmodus -================================== - -Nette verhält sich unterschiedlich, je nachdem, ob es auf einem Entwicklungs- oder einem Produktionsserver läuft: - -🛠️ Entwicklungsmodus: - - Zeigt die Tracy Debug Bar mit nützlichen Informationen (SQL-Queries, Ausführungszeit, verbrauchter Speicher) - - Zeigt bei einem Fehler eine detaillierte Fehlerseite mit Funktionsaufrufen und Variableninhalten - - Aktualisiert den Cache automatisch, wenn Latte-Templates, Konfigurationsdateien usw. geändert werden - - -🚀 Produktionsmodus: - - Zeigt keinerlei Debugging-Informationen an, alle Fehler werden ins Log geschrieben - - Zeigt bei einem Fehler den ErrorPresenter oder eine allgemeine Seite "Server Error" - - Der Cache wird niemals automatisch aktualisiert! - - Optimiert auf Geschwindigkeit und Sicherheit - - -Die Wahl des Modus erfolgt per Autodetection, üblicherweise ist also nichts zu konfigurieren und der Modus nicht manuell umzuschalten: - -- Entwicklungsmodus: auf localhost (IP-Adresse `127.0.0.1` oder `::1`), sofern kein Proxy vorhanden ist (also dessen HTTP-Header nicht erkannt wird) -- Produktionsmodus: überall sonst - -Wollen wir den Entwicklungsmodus auch in anderen Fällen einschalten, etwa für Programmierer, die von einer bestimmten IP-Adresse zugreifen, verwenden wir `setDebugMode()`: - -```php -$this->configurator->setDebugMode('23.75.345.200'); // es lässt sich auch ein Array von IP-Adressen angeben -``` - -Wir empfehlen dringend, die IP-Adresse mit einem Cookie zu kombinieren. Legen Sie im Cookie `nette-debug` ein geheimes Token ab, z. B. `secret1234`, und aktivieren Sie so den Entwicklungsmodus für Programmierer, die von einer bestimmten IP-Adresse zugreifen und zugleich das genannte Token im Cookie haben: - -```php -$this->configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Den Entwicklungsmodus können wir auch vollständig abschalten, sogar für localhost: - -```php -$this->configurator->setDebugMode(false); -``` - -Beachten Sie, dass der Wert `true` den Entwicklungsmodus erzwingt, was auf einem Produktionsserver **niemals** passieren darf. - -Die Autodetection erledigt intern die statische Methode `Configurator::detectDebugMode()`, die Sie auch selbst aufrufen können, etwa um den Entwicklungsmodus außerhalb des Configurators zu erkennen. Sie nimmt optional eine Whitelist von IP-Adressen oder Rechnernamen entgegen und gibt zurück, ob der aktuelle Request im Entwicklungsmodus laufen soll: - -```php -$debug = Nette\Bootstrap\Configurator::detectDebugMode('23.75.345.200'); -``` - - -Debugging-Tool Tracy -==================== - -Für bequemes Debuggen aktivieren wir das ausgezeichnete Werkzeug [Tracy |tracy:]. Im Entwicklungsmodus visualisiert es Fehler, im Produktionsmodus protokolliert es sie in das angegebene Verzeichnis: - -```php -$this->configurator->enableTracy($this->rootDir . '/log'); -``` - - -Temporäre Dateien -================= - -Nette verwendet Cache für den DI-Container, RobotLoader, Templates usw. Deshalb muss der Pfad zu dem Verzeichnis gesetzt werden, in dem der Cache abgelegt wird: - -```php -$this->configurator->setTempDirectory($this->rootDir . '/temp'); -``` - -Setzen Sie unter Linux oder macOS für die Verzeichnisse `log/` und `temp/` [Schreibrechte |nette:troubleshooting#Einstellung der Verzeichnisberechtigungen]. - - -RobotLoader -=========== - -Üblicherweise wollen wir Klassen automatisch mit [RobotLoader |robot-loader:] laden. Wir müssen ihn also starten und Klassen aus dem Verzeichnis laden lassen, in dem `Bootstrap.php` liegt (also `__DIR__`), samt allen Unterverzeichnissen: - -```php -$this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); -``` - -Ein alternativer Weg ist, Klassen ausschließlich über [Composer |best-practices:composer] nach PSR-4 zu laden. - - -Zeitzone -======== - -Über den Configurator lässt sich die Standard-Zeitzone einstellen. - -```php -$this->configurator->setTimeZone('Europe/Prague'); -``` - - -Konfiguration des DI-Containers -=============================== - -Teil des Startvorgangs ist die Erzeugung des DI-Containers, also der Objekt-Factory, die das Herz der gesamten Anwendung ist. Es handelt sich tatsächlich um eine von Nette erzeugte und im Cache-Verzeichnis abgelegte PHP-Klasse. Die Factory stellt die Schlüsselobjekte der Anwendung her, und mit Konfigurationsdateien weisen wir sie an, wie sie diese erzeugen und einrichten soll - und beeinflussen damit das Verhalten der gesamten Anwendung. - -Konfigurationsdateien werden üblicherweise im [NEON-Format |neon:format] geschrieben. In einem eigenen Kapitel lesen Sie, [was sich alles konfigurieren lässt |nette:configuring]. - -.[tip] -Im Entwicklungsmodus wird der Container bei jeder Änderung des Codes oder der Konfigurationsdateien automatisch aktualisiert. Im Produktionsmodus wird er nur einmal erzeugt, und Änderungen werden nicht geprüft, um die Leistung zu maximieren. - -Während `createContainer()` den Container baut und seine Instanz zurückgibt, liefert die Methode `loadContainer()` nur den Namen der generierten Container-Klasse, die Sie dann selbst instanziieren können. Das ist in fortgeschrittenen Szenarien nützlich. - -Konfigurationsdateien werden mit `addConfig()` geladen: - -```php -$this->configurator->addConfig($this->rootDir . '/config/common.neon'); -``` - -Wollen wir weitere Konfigurationsdateien hinzufügen, können wir die Funktion `addConfig()` mehrfach aufrufen. - -```php -$configDir = $this->rootDir . '/config'; -$this->configurator->addConfig($configDir . '/common.neon'); -$this->configurator->addConfig($configDir . '/services.neon'); -if (PHP_SAPI === 'cli') { - $this->configurator->addConfig($configDir . '/cli.php'); -} -``` - -Der Name `cli.php` ist kein Tippfehler; die Konfiguration lässt sich auch in einer PHP-Datei schreiben, die sie als Array zurückgibt. - -Weitere Konfigurationsdateien können wir auch [im Abschnitt `includes` |dependency-injection:configuration#Dateien einbinden] hinzufügen. - -Erscheinen in den Konfigurationsdateien Einträge mit denselben Schlüsseln, werden sie überschrieben, im Fall von [Arrays zusammengeführt |dependency-injection:configuration#Zusammenführen]. Eine später eingebundene Datei hat höhere Priorität als die vorherige. Die Datei, in der der Abschnitt `includes` steht, hat höhere Priorität als die darin eingebundenen Dateien. - - -Statische Parameter -------------------- - -Parameter, die in Konfigurationsdateien verwendet werden, lassen sich [im Abschnitt `parameters` |dependency-injection:configuration#Parameter] definieren und außerdem mit der Methode `addStaticParameters()` übergeben (oder überschreiben), deren älterer, inzwischen veralteter Alias `addParameters()` lautet. Wichtig ist, dass unterschiedliche Parameterwerte die Erzeugung weiterer DI-Container, also weiterer Klassen, bewirken. - -```php -$this->configurator->addStaticParameters([ - 'projectId' => 23, -]); -``` - -Auf den Parameter `projectId` lässt sich in der Konfiguration mit der üblichen Schreibweise `%projectId%` verweisen. - - -Dynamische Parameter --------------------- - -Dem Container können wir auch dynamische Parameter hinzufügen, deren unterschiedliche Werte im Gegensatz zu statischen Parametern keine Erzeugung neuer DI-Container bewirken. - -```php -$this->configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -So lassen sich zum Beispiel bequem Umgebungsvariablen ergänzen, auf die man dann in der Konfiguration mit der Schreibweise `%env.variable%` verweisen kann. - -```php -$this->configurator->addDynamicParameters([ - 'env' => getenv(), -]); -``` - - -Standardparameter ------------------ - -In den Konfigurationsdateien können Sie diese Parameter verwenden: - -- `%appDir%` ist der absolute Pfad zum Verzeichnis mit der Datei `Bootstrap.php` -- `%wwwDir%` ist der absolute Pfad zum Verzeichnis mit der Startdatei `index.php` -- `%tempDir%` ist der absolute Pfad zum Verzeichnis für temporäre Dateien -- `%vendorDir%` ist der absolute Pfad zum Verzeichnis, in das Composer die Bibliotheken installiert -- `%rootDir%` ist der absolute Pfad zum Wurzelverzeichnis des Projekts -- `%baseUrl%` ist die absolute URL zum Wurzelverzeichnis (ein dynamischer Parameter, der zur Laufzeit aufgelöst wird) -- `%debugMode%` gibt an, ob die Anwendung im Debug-Modus läuft -- `%consoleMode%` gibt an, ob der Request über die Kommandozeile kam - - -Importierte Services --------------------- - -Jetzt gehen wir tiefer. Obwohl der Zweck des DI-Containers darin besteht, Objekte zu erzeugen, kann gelegentlich der Bedarf entstehen, ein bereits existierendes Objekt in den Container einzufügen. Wir tun das, indem wir den Service mit dem Flag `imported: true` definieren. - -```neon -services: - myservice: - type: App\Model\MyCustomService - imported: true -``` - -Und im Bootstrap fügen wir das Objekt in den Container ein: - -```php -$this->configurator->addServices([ - 'myservice' => new App\Model\MyCustomService('foobar'), -]); -``` - - -Verschiedene Umgebungen -======================= - -Passen Sie die Klasse `Bootstrap` ruhig Ihren Bedürfnissen an. Sie können der Methode `bootWebApplication()` Parameter hinzufügen, um zwischen Webprojekten zu unterscheiden. Oder wir ergänzen weitere Methoden, etwa `bootTestEnvironment()`, die die Umgebung für Unit-Tests initialisiert, `bootConsoleApplication()` für von der Kommandozeile aufgerufene Skripte usw. - -```php -public function bootTestEnvironment(): Nette\DI\Container -{ - Tester\Environment::setup(); // Initialisierung von Nette Tester - $this->setupContainer(); - return $this->configurator->createContainer(); -} - -public function bootConsoleApplication(): Nette\DI\Container -{ - $this->configurator->setDebugMode(false); - $this->initializeEnvironment(); - $this->setupContainer(); - return $this->configurator->createContainer(); -} -``` diff --git a/application/de/components.texy b/application/de/components.texy deleted file mode 100644 index d6a7e7f72b..0000000000 --- a/application/de/components.texy +++ /dev/null @@ -1,504 +0,0 @@ -Interaktive Komponenten -*********************** - -
    - -Komponenten sind eigenständige, wiederverwendbare Objekte, die wir in Seiten einbetten. Das können Formulare, Datagrids, Umfragen sein - kurz alles, was sich sinnvoll wiederholt verwenden lässt. Wir zeigen: - -- wie man Komponenten verwendet? -- wie man sie schreibt? -- was Signale sind? - -
    - -Nette hat ein eingebautes Komponentensystem. Etwas Ähnliches kennen Veteranen vielleicht aus Delphi oder ASP.NET Web Forms; React oder Vue.js bauen auf entfernt Ähnlichem auf. In der Welt der PHP-Frameworks ist das jedoch eine einzigartige Eigenschaft. - -Zugleich beeinflussen Komponenten die Herangehensweise an die Anwendungsentwicklung grundlegend. Sie können Seiten aus vorbereiteten Einheiten zusammensetzen. Brauchen Sie in Ihrer Administration ein Datagrid? Finden Sie es auf [Componette |https://componette.org/search/component], einem Repository von Open-Source-Erweiterungen (nicht nur Komponenten) für Nette, und fügen Sie es einfach in den Presenter ein. - -Sie können beliebig viele Komponenten in den Presenter einbinden. Und in manche Komponenten können Sie weitere Komponenten einbetten. So entsteht ein Komponentenbaum, dessen Wurzel der Presenter ist. - - -Factory-Methoden -================ - -Wie werden Komponenten in den Presenter eingefügt und anschließend verwendet? Üblicherweise über Factory-Methoden. - -Eine Factory für Komponenten ist ein eleganter Weg, Komponenten erst dann zu erzeugen, wenn sie tatsächlich gebraucht werden (lazy / on demand). Die ganze Magie steckt in der Implementierung einer Methode namens `createComponent()`, wobei `` der Name der erzeugten Komponente ist und die die Komponente erzeugt und zurückgibt. - -```php .{file:DefaultPresenter.php} -class DefaultPresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentPoll(): PollControl - { - $poll = new PollControl; - $poll->items = $this->items; - return $poll; - } -} -``` - -Weil alle Komponenten in eigenen Methoden erzeugt werden, wird der Code übersichtlicher. - -.[note] -Namen von Komponenten beginnen immer mit einem Kleinbuchstaben, obwohl sie im Methodennamen großgeschrieben werden. - -Wir rufen Factories nie direkt auf; sie werden automatisch beim ersten Verwenden der Komponente aufgerufen. Dadurch wird die Komponente im richtigen Moment erzeugt und nur dann, wenn sie tatsächlich gebraucht wird. Verwenden wir die Komponente nicht (z. B. bei einem AJAX-Request, bei dem nur ein Teil der Seite übertragen wird, oder beim Cachen des Templates), wird sie überhaupt nicht erzeugt, was Serverleistung spart. - -```php .{file:DefaultPresenter.php} -// wir greifen auf die Komponente zu, und wenn es das erste Mal war, -// wird createComponentPoll() aufgerufen, die sie erzeugt -$poll = $this->getComponent('poll'); -// alternative Schreibweise: $poll = $this['poll']; -``` - -Im Template lässt sich eine Komponente mit dem Tag [{control} |#Rendering] rendern. Komponenten müssen daher nicht von Hand an das Template übergeben werden. - -```latte -

    Bitte stimmen Sie ab

    - -{control poll} -``` - -.[tip] -Um dynamisch eine variable Anzahl von Komponenten zu erzeugen, verwenden Sie den [Multiplier |multiplier]. - -Die Factory-Methoden `createComponent()` funktionieren nicht nur in Presentern. Auf dieselbe Weise können Sie eine Komponente in eine andere Komponente verschachteln und sie zu einem Baum zusammensetzen - praktisch zum Beispiel für ein separat gerendertes Formular innerhalb einer Komponente. - - -Hollywood Style -=============== - -Komponenten nutzen gewöhnlich eine erfrischende Technik, die wir gerne Hollywood Style nennen. Sie kennen sicher das Klischee, das Teilnehmer von Filmcastings oft hören: "Rufen Sie uns nicht an, wir rufen Sie an." Und genau darum geht es. - -In Nette müssen Sie nicht ständig Fragen stellen ("wurde das Formular abgeschickt?", "war es gültig?", oder "hat der Benutzer diese Schaltfläche gedrückt?"), sondern Sie sagen dem Framework "wenn das passiert, rufe diese Methode auf" und überlassen ihm die weitere Arbeit. Wenn Sie in JavaScript programmieren, ist Ihnen dieser Programmierstil bestens vertraut. Sie schreiben Funktionen, die aufgerufen werden, wenn ein bestimmtes Ereignis eintritt. Und die Sprache übergibt ihnen die passenden Parameter. - -Das ändert die Sicht auf das Schreiben von Anwendungen vollständig. Je mehr Aufgaben Sie dem Framework überlassen können, desto weniger Arbeit haben Sie. Und desto weniger können Sie übersehen. - - -Eine Komponente schreiben -========================= - -Mit dem Begriff Komponente meinen wir üblicherweise einen Nachfahren der Klasse [api:Nette\Application\UI\Control]. (Genauer wäre der Begriff "Controls", aber der hat in manchen Sprachen eine andere Bedeutung, und "Komponenten" hat sich stärker eingebürgert.) Auch der Presenter [api:Nette\Application\UI\Presenter] selbst ist ein Nachfahre der Klasse `Control`. - -```php .{file:PollControl.php} -use Nette\Application\UI\Control; - -class PollControl extends Control -{ -} -``` - - -Rendering -========= - -Wir wissen bereits, dass zum Rendern einer Komponente der Tag `{control componentName}` dient. Er ruft in Wirklichkeit die Methode `render()` der Komponente auf, in der wir uns um das Rendering kümmern. Wir haben, genau wie im Presenter, in der Variablen `$this->template` ein [Latte-Template|templates] zur Verfügung, dem wir Parameter übergeben. Anders als im Presenter müssen wir die Template-Datei angeben und sie rendern lassen: - -```php .{file:PollControl.php} -public function render(): void -{ - // einige Parameter in das Template einfügen - $this->template->param = $value; - // und es rendern - $this->template->render(__DIR__ . '/poll.latte'); -} -``` - -Der Tag `{control}` erlaubt es, der Methode `render()` Parameter zu übergeben: - -```latte -{control poll, $id, $message} -``` - -```php .{file:PollControl.php} -public function render(int $id, string $message): void -{ - // ... -} -``` - -Manchmal kann eine Komponente aus mehreren Teilen bestehen, die wir getrennt rendern wollen. Für jeden davon legen wir eine eigene Rendering-Methode an, im Beispiel hier `renderPaginator()`: - -```php .{file:PollControl.php} -public function renderPaginator(): void -{ - // ... -} -``` - -Und im Template rufen wir sie dann so auf: - -```latte -{control poll:paginator} -``` - -Zum besseren Verständnis ist es gut zu wissen, wie sich dieser Tag in PHP-Code übersetzt. - -```latte -{control poll} -{control poll:paginator, 123, 'hello'} -``` - -übersetzt sich zu: - -```php -$control->getComponent('poll')->render(); -$control->getComponent('poll')->renderPaginator(123, 'hello'); -``` - -Die Methode `getComponent()` gibt die Komponente `poll` zurück, und auf dieser Komponente wird die Methode `render()` aufgerufen, bzw. `renderPaginator()`, wenn im Tag nach dem Doppelpunkt eine andere Rendering-Methode angegeben ist. - -.[caution] -Achtung: Erscheint in den Parametern außerhalb eckiger Klammern ein **`=>`**, werden alle Parameter in ein Array verpackt und als erstes Argument übergeben: - -```latte -{control poll, id: 123, message: 'hello'} -``` - -übersetzt sich zu: - -```php -$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']); -``` - -Rendering einer Unterkomponente: - -```latte -{control cartControl-someForm} -``` - -übersetzt sich zu: - -```php -$control->getComponent("cartControl-someForm")->render(); -``` - -Komponenten übergeben Templates, ebenso wie Presenter, automatisch mehrere nützliche Variablen: - -- `$basePath` ist der absolute URL-Pfad zum Wurzelverzeichnis (z. B. `/eshop`) -- `$baseUrl` ist die absolute URL zum Wurzelverzeichnis (z. B. `http://localhost/eshop`) -- `$user` ist ein Objekt, das [den Benutzer repräsentiert |security:authentication] -- `$presenter` ist der aktuelle Presenter -- `$control` ist die aktuelle Komponente -- `$flashes` ist ein Array von [Meldungen |#Flash-Meldungen], die mit der Funktion `flashMessage()` gesendet wurden - - -Signal -====== - -Wir wissen bereits, dass die Navigation in einer Nette-Anwendung im Verlinken oder Weiterleiten auf Paare `Presenter:action` besteht. Was aber, wenn wir nur eine Aktion auf der **aktuellen Seite** ausführen wollen? Zum Beispiel die Sortierung von Spalten einer Tabelle ändern; ein Element löschen; zwischen hellem und dunklem Modus umschalten; ein Formular absenden; in einer Umfrage abstimmen usw. - -Diese Art von Request nennt man Signal. Und so, wie Aktionen die Methoden `action()` oder `render()` aufrufen, rufen Signale die Methoden `handle()` auf. Während sich der Begriff Aktion (oder View) rein auf Presenter bezieht, betreffen Signale alle Komponenten. Und damit auch Presenter, denn `UI\Presenter` ist ein Nachfahre von `UI\Control`. - -```php -public function handleClick(int $x, int $y): void -{ - // ... Verarbeitung des Signals ... -} -``` - -Einen Link, der ein Signal aufruft, erstellt man auf die übliche Weise, also im Template mit dem Attribut `n:href` oder dem Tag `{link}`, im Code mit der Methode `link()`. Mehr im Kapitel [Erstellen von URL-Links |creating-links#Links auf Signale]. - -```latte -hier klicken -``` - -Ein Signal wird immer auf dem aktuellen Presenter und der aktuellen Aktion aufgerufen; es lässt sich nicht auf einem anderen Presenter oder einer anderen Aktion auslösen. - -Ein Signal bewirkt also, dass die Seite genau wie beim ursprünglichen Request neu geladen wird, ruft zusätzlich aber die Methode zur Verarbeitung des Signals mit den passenden Parametern auf. Existiert die Methode nicht, wird eine Exception [api:Nette\Application\UI\BadSignalException] geworfen, die dem Benutzer als Fehlerseite 403 Forbidden angezeigt wird. - - -Snippets und AJAX -================= - -Signale erinnern Sie vielleicht ein wenig an AJAX: Handler, die auf der aktuellen Seite aufgerufen werden. Und Sie haben recht, Signale werden tatsächlich oft mit AJAX aufgerufen, und anschließend werden nur die geänderten Teile der Seite in den Browser übertragen. Diese nennt man Snippets. Mehr dazu finden Sie auf der [Seite über AJAX |ajax]. - - -Flash-Meldungen -=============== - -Eine Komponente hat einen eigenen, vom Presenter unabhängigen Speicher für Flash-Meldungen. Das sind Meldungen, die zum Beispiel über das Ergebnis einer Operation informieren. Eine wichtige Eigenschaft von Flash-Meldungen ist, dass sie im Template auch nach einer Weiterleitung verfügbar sind. Auch nach der Anzeige bleiben sie noch 30 Sekunden aktiv - zum Beispiel für den Fall, dass der Benutzer die Seite wegen eines Übertragungsfehlers neu lädt, verschwindet die Meldung nicht sofort. - -Das Senden übernimmt die Methode [flashMessage |api:Nette\Application\UI\Control::flashMessage()]. Der erste Parameter ist der Text der Meldung (`string`, `Stringable`) oder ein Objekt `stdClass`, das die Meldung repräsentiert. Der optionale zweite Parameter ist ihr Typ (error, warning, info usw.). Die Methode `flashMessage()` gibt eine Instanz der Flash-Meldung als Objekt `stdClass` zurück, dem sich weitere Informationen hinzufügen lassen. - -```php -$this->flashMessage('Das Element wurde gelöscht.'); -$this->redirect(/* ... */); // und weiterleiten -``` - -Diese Meldungen stehen dem Template in der Variablen `$flashes` als Objekte `stdClass` zur Verfügung, die die Properties `message` (Text der Meldung) und `type` (Typ der Meldung) enthalten und die erwähnten eigenen Informationen enthalten können. Wir rendern sie zum Beispiel so: - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -Weiterleitung nach Verarbeitung eines Signals -============================================= - -Auf die Verarbeitung eines Signals einer Komponente folgt oft eine Weiterleitung. Das ist ähnlich wie bei Formularen - nach dem Absenden leiten wir ebenfalls weiter, damit die Daten beim Neuladen der Seite im Browser nicht erneut abgeschickt werden. - -```php -$this->redirect('this'); // leitet auf den aktuellen Presenter und die aktuelle Aktion weiter -``` - -Da eine Komponente ein wiederverwendbares Element ist und üblicherweise keine direkte Bindung an konkrete Presenter haben sollte, interpretieren die Methoden `redirect()` und `link()` den Parameter automatisch als Signal der Komponente: - -```php -$this->redirect('click'); // leitet auf das Signal 'click' derselben Komponente weiter -``` - -Müssen Sie auf einen anderen Presenter oder eine andere Aktion weiterleiten, geht das über den Presenter: - -```php -$this->getPresenter()->redirect('Product:show'); // leitet auf einen anderen Presenter/eine andere Aktion weiter -``` - - -Persistente Parameter -===================== - -Persistente Parameter dienen dazu, den Zustand von Komponenten über verschiedene Requests hinweg zu erhalten. Ihr Wert bleibt auch nach dem Klick auf einen Link derselbe. Anders als Daten in der Session werden sie in der URL übertragen. Und das geschieht vollautomatisch, einschließlich der Links, die in anderen Komponenten auf derselben Seite erstellt werden. - -Sie haben zum Beispiel eine Komponente zur Paginierung von Inhalten. Auf einer Seite können mehrere solcher Komponenten sein. Und wir wollen, dass alle Komponenten nach dem Klick auf einen Link auf ihrer aktuellen Seite bleiben. Deshalb machen wir die Seitennummer (`page`) zu einem persistenten Parameter. - -Einen persistenten Parameter in Nette zu erstellen ist ausgesprochen einfach. Legen Sie einfach eine public Property an und kennzeichnen Sie sie mit dem Attribut: (früher wurde `/** @persistent */` verwendet) - -```php -use Nette\Application\Attributes\Persistent; // diese Zeile ist wichtig - -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; // muss public sein -} -``` - -Wir empfehlen, für die Property den Datentyp anzugeben (z. B. `int`), und Sie können auch einen Standardwert angeben. Die Werte der Parameter lassen sich [validieren |#Validierung persistenter Parameter]. - -Beim Erstellen eines Links lässt sich der Wert eines persistenten Parameters ändern: - -```latte -weiter -``` - -Oder er lässt sich *zurücksetzen*, also aus der URL entfernen. Er nimmt dann seinen Standardwert an: - -```latte -zurücksetzen -``` - - -Persistente Komponenten -======================= - -Nicht nur Parameter, sondern auch Komponenten können persistent sein. Ihre persistenten Parameter werden dann auch zwischen verschiedenen Aktionen des Presenters oder zwischen mehreren Presentern übertragen. Persistente Komponenten kennzeichnen wir mit einem Attribut an der Klasse des Presenters. So kennzeichnen wir zum Beispiel die Komponenten `calendar` und `poll`: - -```php -use Nette\Application\Attributes\Persistent; - -#[Persistent('calendar', 'poll')] -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Unterkomponenten innerhalb dieser Komponenten müssen nicht gekennzeichnet werden; auch sie werden persistent. - -Die ältere Annotation `@persistent` funktioniert weiterhin, ist aber veraltet und löst eine Warnung aus: - -```php -/** - * @persistent(calendar, poll) - */ -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Komponenten mit Abhängigkeiten -============================== - -Wie erstellt man Komponenten mit Abhängigkeiten, ohne die Presenter, die sie verwenden, zu "verstopfen"? Dank der klugen Fähigkeiten des DI-Containers in Nette lässt sich, ähnlich wie bei klassischen Services, der Großteil der Arbeit dem Framework überlassen. - -Nehmen wir als Beispiel eine Komponente, die eine Abhängigkeit vom Service `PollFacade` hat: - -```php -class PollControl extends Control -{ - public function __construct( - private int $id, // ID der Umfrage, für die wir die Komponente erstellen - private PollFacade $facade, - ) { - } - - public function handleVote(int $voteId): void - { - $this->facade->vote($this->id, $voteId); - // ... - } -} -``` - -Würden wir einen klassischen Service schreiben, gäbe es nichts zu besprechen. Der DI-Container würde die Übergabe aller Abhängigkeiten unsichtbar erledigen. Bei Komponenten lösen wir das jedoch üblicherweise so, dass wir in den [#Factory-Methoden] `createComponent…()` direkt im Presenter eine neue Instanz erzeugen. Aber alle Abhängigkeiten aller Komponenten in den Presenter zu übergeben, nur um sie an die Komponenten weiterzureichen, ist umständlich. Und die Menge des geschriebenen Codes … - -Die logische Frage lautet: Warum registrieren wir die Komponente nicht einfach als klassischen Service, übergeben sie dem Presenter und geben sie dann in der Methode `createComponent…()` zurück? Dieser Ansatz ist jedoch ungeeignet, weil wir die Möglichkeit haben wollen, die Komponente bei Bedarf mehrfach zu erzeugen. - -Die richtige Lösung ist, für die Komponente eine Factory zu schreiben, also eine Klasse, die die Komponente für uns erzeugt: - -```php -class PollControlFactory -{ - public function __construct( - private PollFacade $facade, - ) { - } - - public function create(int $id): PollControl - { - return new PollControl($id, $this->facade); - } -} -``` - -Diese Factory registrieren wir in der Konfiguration in unserem Container: - -```neon -services: - - PollControlFactory -``` - -und schließlich verwenden wir sie in unserem Presenter: - -```php -class PollPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private PollControlFactory $pollControlFactory, - ) { - } - - protected function createComponentPollControl(): PollControl - { - $pollId = 1; // wir können unseren Parameter übergeben - return $this->pollControlFactory->create($pollId); - } -} -``` - -Das Großartige daran ist, dass Nette DI solche einfachen Factories [generieren |dependency-injection:factory] kann, sodass Sie statt des gesamten Codes nur ihr Interface schreiben müssen: - -```php -interface PollControlFactory -{ - public function create(int $id): PollControl; -} -``` - -Und das ist alles. Nette implementiert dieses Interface intern und injiziert es in den Presenter, wo wir es verwenden können. Es ergänzt unsere Komponente auf magische Weise um den Parameter `$id` und eine Instanz der Klasse `PollFacade`. - - -Komponenten im Detail -===================== - -Komponenten stellen in Nette Application wiederverwendbare Teile einer Webanwendung dar, die wir in Seiten einbetten und denen dieses ganze Kapitel gewidmet ist. Was genau kann eine solche Komponente? - -1) Sie ist in einem Template renderbar -2) Sie weiß, [welchen Teil von sich |ajax#Snippets] sie bei einem AJAX-Request rendern soll (Snippets) -3) Sie kann ihren Zustand in der URL speichern (persistente Parameter) -4) Sie kann auf Benutzeraktionen reagieren (Signale) -5) Sie bildet eine hierarchische Struktur (deren Wurzel der Presenter ist) - -Jede dieser Funktionen übernimmt eine der Klassen in der Vererbungslinie. Um das Rendering (1 + 2) kümmert sich [api:Nette\Application\UI\Control], um die Einbindung in den [Lebenszyklus |presenters#Lebenszyklus des Presenters] (3, 4) die Klasse [api:Nette\Application\UI\Component], und um den Aufbau der hierarchischen Struktur (5) die Klassen [Container und Component |component-model:]. - -``` -Nette\ComponentModel\Component { IComponent } -| -+- Nette\ComponentModel\Container { IContainer } - | - +- Nette\Application\UI\Component { SignalReceiver, StatePersistent } - | - +- Nette\Application\UI\Control { Renderable } - | - +- Nette\Application\UI\Presenter { IPresenter } -``` - - -Lebenszyklus einer Komponente ------------------------------ - -[* lifecycle-component.svg *] *** *Lebenszyklus einer Komponente* .<> - - -Validierung persistenter Parameter ----------------------------------- - -Die aus der URL empfangenen Werte der [#Persistente Parameter] werden von der Methode `loadState()` in die Properties geschrieben. Sie prüft außerdem, ob der für die Property angegebene Datentyp passt; andernfalls antwortet sie mit dem Fehler 404 und die Seite wird nicht angezeigt. - -Vertrauen Sie persistenten Parametern niemals blind, denn sie lassen sich vom Benutzer in der URL leicht überschreiben. So prüfen wir zum Beispiel, ob die Seitennummer `$this->page` größer als 0 ist. Ein geeigneter Weg ist, die erwähnte Methode `loadState()` zu überschreiben: - -```php -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; - - public function loadState(array $params): void - { - parent::loadState($params); // hier wird $this->page gesetzt - // es folgt die eigene Wertprüfung: - if ($this->page < 1) { - $this->error(); - } - } -} -``` - -Den umgekehrten Vorgang, also das Einsammeln der Werte aus den persistenten Properties, übernimmt die Methode `saveState()`. - - -Verbindung mit dem Presenter ----------------------------- - -In dem Moment, in dem eine Komponente Teil der Hierarchie des Presenters wird, werden ihre im Array `$onAnchor` gespeicherten Callbacks aufgerufen. Ab diesem Zeitpunkt hat die Komponente den Presenter zur Verfügung, kann gefahrlos Links erstellen, persistente Parameter lesen und so weiter. - -```php -$control->onAnchor[] = function ($control): void { - // die Komponente hat nun den Presenter zur Verfügung -}; -``` - - -Signale im Detail ------------------ - -Ein Signal bewirkt, dass die Seite genau wie beim ursprünglichen Request neu geladen wird (außer beim Aufruf über AJAX), und ruft die Methode `signalReceived($signal)` auf, deren Standardimplementierung in der Klasse `Nette\Application\UI\Component` versucht, eine aus den Wörtern `handle` zusammengesetzte Methode aufzurufen. Die weitere Verarbeitung liegt beim jeweiligen Objekt. Objekte, die von `Component` erben (also `Control` und `Presenter`), reagieren so, dass sie versuchen, die Methode `handle` mit den passenden Parametern aufzurufen. - -Mit anderen Worten: Es wird die Definition der Funktion `handle` genommen, samt allen Parametern, die mit dem Request kamen, und die Parameter aus der URL werden den Argumenten anhand des Namens zugeordnet, dann wird versucht, die Methode aufzurufen. Zum Beispiel wird der Wert des Parameters `id` aus der URL als Argument `$id` übergeben, `something` aus der URL als `$something` usw. Und existiert die Methode nicht, wirft die Methode `signalReceived` eine [Exception |api:Nette\Application\UI\BadSignalException]. - -Neben den Parametern aus der URL liest ein Signal auch die im **POST-Body des Requests** gesendeten Parameter. Das ist praktisch, weil Signale oft über JavaScript aufgerufen werden, wo es natürlich ist, Daten mit der Methode POST zu senden. Kommt ein Parameter gleichen Namens jedoch sowohl aus der URL als auch aus dem POST-Body, hat der Wert **aus der URL Vorrang**. Vermeiden Sie es daher, einem POST-Feld denselben Namen wie einem URL- oder Route-Parameter zu geben, sonst würde der Wert aus der URL ihn stillschweigend überschreiben. Die Signalparameter teilen sich einen gemeinsamen Raum mit den Parametern der Aktion und den persistenten Parametern, siehe [Gemeinsamer Parameterraum |presenters#Gemeinsamer Parameterraum]. - -Ein Signal kann jede Komponente, jeder Presenter oder jedes Objekt empfangen, das das Interface `SignalReceiver` implementiert und mit dem Komponentenbaum verbunden ist. - -Die Hauptempfänger von Signalen werden `Presenter` und visuelle Komponenten sein, die von `Control` erben. Ein Signal soll einem Objekt als Zeichen dienen, dass es etwas tun soll - eine Umfrage soll eine Stimme des Benutzers zählen, ein Nachrichtenblock soll sich ausklappen und doppelt so viele Nachrichten anzeigen, ein Formular wurde abgeschickt und soll Daten verarbeiten und so weiter. - -Die URL für ein Signal wird mit der Methode [Component::link() |api:Nette\Application\UI\Component::link()] erstellt. Als Parameter `$destination` übergeben wir den String `{signal}!` und als `$args` ein Array von Argumenten, die wir dem Signal übergeben wollen. Das Signal wird immer auf dem aktuellen Presenter und der aktuellen Aktion mit den aktuellen Parametern aufgerufen; die Parameter des Signals kommen lediglich hinzu. Zusätzlich wird der **Parameter `?do`, der das Signal angibt**, ergänzt. - -Sein Format lautet entweder `{signal}` oder `{signalReceiver}-{signal}`. `{signalReceiver}` ist der Name der Komponente im Presenter. Deshalb darf im Namen der Komponente kein Bindestrich vorkommen - er dient der Trennung von Komponentenname und Signal, wobei sich auf diese Weise mehrere Komponenten verschachteln lassen. - -Die Methode [isSignalReceiver()|api:Nette\Application\UI\Presenter::isSignalReceiver()] prüft, ob die Komponente (erstes Argument) Empfänger des Signals (zweites Argument) ist. Das zweite Argument kann weggelassen werden - dann wird geprüft, ob die Komponente Empfänger irgendeines Signals ist. Wird der zweite Parameter auf `true` gesetzt, wird geprüft, ob die angegebene Komponente oder einer ihrer Nachfahren Empfänger ist. - -In jeder Phase vor `handle` können wir das Signal manuell ausführen, indem wir die Methode [processSignal()|api:Nette\Application\UI\Presenter::processSignal()] aufrufen, die sich um die Verarbeitung des Signals kümmert - sie nimmt die als Signalempfänger bestimmte Komponente (ist kein Empfänger angegeben, ist es der Presenter selbst) und sendet ihr das Signal. - -Beispiel: - -```php -if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) { - $this->processSignal(); -} -``` - -Dadurch wird das Signal vorzeitig ausgeführt und nicht erneut aufgerufen. diff --git a/application/de/configuration.texy b/application/de/configuration.texy deleted file mode 100644 index bc2f529a4c..0000000000 --- a/application/de/configuration.texy +++ /dev/null @@ -1,186 +0,0 @@ -Konfiguration der Anwendung -*************************** - -.[perex] -Übersicht der Konfigurationsoptionen für Nette Application. - - -Application -=========== - -```neon -application: - # das Panel "Nette Application" im Tracy BlueScreen anzeigen? - debugger: ... # (bool) aktiv, wenn Tracy verfügbar ist - - # in der Produktion werden Exceptions immer vom Error-Presenter behandelt; - # diese Option aktiviert dieses Verhalten zusätzlich im Entwicklungsmodus - catchExceptions: ... # (bool) Standard ist false - also aus im Dev-Modus, in der Produktion immer an - - # Name des Error-Presenters - errorPresenter: Error # (string|array) Standard ist 'Nette:Error' - - # definiert Aliase für Presenter und Aktionen - aliases: ... - - # definiert die Regeln für die Übersetzung des Presenter-Namens in eine Klasse - mapping: ... - - # Warnungen für ungültige Links unterdrücken? - # wirkt nur im Entwicklungsmodus - silentLinks: ... # (bool) Standard ist false -``` - -Seit `nette/application` Version 3.2 lässt sich ein Paar von Error-Presentern definieren: - -```neon -application: - errorPresenter: - 4xx: Error4xx # für Nette\Application\BadRequestException - 5xx: Error5xx # für übrige Exceptions -``` - -Die Aufteilung ist sinnvoll, weil sich beide Situationen grundlegend unterscheiden. Eine `BadRequestException` (Codes 4xx) bedeutet, dass mit der Anwendung alles in Ordnung ist und der Besucher lediglich etwas angefordert hat, das es nicht gibt. Sie können daher einen vollwertigen Presenter verwenden, der eine freundliche Meldung im Layout Ihrer Website anzeigt. Ein Fehler 5xx bedeutet dagegen, dass in der Anwendung etwas kaputtgegangen ist und Sie nicht wissen, was. Halten Sie den Presenter für 5xx deshalb so einfach wie möglich, damit bei seinem Rendering nichts weiter fehlschlagen kann - idealerweise sollte er weder auf die Datenbank noch auf das Layout oder den angemeldeten Benutzer zugreifen. - -Die Option `silentLinks` bestimmt, wie sich Nette im Entwicklungsmodus verhält, wenn die Erzeugung eines Links fehlschlägt (etwa weil der Presenter nicht existiert usw.). Der Standardwert `false` bedeutet, dass Nette einen Fehler `E_USER_WARNING` auslöst. Die Einstellung `true` unterdrückt diese Fehlermeldung. In der Produktionsumgebung wird `E_USER_WARNING` immer ausgelöst. Dieses Verhalten lässt sich auch über die Presenter-Variable [$invalidLinkMode |creating-links#Ungültige Links] beeinflussen. - -[Aliase vereinfachen das Referenzieren |creating-links#Aliase] häufig verwendeter Presenter. - -Das [Mapping definiert die Regeln |directory-structure#Presenter-Mapping], nach denen aus dem Presenter-Namen der Klassenname abgeleitet wird. - - -Automatische Registrierung von Presentern ------------------------------------------ - -Nette fügt Presenter automatisch als Services in den DI-Container ein, was ihre Erzeugung deutlich beschleunigt. Wie Nette die Presenter findet, lässt sich konfigurieren: - -```neon -application: - # Presenter in der Composer-Class-Map suchen? - scanComposer: ... # (bool) Standard ist true - - # Maske, der Klassen- und Dateiname entsprechen müssen - scanFilter: ... # (string) Standard ist '*Presenter' - - # in welchen Verzeichnissen nach Presentern suchen? - scanDirs: # (string[]|false) Standard ist '%appDir%' - - %vendorDir%/mymodule -``` - -Die in `scanDirs` angegebenen Verzeichnisse überschreiben den Standardwert `%appDir%` nicht, sondern ergänzen ihn; `scanDirs` enthält also beide Pfade `%appDir%` und `%vendorDir%/mymodule`. Wollen wir das Standardverzeichnis weglassen, verwenden wir ein [Ausrufezeichen |dependency-injection:configuration#Zusammenführen]: - -```neon -application: - scanDirs!: - - %vendorDir%/mymodule -``` - -Das Scannen der Verzeichnisse lässt sich mit dem Wert `false` abschalten. Presenter werden dann nicht mehr als Services registriert, lassen sich also nicht über den Abschnitt [decorator |dependency-injection:configuration#Decorator] anpassen, und ihre Erzeugung ist langsamer. Wir empfehlen daher nicht, die automatische Registrierung vollständig zu unterdrücken, da dies die Leistung der Anwendung senkt. - - -Latte-Templates -=============== - -Diese Einstellung beeinflusst global das Verhalten von Latte in Komponenten und Presentern. - -```neon -latte: - # das Latte-Panel in der Tracy Bar für das Haupttemplate (true) oder für alle Komponenten (all) anzeigen? - debugger: ... # (true|false|'all') aktiv, wenn Tracy verfügbar ist (nur im Debug-Modus) - - # Templates mit dem Header declare(strict_types=1) erzeugen - strictTypes: ... # (bool) Standard ist false - - # [strikten Parser-Modus |latte:develop#strict mode] aktivieren - strictParsing: ... # (bool) Standard ist false - - # beschränkt die Gültigkeit von Variablen auf den Schleifenkörper - scopedLoopVariables: ... # (bool) Standard ist false - - # entfernt die durch Verschachtelung in Paar-Tags entstandene Einrückung - dedent: ... # (bool) Standard ist false - - # [Prüfung des generierten Codes |latte:develop#Checking Generated Code] aktivieren - phpLinter: ... # (string) Standard ist null - - # setzt das Locale - locale: de_DE # (string) Standard ist null - - # Klasse des Objekts $this->template - templateClass: App\MyTemplateClass # Standard ist Nette\Bridges\ApplicationLatte\DefaultTemplate -``` - -Neue [Extensions |latte:extending-latte#Latte Extension] fügen Sie so hinzu: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - - -Routing -======= - -Grundeinstellungen: - -```neon -routing: - # das Routing-Panel in der Tracy Bar anzeigen? - debugger: ... # (bool) aktiv, wenn Tracy verfügbar ist (nur im Debug-Modus) - - # den Router in den DI-Container serialisieren - cache: ... # (bool) Standard ist false -``` - -Das Routing definieren wir üblicherweise in der Klasse [RouterFactory |routing#Routensammlung]. Alternativ lassen sich Routes auch in der Konfiguration über Paare `Maske: Aktion` definieren, diese Methode bietet jedoch nicht so viel Flexibilität: - -```neon -routing: - routes: - 'detail/': Admin:Home:default - '/': Front:Home:default -``` - - -Konstanten -========== - -Erzeugung von PHP-Konstanten. - -```neon -constants: - Foobar: 'baz' -``` - -Die Konstante `Foobar` wird nach dem Start der Anwendung erzeugt. - -.[note] -Konstanten sollten nicht als global verfügbare Variablen dienen. Verwenden Sie zur Übergabe von Werten an Objekte [Dependency Injection |dependency-injection:passing-dependencies]. - - -PHP -=== - -Einstellung von PHP-Direktiven. Eine Übersicht aller Direktiven finden Sie auf [php.net |https://www.php.net/manual/en/ini.list.php]. - -```neon -php: - date.timezone: Europe/Prague -``` - - -DI-Services -=========== - -Diese Services werden in den DI-Container eingefügt: - -| Name | Typ | Beschreibung -|----------------------------|---------------------------------------------------|----------------------------------------- -| `application.application` | [api:Nette\Application\Application] | der [Anwendungsstarter |how-it-works#Nette Application] -| `application.linkGenerator` | [api:Nette\Application\LinkGenerator] | [LinkGenerator |creating-links#LinkGenerator] -| `application.presenterFactory` | [api:Nette\Application\IPresenterFactory] | Presenter-Factory -| `application.###` | [api:Nette\Application\UI\Presenter] | einzelne Presenter -| `routing.router` | [api:Nette\Routing\Router] | Router -| `latte.latteFactory` | [api:Nette\Bridges\ApplicationLatte\LatteFactory] | Factory für das Objekt `Latte\Engine` -| `latte.templateFactory` | [api:Nette\Application\UI\TemplateFactory] | Factory für [`$this->template` |templates] diff --git a/application/de/creating-links.texy b/application/de/creating-links.texy deleted file mode 100644 index efb5faf9c9..0000000000 --- a/application/de/creating-links.texy +++ /dev/null @@ -1,319 +0,0 @@ -Erstellen von URL-Links -*********************** - -
    - -Links in Nette zu erstellen ist so einfach wie mit dem Finger zu zeigen. Sie müssen nur zielen, und das Framework erledigt die ganze Arbeit für Sie. Wir zeigen: - -- wie man Links in Templates und anderswo erstellt -- wie man einen Link auf die aktuelle Seite erkennt -- was man mit ungültigen Links macht - -
    - - -Dank des [bidirektionalen Routings |routing] müssen Sie URLs Ihrer Anwendung nie fest in Templates oder Code schreiben - URLs, die sich später ändern könnten oder kompliziert zusammenzusetzen wären. Im Link geben Sie einfach den Presenter und die Aktion an, übergeben eventuelle Parameter, und das Framework erzeugt die URL selbst. Es ist eigentlich sehr ähnlich wie ein Funktionsaufruf. Das wird Ihnen gefallen. - - -Im Template des Presenters -========================== - -Am häufigsten erstellen wir Links in Templates, und ein großartiger Helfer ist dabei das Attribut `n:href`: - -```latte -Detail -``` - -Beachten Sie, dass wir statt des HTML-Attributs `href` das [n:Attribut |latte:syntax#n:Attribute] `n:href` verwendet haben. Sein Wert ist keine URL, wie es beim Attribut `href` der Fall wäre, sondern der Name des Presenters und der Aktion. - -Auf einen Link zu klicken ist, vereinfacht gesagt, so etwas wie der Aufruf der Methode `ProductPresenter::renderShow()`. Und wenn diese in ihrer Signatur Parameter hat, können wir sie mit Argumenten aufrufen: - -```latte -Produktdetail -``` - -Es lassen sich auch benannte Parameter übergeben. Der folgende Link übergibt den Parameter `lang` mit dem Wert `en`: - -```latte -Produktdetail -``` - -Hat die Methode `ProductPresenter::renderShow()` in ihrer Signatur kein `$lang`, kann sie den Wert des Parameters mit `$lang = $this->getParameter('lang')` oder aus einer [Property |presenters#Parameter des Requests] holen. - -Sind die Parameter in einem Array gespeichert, lassen sie sich mit dem Operator `...` entpacken: - -```latte -{var $args = [$product->id, lang => en]} -Produktdetail -``` - -In Links werden automatisch auch die sogenannten [persistenten Parameter |presenters#Persistente Parameter] übergeben. - -Das Attribut `n:href` ist für HTML-Tags `` sehr praktisch. Wollen wir den Link anderswo ausgeben, zum Beispiel im Text, verwenden wir `{link}`: - -```latte -Die URL lautet: {link Home:default} -``` - - -Im Code -======= - -Zum Erstellen eines Links im Presenter dient die Methode `link()`: - -```php -$url = $this->link('Product:show', $product->id); -``` - -Die Parameter lassen sich auch als Array übergeben, in dem sich ebenfalls benannte Parameter angeben lassen: - -```php -$url = $this->link('Product:show', [$product->id, 'lang' => 'en']); -``` - -Links lassen sich auch ohne Presenter erstellen, und zwar mit dem [#LinkGenerator] und seiner Methode `link()`. - -Manchmal brauchen Sie einen Link jetzt, wollen die eigentliche URL aber erst später erzeugen. Dafür gibt es die Methode `lazyLink()`, die ein Objekt `Nette\Application\UI\Link` zurückgibt. Der Vorteil ist, dass Sie dieses Objekt weiterreichen können, zum Beispiel in ein Template, und seine Parameter vor dem Rendern noch mit der Methode `setParameter()` anpassen können. Die URL selbst wird erst zusammengesetzt, wenn das Objekt in einen String umgewandelt wird: - -```php -$link = $this->lazyLink('Product:show', $id); -// ... -echo $link; // erst hier wird die URL erzeugt -``` - - -Links auf Presenter -=================== - -Ist das Ziel des Links ein Presenter und eine Aktion, hat er diese Syntax: - -``` -[//] [[[[:]module:]presenter:]action | this] [#fragment] -``` - -Dieses Format unterstützen alle Latte-Tags und alle Methoden des Presenters, die mit Links arbeiten, also `n:href`, `{link}`, `{plink}`, `link()`, `lazyLink()`, `isLinkCurrent()`, `redirect()`, `redirectPermanent()`, `forward()`, `canonicalize()` und auch der [#LinkGenerator]. Auch wenn in den Beispielen `n:href` verwendet wird, könnte dort also jede dieser Funktionen stehen. - -Die Grundform ist demnach `Presenter:action`: - -```latte -Startseite -``` - -Verlinken wir auf eine Aktion des aktuellen Presenters, können wir dessen Namen weglassen: - -```latte -Startseite -``` - -Ist die Zielaktion `default`, können wir sie weglassen, der Doppelpunkt muss aber bleiben: - -```latte -Startseite -``` - -Links können auch auf andere [Module |directory-structure#Presenter und Templates] zeigen. Dabei unterscheidet man Links, die relativ zu einem verschachtelten Untermodul sind, und absolute Links. Das Prinzip ist analog zu Pfaden auf der Festplatte, nur werden statt Schrägstrichen Doppelpunkte verwendet. Nehmen wir an, der aktuelle Presenter gehört zum Modul `Front`, dann schreiben wir: - -```latte -Link auf Front:Shop:Product:show -Link auf Admin:Product:show -``` - -Ein Sonderfall ist ein Link [auf sich selbst |#Link auf die aktuelle Seite], bei dem wir als Ziel `this` angeben. - -```latte -aktualisieren -``` - -Auf einen bestimmten Teil der Seite können wir über ein sogenanntes Fragment nach dem Rautezeichen `#` verlinken: - -```latte -Link auf Home:default und das Fragment #main -``` - -.{data-version:3.3.0} -Das Fragment lässt sich auch dynamisch als Argument mit dem Schlüssel `#` setzen. Sein Wert wird automatisch kodiert und hat Vorrang vor dem im Ziel angegebenen Fragment: - -```php -$this->link('Home:default', ['#' => $fragment]); -``` - - -Absolute Pfade -============== - -Mit `link()` oder `n:href` erzeugte Links sind immer absolute Pfade (sie beginnen also mit `/`), aber keine absoluten URLs mit Protokoll und Domain wie `https://domain`. - -Um eine absolute URL zu erzeugen, ergänzen Sie am Anfang zwei Schrägstriche (z. B. `n:href="//Home:"`). Alternativ können Sie den Presenter mit `$this->absoluteUrls = true` so umschalten, dass er nur absolute Links erzeugt. - -Im Template lässt sich außerdem der Filter `|absoluteUrl` verwenden, der einen relativen Pfad in einen absoluten umwandelt. - - -Link auf die aktuelle Seite -=========================== - -Das Ziel `this` erzeugt einen Link auf die aktuelle Seite: - -```latte -aktualisieren -``` - -Dabei werden zugleich alle Parameter übernommen, die in der Signatur der Methode `action()` oder `render()` angegeben sind (sofern `action()` nicht definiert ist). Sind wir also auf der Seite `Product:show` mit `id: 123`, übergibt der Link auf `this` diesen Parameter ebenfalls. - -Natürlich lassen sich Parameter auch direkt angeben: - -```latte -aktualisieren -``` - -Die Funktion `isLinkCurrent()` prüft, ob das Ziel des Links mit der aktuellen Seite identisch ist. Das lässt sich zum Beispiel im Template nutzen, um Links optisch zu unterscheiden usw. - -Die Parameter sind dieselben wie bei der Methode `link()`, zusätzlich lässt sich anstelle einer konkreten Aktion die Wildcard `*` verwenden, was jede beliebige Aktion des angegebenen Presenters bedeutet. - -```latte -{if !isLinkCurrent('Admin:login')} - Anmelden -{/if} - -
  • - ... -
  • -``` - -In Kombination mit `n:href` im selben Element lässt sich eine Kurzform verwenden: - -```latte -... -``` - -Die Wildcard `*` lässt sich nur anstelle der Aktion verwenden, nicht anstelle des Presenters. - -Um festzustellen, ob wir uns in einem bestimmten Modul oder dessen Untermodul befinden, dient die Methode `isModuleCurrent(moduleName)`. - -```latte -
  • - ... -
  • -``` - - -Link-Basis ändern .{data-version:3.2.7} -======================================= - -Standardmäßig werden relative Links vom aktuellen Presenter abgeleitet. Das lässt sich mit `{linkBase}` ändern: - -```latte -{linkBase Admin:Dashboard} -Produktdetail -``` - -Der Link führt dann auf `Admin:Dashboard:Product:show`. Betroffen sind nur relative Links - absolute Links, die mit einem Doppelpunkt beginnen, und Links auf den aktuellen Presenter (`this`, `show`) bleiben unverändert. - -`{linkBase}` gilt für das gesamte Template und ist besonders in Layout-Templates nützlich, wo es unabhängig vom aufrufenden Presenter für einheitliche Links sorgt. -Der Tag muss am Anfang des Templates stehen, sonst wirft er eine `CompileException`. - - -Links auf Signale -================= - -Das Ziel eines Links muss nicht nur ein Presenter und eine Aktion sein, sondern kann auch ein [Signal |components#Signal] sein (es ruft die Methode `handle()` auf). Dann sieht die Syntax so aus: - -``` -[//] [sub-component:]signal! [#fragment] -``` - -Das Signal wird also durch ein Ausrufezeichen unterschieden: - -```latte -Signal -``` - -Sie können auch einen Link auf ein Signal einer Unterkomponente (oder Unter-Unterkomponente) erstellen: - -```latte -Signal -``` - - -Links in einer Komponente -========================= - -Da [Komponenten|components] eigenständige, wiederverwendbare Einheiten sind, die keinerlei Bindung an umgebende Presenter haben sollten, funktionieren Links hier etwas anders. Das Latte-Attribut `n:href` und der Tag `{link}` sowie Methoden der Komponente wie `link()` und weitere **betrachten das Ziel des Links immer als Namen eines Signals**. Deshalb muss nicht einmal ein Ausrufezeichen angegeben werden: - -```latte -Signal, keine Aktion -``` - -Wollen wir im Template der Komponente auf Presenter verlinken, verwenden wir den Tag `{plink}`: - -```latte -Startseite -``` - -oder im Code - -```php -$this->getPresenter()->link('Home:default') -``` - - -Aliase .{data-version:3.2.3} -============================ - -Manchmal ist es praktisch, einem Paar Presenter:action einen leicht merkbaren Alias zuzuweisen. Zum Beispiel die Startseite `Front:Home:default` einfach `home` zu nennen oder `Admin:Dashboard:default` als `admin`. - -Aliase 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 schreibt man sie dann mit einem At-Zeichen, zum Beispiel: - -```latte -Administration -``` - -Unterstützt werden sie auch in allen Methoden, die mit Links arbeiten, etwa `redirect()` und ähnlichen. - - -Ungültige Links -=============== - -Es kann passieren, dass wir einen ungültigen Link erzeugen - entweder weil er auf einen nicht existierenden Presenter führt, oder weil er mehr Parameter übergibt, als die Zielmethode in ihrer Signatur entgegennimmt, oder wenn für die Zielaktion keine URL erzeugt werden kann. Wie mit ungültigen Links umgegangen wird, legt man im Presenter über `$this->invalidLinkMode` fest. Es kann eine Kombination dieser Werte (Konstanten) annehmen: - -- `Presenter::InvalidLinkSilent` - stiller Modus, gibt als URL das Zeichen # zurück -- `Presenter::InvalidLinkWarning` - es wird eine Warnung E_USER_WARNING ausgelöst, die im Produktionsmodus protokolliert wird, die Ausführung des Skripts aber nicht unterbricht -- `Presenter::InvalidLinkTextual` - visuelle Warnung, gibt den Fehler direkt in den Link aus -- `Presenter::InvalidLinkException` - wirft eine InvalidLinkException - -Die Standardeinstellung ist `InvalidLinkWarning` im Produktionsmodus und `InvalidLinkWarning | InvalidLinkTextual` im Entwicklungsmodus. `InvalidLinkWarning` bewirkt in der Produktionsumgebung keinen Abbruch des Skripts, die Warnung wird jedoch protokolliert. In der Entwicklungsumgebung fängt [Tracy |tracy:] sie ab und zeigt einen Bluescreen. `InvalidLinkTextual` funktioniert so, dass es als URL eine Fehlermeldung zurückgibt, die mit den Zeichen `#error:` beginnt. Damit solche Links auf den ersten Blick auffallen, ergänzen Sie Ihr CSS um: - -```css -a[href^="#error:"] { - background: red; - color: white; -} -``` - -Wollen wir, dass in der Entwicklungsumgebung keine Warnungen entstehen, können wir sie direkt in der [Konfiguration|configuration] unterdrücken. - -```neon -application: - silentLinks: true -``` - - -LinkGenerator -============= - -Wie erstellt man Links mit ähnlichem Komfort wie mit der Methode `link()`, aber ohne die Anwesenheit eines Presenters? Dafür ist [api:Nette\Application\LinkGenerator] da. - -Der LinkGenerator ist ein Service, den Sie sich über den Konstruktor übergeben lassen können und mit dessen Methode `link()` Sie dann Links erstellen. - -Gegenüber Presentern gibt es einen Unterschied. Der LinkGenerator erzeugt alle Links direkt als absolute URLs. Außerdem gibt es keinen "aktuellen Presenter", daher lässt sich als Ziel weder nur der Name der Aktion `link('default')` angeben noch lassen sich relative Pfade zu Modulen verwenden. - -Ungültige Links werfen immer eine `Nette\Application\UI\InvalidLinkException`. diff --git a/application/de/directory-structure.texy b/application/de/directory-structure.texy deleted file mode 100644 index 450885cdd4..0000000000 --- a/application/de/directory-structure.texy +++ /dev/null @@ -1,526 +0,0 @@ -Verzeichnisstruktur der Anwendung -********************************* - -
    - -Wie entwirft man eine übersichtliche und skalierbare Verzeichnisstruktur für Projekte im Nette Framework? Wir zeigen Ihnen bewährte Praktiken, die Ihnen bei der Organisation Ihres Codes helfen. Sie erfahren: - -- wie man die Anwendung **logisch** in Verzeichnisse gliedert -- wie man die Struktur so entwirft, dass sie mit wachsendem Projekt **gut skaliert** -- welche **möglichen Alternativen** es gibt und welche Vor- und Nachteile sie haben - -
    - - -Wichtig ist zu erwähnen, dass das Nette Framework selbst keine bestimmte Struktur erzwingt. Es ist so gestaltet, dass es sich leicht an beliebige Bedürfnisse und Vorlieben anpassen lässt. - - -Grundlegende Projektstruktur -============================ - -Auch wenn das Nette Framework keine feste Verzeichnisstruktur vorschreibt, gibt es eine bewährte Standardaufteilung in Form des [Web Project|https://github.com/nette/web-project]: - -/--pre -web-project/ -├── app/ ← Verzeichnis der Anwendung -├── assets/ ← SCSS-, JS-Dateien, Bilder…, alternativ resources/ -├── bin/ ← Skripte für die Kommandozeile -├── config/ ← Konfiguration -├── log/ ← protokollierte Fehler -├── temp/ ← temporäre Dateien, Cache -├── tests/ ← Tests -├── vendor/ ← von Composer installierte Bibliotheken -└── www/ ← öffentliches Verzeichnis (Document-Root) -\-- - -Sie können diese Struktur nach Ihren Bedürfnissen frei anpassen - Ordner umbenennen oder verschieben. Danach müssen Sie lediglich die relativen Pfade zu den Verzeichnissen in `Bootstrap.php` und gegebenenfalls in `composer.json` anpassen. Mehr ist nicht nötig, keine komplizierte Neukonfiguration, keine Änderungen an Konstanten. Nette hat eine intelligente Autodetection und erkennt den Ort der Anwendung samt ihrer Basis-URL automatisch. - - -Prinzipien der Code-Organisation -================================ - -Wenn Sie ein neues Projekt zum ersten Mal erkunden, sollten Sie sich schnell zurechtfinden. 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 irgendwelche Services, Repositories und Entities verwendet. Über den eigentlichen Zweck der Anwendung erfahren Sie nichts. - -Schauen wir uns einen anderen Ansatz an - die **Organisation nach Domänen**: - -/--pre -app/Model/ -├── Cart/ -├── Payment/ -├── Order/ -└── Product/ -\-- - -Hier ist es anders - auf den ersten Blick ist klar, dass es sich um einen E-Shop handelt. Schon die Verzeichnisnamen 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 zusammengehöriger Code ist über verschiedene Ordner verstreut, und Sie müssen zwischen ihnen hin- und herspringen. Deshalb organisieren wir nach Domänen. - - -Namespaces ----------- - -Es ist üblich, dass die Verzeichnisstruktur den Namespaces in der Anwendung entspricht. Das bedeutet, dass der physische Ort der Dateien ihrem Namespace entspricht. Eine Klasse in `app/Model/Product/ProductRepository.php` sollte zum Beispiel den Namespace `App\Model\Product` haben. Dieses Prinzip hilft beim Navigieren im Code und vereinfacht das Autoloading. - - -Singular vs. Plural in Namen ----------------------------- - -Beachten Sie, dass wir für die Hauptverzeichnisse der Anwendung den Singular verwenden: `app`, `config`, `log`, `temp`, `www`. Dasselbe gilt innerhalb der Anwendung: `Model`, `Core`, `Presentation`. Der Grund ist, dass jedes davon ein einziges zusammenhängendes Konzept darstellt. - -Ebenso stellt `app/Model/Product` alles dar, was mit Produkten zu tun hat. Wir nennen es nicht `Products`, weil es kein Ordner voller Produkte ist (der würde Dateien wie `nokia.php`, `samsung.php` enthalten). Es ist ein Namespace mit Klassen für die Arbeit mit Produkten - `ProductRepository.php`, `ProductService.php`. - -Der Ordner `app/Tasks` steht im Plural, weil er eine Menge eigenständiger ausführbarer Skripte enthält - `CleanupTask.php`, `ImportTask.php`. Jedes davon ist eine unabhängige Einheit. - -Aus Gründen der Konsistenz empfehlen wir: -- Singular für Namespaces, die eine funktionale Einheit darstellen (auch wenn sie mit mehreren Entities arbeiten) -- Plural für Sammlungen unabhängiger Einheiten -- Im Zweifel, oder wenn Sie nicht darüber nachdenken wollen, wählen Sie den Singular - - -Öffentliches Verzeichnis `www/` -=============================== - -Dieses Verzeichnis ist das einzige, das aus dem Web erreichbar ist (der Document-Root). Häufig begegnet Ihnen statt `www/` der Name `public/` - das ist reine Konventionssache und hat keinen Einfluss auf die Funktion der Anwendung. Das Verzeichnis enthält: -- den [Einstiegspunkt |bootstrapping#index.php] der Anwendung `index.php` -- die Datei `.htaccess` mit den Regeln für mod_rewrite (für Apache) -- statische Dateien (CSS, JavaScript, Bilder) -- hochgeladene Dateien - -Für die richtige Absicherung der Anwendung ist es entscheidend, den [Document-Root korrekt zu konfigurieren |nette:troubleshooting#Wie ändert oder entfernt man das Verzeichnis www aus der URL?]. - -.[note] -Legen Sie den Ordner `node_modules/` niemals in dieses Verzeichnis - er enthält Tausende von Dateien, die ausführbar sein können und nicht öffentlich zugänglich sein sollten. - - -Verzeichnis der Anwendung `app/` -================================ - -Das ist das Hauptverzeichnis mit dem Code der Anwendung. Grundstruktur: - -/--pre -app/ -├── Core/ ← infrastrukturelle Belange -├── Model/ ← Geschäftslogik -├── Presentation/ ← Presenter und Templates -├── Tasks/ ← Kommandoskripte -└── Bootstrap.php ← Startklasse der Anwendung -\-- - -`Bootstrap.php` ist die [Startklasse der Anwendung|bootstrapping], die die Umgebung initialisiert, die Konfiguration lädt und den DI-Container erzeugt. - -Schauen wir uns nun die einzelnen Unterverzeichnisse genauer an. - - -Presenter und Templates -======================= - -Der Präsentationsteil der Anwendung liegt im Verzeichnis `app/Presentation`. Eine Alternative ist das kürzere `app/UI`. Das ist der Ort für alle Presenter, ihre Templates und eventuelle zugehörige Hilfsklassen. - -Diese Schicht organisieren wir nach Domänen. In einem komplexen Projekt, das E-Shop, Blog und API vereint, sähe die Struktur so aus: - -/--pre -app/Presentation/ -├── Shop/ ← Frontend des E-Shops -│ ├── Product/ -│ ├── Cart/ -│ └── Order/ -├── Blog/ ← Blog -│ ├── Home/ -│ └── Post/ -├── Admin/ ← Administration -│ ├── Dashboard/ -│ └── Products/ -└── Api/ ← API-Endpunkte - └── V1/ -\-- - -Für einen einfachen Blog würden wir dagegen folgende Struktur verwenden: - -/--pre -app/Presentation/ -├── Front/ ← Frontend der Website -│ ├── Home/ -│ └── Post/ -├── Admin/ ← Administration -│ ├── Dashboard/ -│ └── Posts/ -├── Error/ -└── Export/ ← RSS, Sitemaps usw. -\-- - -Ordner wie `Home/` oder `Dashboard/` enthalten Presenter und Templates. Ordner wie `Front/`, `Admin/` oder `Api/` nennt man **Module**. Technisch sind das gewöhnliche Verzeichnisse, die der logischen Aufteilung der Anwendung dienen. - -Jeder Ordner mit einem Presenter enthält die Presenter-Datei selbst und ihre Templates. Der Ordner `Dashboard/` enthält zum Beispiel: - -/--pre -Dashboard/ -├── DashboardPresenter.php ← Presenter -└── default.latte ← Template -\-- - -Diese Verzeichnisstruktur spiegelt sich in den Namespaces der Klassen wider. `DashboardPresenter` liegt zum Beispiel im Namespace `App\Presentation\Admin\Dashboard` (siehe [#Presenter-Mapping]): - -```php -namespace App\Presentation\Admin\Dashboard; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -Auf den Presenter `Dashboard` im Modul `Admin` verweisen wir in der Anwendung mit der Doppelpunkt-Schreibweise als `Admin:Dashboard`. Seine Aktion `default` wird dann als `Admin:Dashboard:default` bezeichnet. Bei verschachtelten Modulen verwenden wir mehrere Doppelpunkte, zum Beispiel `Shop:Order:Detail:default`. - - -Flexible Entwicklung der Struktur ---------------------------------- - -Einer der großen Vorteile dieser Struktur ist, wie elegant sie sich an wachsende Anforderungen des Projekts anpasst. Nehmen wir als Beispiel den Teil, der XML-Feeds erzeugt. Am Anfang haben wir eine einfache Form: - -/--pre -Export/ -├── ExportPresenter.php ← ein Presenter für alle Exporte -├── sitemap.latte ← Template für die Sitemap -└── feed.latte ← Template für den RSS-Feed -\-- - -Mit der Zeit kommen weitere Feed-Typen hinzu, und wir brauchen mehr Logik dafür … 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 verläuft völlig reibungslos - legen Sie einfach neue Unterordner an, verteilen Sie den Code darauf und aktualisieren Sie die Links (z. B. von `Export:feed` auf `Export:Feed:amazon`). Dadurch können wir die Struktur nach Bedarf schrittweise erweitern, die Verschachtelungstiefe ist in keiner Weise begrenzt. - -Wenn Sie zum Beispiel in der Administration viele Presenter rund um die Verwaltung von Bestellungen haben, etwa `OrderDetail`, `OrderEdit`, `OrderDispatch` usw., können Sie zur besseren Organisation ein Modul (einen Ordner) namens `Order` anlegen, das (Ordner für) die Presenter `Detail`, `Edit`, `Dispatch` und weitere enthält. - - -Ort der Templates ------------------ - -In den vorherigen Beispielen haben wir gesehen, dass die Templates direkt im Ordner mit dem Presenter liegen: - -/--pre -Dashboard/ -├── DashboardPresenter.php ← Presenter -├── DashboardTemplate.php ← optionale Template-Klasse -└── default.latte ← Template -\-- - -Dieser Ort erweist sich in der Praxis als der bequemste - Sie haben alle zusammengehörigen Dateien griffbereit. - -Alternativ können Sie die Templates in einen Unterordner `templates/` legen. Nette unterstützt beide Varianten. Sie können die Templates sogar vollständig außerhalb des Ordners `Presentation/` ablegen. Alles über die Möglichkeiten des Template-Orts finden Sie im Kapitel [Suche nach Templates |templates#Suche nach Templates]. - - -Hilfsklassen und Komponenten ----------------------------- - -Zu Presentern und Templates kommen oft weitere Hilfsdateien. Wir platzieren sie logisch nach ihrem Geltungsbereich: - -1. **Direkt beim Presenter** im Fall von Komponenten, die für diesen Presenter spezifisch sind: - -/--pre -Product/ -├── ProductPresenter.php -├── ProductGrid.php ← Komponente für die Produktauflistung -└── FilterForm.php ← Formular zum Filtern -\-- - -2. **Für das Modul** - wir empfehlen den Ordner `Accessory`, der alphabetisch praktischerweise ganz am Anfang steht: - -/--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/ -\-- - -Alternativ können Sie Hilfsklassen wie `LatteExtension.php` oder `TemplateFilters.php` in den Infrastrukturordner `app/Core/Latte/` legen. Und Komponenten nach `app/Components`. Die Wahl hängt von den Konventionen im Team ab. - - -Model - Herz der Anwendung -========================== - -Das Model enthält die gesamte Geschäftslogik der Anwendung. Die Regel für seine Organisation lautet wieder: Struktur nach Domänen: - -/--pre -app/Model/ -├── Payment/ ← alles rund um Zahlungen -│ ├── PaymentFacade.php ← Haupteinstiegspunkt -│ ├── PaymentRepository.php -│ ├── Payment.php ← Entity -├── Order/ ← alles rund um Bestellungen -│ ├── OrderFacade.php -│ ├── OrderRepository.php -│ ├── Order.php -└── Shipping/ ← alles rund um den Versand -\-- - -Im Model begegnen Ihnen typischerweise diese Arten von Klassen: - -**Facades**: Sie stellen den Haupteinstiegspunkt in eine bestimmte Domäne der Anwendung dar. Sie treten als Orchestrator auf und koordinieren die Zusammenarbeit verschiedener Services, um vollständige Use-Cases umzusetzen (etwa "Bestellung anlegen" oder "Zahlung verarbeiten"). Unterhalb ihrer Orchestrierungsschicht verbirgt die Facade Implementierungsdetails vor dem Rest der Anwendung und bietet damit eine saubere Schnittstelle für die Arbeit mit der jeweiligen Domäne. - -```php -class OrderFacade -{ - public function createOrder(Cart $cart): Order - { - // Validierung - // Anlegen der Bestellung - // Versand der E-Mail - // Schreiben in die Statistik - } -} -``` - -**Services**: Sie konzentrieren sich auf konkrete Geschäftsoperationen innerhalb einer Domäne. Anders als Facades, die ganze Use-Cases orchestrieren, implementiert ein Service konkrete Geschäftslogik (etwa Preisberechnungen oder die Verarbeitung von Zahlungen). Services sind typischerweise zustandslos und lassen sich entweder von Facades als Bausteine für komplexere Operationen oder für einfachere Aufgaben direkt von anderen Teilen der Anwendung verwenden. - -```php -class PricingService -{ - public function calculateTotal(Order $order): Money - { - // Preisberechnung - } -} -``` - -**Repositories**: Sie kümmern sich um die gesamte Kommunikation mit dem Datenspeicher, typischerweise einer Datenbank. Ihre Aufgabe ist es, Entities zu laden und zu speichern und Methoden zu ihrer Suche zu implementieren. Ein Repository schirmt den Rest der Anwendung von den Implementierungsdetails der Datenbank 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 - { - } -} -``` - -**Entities**: Objekte, die die wichtigsten Geschäftskonzepte der Anwendung darstellen, eine eigene Identität haben und sich im Laufe der Zeit ändern. Typischerweise sind das Klassen, die per ORM (etwa Nette Database Explorer oder Doctrine) auf Datenbanktabellen abgebildet werden. Entities können Geschäftsregeln zu ihren Daten und Validierungslogik enthalten. - -```php -// Entity, die auf die Datenbanktabelle 'orders' abgebildet 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, - ]); - } -} -``` - -**Value Objects**: unveränderliche Objekte, die Werte ohne eigene Identität darstellen - zum Beispiel einen Geldbetrag oder eine E-Mail-Adresse. Zwei Instanzen eines Value Objects mit denselben Werten gelten als identisch. - - -Infrastrukturcode -================= - -Der Ordner `Core/` (alternativ `Infrastructure/`) ist die Heimat der technischen Grundlage der Anwendung. Zum Infrastrukturcode gehören typischerweise: - -/--pre -app/Core/ -├── Router/ ← Routing und URL-Verwaltung -│ └── RouterFactory.php -├── Security/ ← Authentifizierung und Autorisierung -│ ├── Authenticator.php -│ └── Authorizator.php -├── Logging/ ← Logging und Monitoring -│ ├── SentryLogger.php -│ └── FileLogger.php -├── Cache/ ← Caching-Schicht -│ └── FullPageCache.php -└── Integration/ ← Integration externer Dienste - ├── Slack/ - └── Stripe/ -\-- - -Für kleinere Projekte genügt natürlich eine flache Struktur: - -/--pre -Core/ -├── RouterFactory.php -├── Authenticator.php -└── QueueMailer.php -\-- - -Das ist Code, der: - -- sich um die technische Infrastruktur kümmert (Routing, Logging, Caching) -- externe Dienste integriert (Sentry, Elasticsearch, Redis) -- grundlegende Services für die gesamte Anwendung bereitstellt (Mail, Datenbank) -- meist unabhängig von einer konkreten Domäne ist - Cache oder Logger funktionieren für einen E-Shop genauso wie für einen Blog. - -Sie fragen sich, ob eine bestimmte Klasse hierher oder ins Model gehört? Der entscheidende Unterschied ist, dass Code in `Core/`: - -- nichts über die Domäne weiß (Produkte, Bestellungen, Artikel) -- sich meist in ein anderes Projekt übertragen lässt -- löst "wie es funktioniert" (wie man eine E-Mail versendet), nicht "was es tut" (welche E-Mail zu versenden ist) - -Ein Beispiel zum besseren Verständnis: - -- `App\Core\MailerFactory` - erzeugt Instanzen der Klasse zum Versenden von E-Mails, kümmert sich um die SMTP-Einstellungen -- `App\Model\OrderMailer` - nutzt `MailerFactory`, um E-Mails zu Bestellungen zu versenden, kennt ihre Templates und weiß, wann sie versendet werden sollen - - -Kommandoskripte -=============== - -Anwendungen müssen oft Tätigkeiten außerhalb der gewöhnlichen HTTP-Requests ausführen - sei es Datenverarbeitung im Hintergrund, Wartung oder periodische Aufgaben. Zur Ausführung dienen einfache Skripte im Verzeichnis `bin/`, während die eigentliche Implementierungslogik in `app/Tasks/` (oder `app/Commands/`) liegt. - -Beispiel: - -/--pre -app/Tasks/ -├── Maintenance/ ← Wartungsskripte -│ ├── CleanupCommand.php ← Löschen alter Daten -│ └── DbOptimizeCommand.php ← Optimierung der Datenbank -├── Integration/ ← Integration externer Systeme -│ ├── ImportProducts.php ← Import aus dem System des Lieferanten -│ └── SyncOrders.php ← Synchronisation der Bestellungen -└── Scheduled/ ← regelmäßige Aufgaben - ├── NewsletterCommand.php ← Versand von Newslettern - └── ReminderCommand.php ← Benachrichtigungen an Kunden -\-- - -Was gehört ins Model und was in Kommandoskripte? Die Logik zum Versenden einer einzelnen E-Mail ist zum Beispiel Teil des Models, während der Massenversand Tausender E-Mails in `Tasks/` gehört. - -Tasks werden üblicherweise von der Kommandozeile oder per Cron ausgeführt: Das Skript in `bin/` erzeugt mit der Methode [bootConsoleApplication() |bootstrapping#Verschiedene Umgebungen] den DI-Container und holt den benötigten Service aus ihm. Sie lassen sich auch über einen HTTP-Request ausführen, dabei muss aber an die Sicherheit gedacht werden. Der Presenter, der den Task ausführt, muss abgesichert werden, zum Beispiel nur für angemeldete Benutzer oder mit einem starken Token und Zugriff von erlaubten IP-Adressen. Bei lang laufenden Tasks muss das Zeitlimit des Skripts erhöht und `session_write_close()` verwendet werden, um die Session nicht zu blockieren. - - -Weitere mögliche Verzeichnisse -============================== - -Neben den genannten Grundverzeichnissen können Sie je nach Bedarf des Projekts weitere spezialisierte Ordner ergänzen. Schauen wir uns die häufigsten 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 der gesamten Anwendung -├── Event/ ← nützlich bei einer ereignisgesteuerten Architektur -├── Mail/ ← E-Mail-Templates und zugehörige Logik -└── Utils/ ← Hilfsklassen -\-- - -Für gemeinsame visuelle Komponenten, die in Presentern der gesamten Anwendung verwendet werden, können Sie den Ordner `app/Components` oder `app/Controls` nutzen: - -/--pre -app/Components/ -├── Form/ ← gemeinsame Formularkomponenten -│ ├── SignInForm.php -│ └── UserForm.php -├── Grid/ ← Komponenten für Datenauflistungen -│ └── DataGrid.php -└── Navigation/ ← Navigationselemente - ├── Breadcrumbs.php - └── Menu.php -\-- - -Hierher gehören Komponenten mit komplexerer Logik. Wollen Sie Komponenten zwischen mehreren Projekten teilen, empfiehlt es sich, sie in ein eigenes Composer-Paket auszulagern. - -Im Verzeichnis `app/Mail` können Sie die Verwaltung der E-Mail-Kommunikation unterbringen: - -/--pre -app/Mail/ -├── templates/ ← E-Mail-Templates -│ ├── order-confirmation.latte -│ └── welcome.latte -└── OrderMailer.php -\-- - - -Presenter-Mapping -================= - -Das Mapping definiert die Regeln, nach denen aus dem Namen des Presenters der Klassenname abgeleitet wird. 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. Seit Nette Application 3.3 ist das die Standardkonvention, die nicht konfiguriert werden muss. Wenn Sie eine andere Struktur verwenden oder das Mapping ausdrücklich angeben wollen, entspricht die Standardeinstellung dieser Zeile: - -```neon -application: - mapping: App\Presentation\*\**Presenter -``` - -Wie funktioniert das Mapping? Zum besseren Verständnis stellen wir uns zunächst eine Anwendung ohne Module vor. Wir wollen, dass die Presenter-Klassen unter den Namespace `App\Presentation` fallen, sodass der Presenter `Home` auf die Klasse `App\Presentation\HomePresenter` abgebildet wird. Das erreichen wir mit dieser Konfiguration: - -```neon -application: - mapping: App\Presentation\*Presenter -``` - -Das Mapping funktioniert so, dass der Stern in der Maske `App\Presentation\*Presenter` durch den Namen des Presenters `Home` ersetzt wird, woraus der endgültige Klassenname `App\Presentation\HomePresenter` entsteht. Einfach! - -Wie Sie jedoch in den Beispielen in diesem und anderen Kapiteln sehen, legen wir die Presenter-Klassen in gleichnamige Unterverzeichnisse, der Presenter `Home` wird also auf die Klasse `App\Presentation\Home\HomePresenter` abgebildet. Das erreichen wir mit dem doppelten Stern `**` (erfordert Nette Application 3.2.3): - -```neon -application: - mapping: App\Presentation\**Presenter -``` - -Nun gehen wir zum Mapping von Presentern in Module über. Für jedes Modul können wir ein eigenes Mapping definieren: - -```neon -application: - mapping: - Front: App\Presentation\Front\**Presenter - Admin: App\Presentation\Admin\**Presenter - Api: App\Api\*Presenter -``` - -Nach dieser Konfiguration wird der Presenter `Front:Home` auf die Klasse `App\Presentation\Front\Home\HomePresenter` abgebildet, während der Presenter `Api:OAuth` auf die Klasse `App\Api\OAuthPresenter` abgebildet wird. - -Da die Module `Front` und `Admin` ein ähnliches Mapping-Muster haben und es davon vermutlich noch mehr geben wird, lässt sich eine allgemeine Regel erstellen, die sie ersetzt. In die Klassenmaske kommt ein neuer Stern für das Modul hinzu: - -```neon -application: - mapping: - *: App\Presentation\*\**Presenter - Api: App\Api\*Presenter -``` - -Es funktioniert auch für tiefer verschachtelte Verzeichnisstrukturen, etwa den Presenter `Admin:User:Edit`, bei dem sich das Segment mit dem Stern für jede Modulebene wiederholt, woraus die Klasse `App\Presentation\Admin\User\Edit\EditPresenter` entsteht. - -Eine alternative Schreibweise ist, statt eines Strings ein Array aus drei Segmenten zu verwenden. Für die oben gezeigten Beispiele ist diese Schreibweise gleichwertig 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 deleted file mode 100644 index 12eca8f7b7..0000000000 --- a/application/de/how-it-works.texy +++ /dev/null @@ -1,200 +0,0 @@ -Wie funktionieren Anwendungen? -****************************** - -
    - -Sie lesen gerade das grundlegende Kapitel der Nette-Dokumentation. Sie erfahren das komplette Prinzip, wie Webanwendungen funktionieren - von A bis Z, vom Moment der Entstehung eines Requests bis zum Ende der Ausführung des PHP-Skripts. Nach der Lektüre werden Sie wissen: - -- wie das Ganze funktioniert -- was Bootstrap, Presenter und der DI-Container sind -- wie die Verzeichnisstruktur aussieht - -
    - - -Verzeichnisstruktur -=================== - -Öffnen Sie das Beispielskelett einer Webanwendung namens [WebProject|https://github.com/nette/web-project]. Beim Lesen können Sie in die besprochenen Dateien hineinschauen. - -Die Verzeichnisstruktur sieht ungefähr so aus: - -/--pre -web-project/ -├── app/ ← Verzeichnis der Anwendung -│ ├── Core/ ← für den Betrieb notwendige Basisklassen -│ │ └── RouterFactory.php ← Konfiguration der URL-Adressen -│ ├── Presentation/ ← Presenter, Templates & Co. -│ │ ├── @layout.latte ← Template des Layouts -│ │ └── Home/ ← Verzeichnis des Home-Presenters -│ │ ├── HomePresenter.php ← Klasse des Home-Presenters -│ │ └── default.latte ← Template für die Aktion default -│ └── Bootstrap.php ← Startklasse Bootstrap -├── assets/ ← Ressourcen (SCSS, TypeScript, Quellbilder) -├── bin/ ← von der Kommandozeile ausgeführte Skripte -├── config/ ← Konfigurationsdateien -│ ├── common.neon -│ └── services.neon -├── log/ ← protokollierte Fehler -├── temp/ ← temporäre Dateien, Cache, … -├── vendor/ ← von Composer installierte Bibliotheken -│ ├── ... -│ └── autoload.php ← Autoloading aller installierten Pakete -├── www/ ← öffentliches Verzeichnis, Document-Root des Projekts -│ ├── assets/ ← kompilierte statische Dateien (CSS, JS, Bilder, ...) -│ ├── .htaccess ← Regeln für mod_rewrite -│ └── index.php ← Startdatei, die die Anwendung startet -└── .htaccess ← verbietet den Zugriff auf alle Verzeichnisse außer www -\-- - -Sie können die Verzeichnisstruktur beliebig ändern, Ordner umbenennen oder verschieben; sie ist völlig flexibel. Nette verfügt außerdem über eine intelligente Autodetection und erkennt den Ort der Anwendung samt ihrer URL-Basis automatisch. - -Bei etwas größeren Anwendungen können wir die Ordner mit Presentern und Templates in [Unterverzeichnisse |directory-structure#Presenter und Templates] gliedern und die Klassen in Namespaces zusammenfassen, die wir Module nennen. - -Das Verzeichnis `www/` stellt das öffentliche Verzeichnis bzw. den Document-Root des Projekts dar. Sie können es umbenennen, ohne auf der Seite der Anwendung sonst etwas konfigurieren zu müssen. Sie müssen lediglich [das Hosting so einstellen |nette:troubleshooting#Wie ändert oder entfernt man das Verzeichnis www aus der URL?], dass der Document-Root auf dieses Verzeichnis zeigt. - -WebProject können Sie samt Nette auch direkt mit [Composer |best-practices:composer] herunterladen: - -```shell -composer create-project nette/web-project -``` - -Setzen Sie unter Linux oder macOS für die Verzeichnisse `log/` und `temp/` [Schreibrechte |nette:troubleshooting#Einstellung der Verzeichnisberechtigungen]. - -Die Anwendung WebProject ist startbereit; es ist überhaupt nichts zu konfigurieren, und Sie können sie direkt im Browser betrachten, indem Sie den Ordner `www/` aufrufen. - - -HTTP-Request -============ - -Alles beginnt damit, dass ein Benutzer im Browser eine Seite öffnet. Der Browser sendet einen HTTP-Request an den Server. Dieser Request zielt auf eine einzige PHP-Datei im öffentlichen Verzeichnis `www/`, nämlich `index.php`. Nehmen wir an, der Request gilt der Adresse `https://example.com/product/123`. Dank einer passenden [Serverkonfiguration |nette:troubleshooting#Wie konfiguriert man den Server für schöne URLs?] wird auch diese URL auf die Datei `index.php` abgebildet, die dann ausgeführt wird. - -Ihre Aufgabe ist es: - -1) die Umgebung zu initialisieren -2) die Factory zu beschaffen -3) die Nette-Anwendung zu starten, die den Request bearbeitet - -Welche Factory? Wir stellen doch keine Traktoren her, wir bauen Websites! Nur Geduld, das wird gleich erklärt. - -Mit "Initialisierung der Umgebung" meinen wir zum Beispiel die Aktivierung von [Tracy|tracy:], einem großartigen Werkzeug zum Protokollieren und Visualisieren von Fehlern. Auf dem Produktionsserver protokolliert es Fehler, in der Entwicklungsumgebung zeigt es sie direkt an. Zur Initialisierung gehört daher auch die Entscheidung, ob die Website im Produktions- oder im Entwicklungsmodus läuft. Nette nutzt dafür eine [intelligente Autodetection |bootstrapping#Entwicklungs- vs. Produktionsmodus]: Wenn Sie die Website auf localhost betreiben, läuft sie im Entwicklungsmodus. Sie müssen nichts konfigurieren, und die Anwendung ist sofort sowohl für die Entwicklung als auch für den Live-Betrieb bereit. Diese Schritte werden im Kapitel über die [Klasse Bootstrap|bootstrapping] ausgeführt und ausführlich beschrieben. - -Der dritte Punkt (ja, den zweiten haben wir übersprungen, aber wir kommen darauf zurück) ist der Start der Anwendung. Die Bearbeitung von HTTP-Requests ist in Nette Aufgabe der Klasse `Nette\Application\Application` (im Folgenden `Application`). Wenn wir also sagen "die Anwendung starten", meinen wir konkret den Aufruf der treffend benannten Methode `run()` auf einem Objekt dieser Klasse. - -Nette ist ein Mentor, der Sie dazu anleitet, saubere Anwendungen nach bewährten Methodiken zu schreiben. Eine der etabliertesten davon heißt **Dependency Injection**, kurz DI. Wir wollen Sie an dieser Stelle nicht mit der Erklärung von DI belasten, dafür gibt es ein [eigenes Kapitel|dependency-injection:introduction]. Die wesentliche Konsequenz ist, dass Schlüsselobjekte üblicherweise von einer Objekt-Factory erzeugt werden, die man **DI-Container** (kurz DIC) nennt. Ja, das ist die Factory, von der eben die Rede war. Sie erzeugt für uns auch das Objekt `Application`, weshalb wir zuerst den Container brauchen. Wir beschaffen ihn mit der Klasse `Configurator`, lassen ihn das Objekt `Application` erzeugen, rufen darauf die Methode `run()` auf, und damit startet die Nette-Anwendung. Genau das passiert in der Datei [index.php |bootstrapping#index.php]. - - -Nette Application -================= - -Die Klasse `Application` hat eine einzige Aufgabe: auf den HTTP-Request zu antworten. - -In Nette geschriebene Anwendungen sind in viele sogenannte Presenter unterteilt (in anderen Frameworks begegnet Ihnen vielleicht der Begriff "Controller", was im Wesentlichen dasselbe ist). Das sind Klassen, von denen jede eine konkrete Seite der Website repräsentiert: z. B. die Startseite, ein Produkt im E-Shop, ein Anmeldeformular, einen Sitemap-Feed usw. Eine Anwendung kann von einem bis zu Tausenden von Presentern haben. - -Die `Application` beginnt damit, dass sie den sogenannten Router fragt, welcher Presenter den aktuellen Request bearbeiten soll. Der Router entscheidet über die Zuständigkeit. Er schaut sich die Eingabe-URL `https://example.com/product/123` an und entscheidet anhand seiner Konfiguration, dass diese Aufgabe zum Beispiel dem **Presenter** `Product` gehört, der die **Aktion** `show` für das Produkt mit `id: 123` ausführen soll. Es ist eine gute Gewohnheit, das Paar Presenter + Aktion durch einen Doppelpunkt getrennt zu schreiben, also `Product:show`. - -Der Router hat die URL also in das Paar `Presenter:Aktion` + Parameter umgewandelt, in unserem Fall `Product:show` + `id: 123`. Wie ein solcher Router aussieht, sehen Sie in der Datei `app/Core/RouterFactory.php`, und wir beschreiben ihn ausführlich im Kapitel [Routing |Routing]. - -Gehen wir weiter. Die `Application` kennt nun den Namen des Presenters und kann fortfahren. Sie erzeugt dazu eine Instanz der Klasse `ProductPresenter`, die den Code des Presenters `Product` enthält. Genauer gesagt bittet sie den DI-Container, den Presenter zu erzeugen, denn das Erzeugen von Objekten ist dessen Aufgabe. - -Der Presenter könnte so aussehen: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ProductRepository $repository, - ) { - } - - public function renderShow(int $id): void - { - // Daten aus dem Model holen und an das Template übergeben - $this->template->product = $this->repository->getProduct($id); - } -} -``` - -Der Presenter übernimmt die Bearbeitung des Requests. Die Aufgabe ist klar: die Aktion `show` mit `id: 123` ausführen. In der Terminologie der Presenter bedeutet das, dass die Methode `renderShow()` aufgerufen wird, die im Parameter `$id` den Wert `123` erhält. - -Ein Presenter kann mehrere Aktionen bearbeiten, also mehrere Methoden `render()` haben. Wir empfehlen jedoch, Presenter mit einer oder möglichst wenigen Aktionen zu entwerfen. - -Es wurde also die Methode `renderShow(123)` aufgerufen. Ihr Code ist ein erfundenes Beispiel, zeigt aber, wie Daten an das Template übergeben werden, nämlich durch Schreiben in `$this->template`. - -Anschließend gibt der Presenter eine Antwort zurück. Das kann eine HTML-Seite sein, ein Bild, ein XML-Dokument, das Senden einer Datei von der Festplatte, JSON oder auch eine Weiterleitung auf eine andere Seite. Wichtig ist: Wenn wir nicht ausdrücklich sagen, wie geantwortet werden soll (was bei `ProductPresenter` der Fall ist), lautet die Antwort, ein Template in eine HTML-Seite zu rendern. Warum? Weil wir in 99 % der Fälle ein Template rendern wollen. Deshalb macht der Presenter dieses Verhalten zum Standardverhalten und erleichtert uns die Arbeit. Das ist das Wesen von Nette. - -Wir müssen nicht einmal angeben, welches Template gerendert werden soll; den Pfad leitet das Framework selbst ab. Im Fall der Aktion `show` versucht es einfach, das Template `show.latte` in dem Verzeichnis zu laden, in dem auch die Klasse `ProductPresenter` liegt. Ebenso versucht es, das Layout in der Datei `@layout.latte` zu finden (mehr zur [Suche nach Templates |templates#Suche nach Templates]). - -Danach werden die Templates gerendert. Damit ist die Aufgabe des Presenters und der gesamten Anwendung erfüllt. Existiert das Template nicht, wird eine Fehlerseite 404 zurückgegeben. Mehr über Presenter erfahren Sie auf der Seite [Presenter|presenters]. - -[* request-flow.svg *] - -Zur Sicherheit fassen wir den gesamten Ablauf noch einmal mit einer etwas anderen URL zusammen: - -1) Die URL ist `https://example.com` -2) Die Anwendung startet, der DI-Container wird erzeugt und `Application::run()` wird ausgeführt. -3) Der Router dekodiert die URL in das Paar `Home:default`. -4) Es wird eine Instanz der Klasse `HomePresenter` erzeugt. -5) Die Methode `renderDefault()` wird aufgerufen (sofern sie existiert). -6) Das Template, z. B. `default.latte`, wird samt Layout, z. B. `@layout.latte`, gerendert. - - -Vielleicht sind Ihnen gerade viele neue Begriffe begegnet, aber wir glauben, dass sie Sinn ergeben. Anwendungen in Nette zu entwickeln ist bemerkenswert unkompliziert. - - -Templates -========= - -Da wir gerade bei Templates sind: Nette verwendet das Templating-System [Latte |latte:]. Deshalb haben die Template-Dateien die Endung `.latte`. Latte wird vor allem deshalb verwendet, weil es das sicherste Templating-System für PHP ist und zugleich das intuitivste. Sie müssen nicht viel Neues lernen; Kenntnisse von PHP und einigen wenigen Tags genügen. Alles Nötige finden Sie [in der Dokumentation |templates]. - -Im Template [erstellen Sie Links |creating-links] auf andere Presenter & Aktionen so: - -```latte -Produktdetail -``` - -Schreiben Sie einfach das vertraute Paar `Presenter:Aktion` statt der tatsächlichen URL und ergänzen Sie eventuelle Parameter. Der Trick steckt in `n:href`, das Nette sagt, dieses Attribut zu verarbeiten. Es erzeugt dann: - -```latte -Produktdetail -``` - -Um die Erzeugung der URL kümmert sich der bereits erwähnte Router. Router sind in Nette außergewöhnlich, weil sie nicht nur die Umwandlung einer URL in das Paar `Presenter:Aktion` beherrschen, sondern auch den umgekehrten Weg: aus dem Namen des Presenters, der Aktion und den Parametern eine URL zu erzeugen. Dadurch können Sie in einer fertigen Anwendung in Nette das URL-Format vollständig ändern, ohne in den Templates oder Presentern ein einziges Zeichen anzufassen - Sie ändern lediglich den Router. Das ermöglicht auch die sogenannte Kanonisierung, eine weitere einzigartige Eigenschaft von Nette, die das SEO (Search Engine Optimization) verbessert, indem sie automatisch verhindert, dass derselbe Inhalt unter verschiedenen URLs existiert. Viele Programmierer finden das erstaunlich. - - -Interaktive Komponenten -======================= - -Über Presenter müssen wir Ihnen noch eines sagen: Sie haben ein eingebautes Komponentensystem. Erfahrenere erinnern sich vielleicht an etwas Ähnliches aus Delphi oder ASP.NET Web Forms; React oder Vue.js bauen auf entfernt verwandten Konzepten auf. In der Welt der PHP-Frameworks ist das eine völlig einzigartige Eigenschaft. - -Komponenten sind eigenständige, wiederverwendbare Einheiten, die wir in Seiten (also in Presenter) einbetten. Das können [Formulare |forms:in-presenter] sein, [Datagrids |https://componette.org/contributte/datagrid/], Menüs, Umfragen - kurz alles, was sich sinnvoll wiederverwenden lässt. Wir können eigene Komponenten erstellen oder einige aus der [riesigen Auswahl |https://componette.org] an Open-Source-Komponenten nutzen. - -Komponenten beeinflussen die Herangehensweise an die Anwendungsentwicklung grundlegend. Sie eröffnen neue Möglichkeiten, Seiten aus vorbereiteten Einheiten zusammenzusetzen. Und sie haben auch etwas mit [Hollywood |components#Hollywood Style] gemeinsam. - - -DI-Container und Konfiguration -============================== - -Der DI-Container, also die Objekt-Factory, ist das Herz der gesamten Anwendung. - -Keine Sorge, es ist keine magische Blackbox, wie es die vorangegangenen Zeilen vielleicht nahelegen. In Wirklichkeit ist es eine ziemlich langweilige PHP-Klasse, die Nette erzeugt und im Cache-Verzeichnis ablegt. Sie enthält viele Methoden mit Namen wie `createServiceAbcd()`, von denen jede ein bestimmtes Objekt erzeugen und zurückgeben kann. Ja, darunter ist auch die Methode `createServiceApplication__application()`, die die Instanz von `Nette\Application\Application` erzeugt, die wir in `index.php` zum Starten der Anwendung gebraucht haben. Und es gibt Methoden zum Erzeugen der einzelnen Presenter und so weiter. - -Die vom DI-Container erzeugten Objekte nennt man aus irgendeinem Grund Services. - -Das wirklich Besondere an dieser Klasse ist, dass Sie sie nicht programmieren - das übernimmt das Framework. Es erzeugt tatsächlich den PHP-Code und speichert ihn auf der Festplatte. Sie geben nur Anweisungen, welche Objekte der Container erzeugen können soll und wie genau. Diese Anweisungen stehen in [Konfigurationsdateien |bootstrapping#Konfiguration des DI-Containers], die das Format [NEON|neon:format] verwenden und daher die Endung `.neon` haben. - -Konfigurationsdateien dienen ausschließlich dazu, den DI-Container zu instruieren. Wenn Sie also zum Beispiel im Abschnitt [session |http:configuration#Session] die Option `expiration: 14 days` angeben, ruft der DI-Container beim Erzeugen des Objekts `Nette\Http\Session`, das die Session repräsentiert, dessen Methode `setExpiration('14 days')` auf und macht die Konfiguration damit Wirklichkeit. - -Für Sie ist ein ganzes Kapitel vorbereitet, das beschreibt, was sich alles [konfigurieren |nette:configuring] lässt und wie Sie [eigene Services definieren |dependency-injection:services]. - -Sobald Sie sich ein wenig in das Erzeugen von Services vertieft haben, begegnet Ihnen der Begriff [Autowiring |dependency-injection:autowiring]. Das ist eine Eigenschaft, die Ihnen das Leben unglaublich erleichtert. Sie kann Objekte automatisch dorthin übergeben, wo Sie sie brauchen (zum Beispiel in die Konstruktoren Ihrer Klassen), ohne dass Sie etwas tun müssen. Sie werden feststellen, dass der DI-Container in Nette ein kleines Wunder ist. - - -Wie geht es weiter? -=================== - -Wir haben die grundlegenden Prinzipien von Anwendungen in Nette durchgenommen. Bisher war es nur ein Blick an die Oberfläche, aber Sie werden bald tiefer eintauchen und mit der Zeit großartige Webanwendungen erschaffen. Wo geht es weiter? Haben Sie schon das Tutorial [Erstellen Sie Ihre erste Anwendung|quickstart:] ausprobiert? - -Über das oben Beschriebene hinaus bietet Nette ein ganzes Arsenal [nützlicher Klassen|utils:], eine [Datenbankschicht|database:] usw. Klicken Sie sich ruhig durch die Dokumentation. Oder besuchen Sie den [Blog|https://blog.nette.org]. Sie werden viel Interessantes entdecken. - -Möge Ihnen das Framework viel Freude bereiten 💙 diff --git a/application/de/multiplier.texy b/application/de/multiplier.texy deleted file mode 100644 index 61c6c08e84..0000000000 --- a/application/de/multiplier.texy +++ /dev/null @@ -1,63 +0,0 @@ -Multiplier: dynamische Komponenten -********************************** - -.[perex] -Ein Werkzeug für die dynamische Erstellung interaktiver Komponenten. - -Beginnen wir mit einem typischen Beispiel: Stellen Sie sich eine Produktliste in einem E-Shop vor, bei der Sie zu jedem Artikel ein Formular "In den Warenkorb legen" ausgeben möchten. Eine mögliche Variante ist, die gesamte Auflistung in ein einziges Formular zu packen. Einen viel bequemeren Weg bietet jedoch [api:Nette\Application\UI\Multiplier]. - -Der Multiplier erlaubt es, bequem eine Factory für mehrere Komponenten zu definieren. Er funktioniert nach dem Prinzip verschachtelter Komponenten - jede Komponente, die von [api:Nette\ComponentModel\Container] erbt, kann weitere Komponenten enthalten. - -.[tip] -Siehe das Kapitel über das [Komponentenmodell |components#Komponenten im Detail] in der Dokumentation. - -Das Wesen des Multipliers ist, dass er als Elternteil auftritt, der seine Kinder mithilfe eines im Konstruktor übergebenen Callbacks dynamisch erzeugen kann. Siehe das Beispiel: - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function () { - $form = new Nette\Application\UI\Form; - $form->addInteger('amount', 'Anzahl:') - ->setRequired(); - $form->addSubmit('send', 'In den Warenkorb'); - return $form; - }); -} -``` - -Nun können wir im Template ganz einfach zu jedem Produkt das Formular ausgeben lassen - und jedes davon wird wirklich eine eigenständige Komponente sein. - -```latte -{foreach $items as $item} -

    {$item->title}

    - {$item->description} - - {control "shopForm-$item->id"} -{/foreach} -``` - -Das im Tag `{control}` übergebene Argument hat ein Format, das bedeutet: - -1. Hole die Komponente `shopForm`. -2. Hole aus ihr das Kind mit dem Namen `$item->id`. - -Beim ersten Aufruf von Punkt **1** existiert die Komponente `shopForm` noch nicht, daher wird ihre Factory `createComponentShopForm` aufgerufen. Auf der so erhaltenen Komponente (einer Instanz des Multipliers) wird dann die Factory des konkreten Formulars aufgerufen - das ist die anonyme Funktion, die wir dem Konstruktor des Multipliers übergeben haben. - -In der nächsten Iteration der foreach-Schleife wird die Methode `createComponentShopForm` nicht erneut aufgerufen (die Komponente existiert bereits). Da wir jedoch ein anderes Kind suchen (denn `$item->id` ist in jeder Iteration anders), wird die anonyme Funktion erneut aufgerufen und liefert ein neues Formular zurück. - -Es bleibt nur noch sicherzustellen, dass das Formular auch wirklich das richtige Produkt in den Warenkorb legt - derzeit ist das Formular bei jedem Produkt identisch. Dabei hilft uns eine Eigenschaft des Multipliers (und generell jeder Komponenten-Factory im Nette Framework): Jede Factory erhält als erstes Argument den Namen der erzeugten Komponente. Die Factory des Multipliers erhält darüber hinaus als zweites Argument die Instanz des Multipliers selbst. In unserem Fall ist das erste Argument `$item->id`, also genau die Information, die wir brauchen. Es genügt daher, die Erstellung des Formulars leicht anzupassen: - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function (string $itemId) { - $form = new Nette\Application\UI\Form; - $form->addInteger('amount', 'Anzahl:') - ->setRequired(); - $form->addHidden('itemId', $itemId); - $form->addSubmit('send', 'In den Warenkorb'); - return $form; - }); -} -``` diff --git a/application/de/presenters.texy b/application/de/presenters.texy deleted file mode 100644 index 230239d102..0000000000 --- a/application/de/presenters.texy +++ /dev/null @@ -1,595 +0,0 @@ -Presenter -********* - -
    - -Wir sehen uns an, wie in Nette Presenter und Templates geschrieben werden. Nach der Lektüre werden Sie wissen: - -- wie Presenter funktionieren -- was persistente Parameter sind -- wie Templates gerendert werden - -
    - -[Wir wissen bereits |how-it-works#Nette Application], dass ein Presenter eine Klasse ist, die eine konkrete Seite einer Webanwendung repräsentiert, etwa die Startseite, ein Produkt im E-Shop, ein Anmeldeformular, einen Sitemap-Feed usw. Eine Anwendung kann von einem bis zu Tausenden von Presentern haben. In anderen Frameworks sind sie auch als Controller bekannt. - -Üblicherweise meint der Begriff Presenter einen Nachfahren der Klasse [api:Nette\Application\UI\Presenter], die sich zum Erzeugen von Weboberflächen eignet und der sich der Rest dieses Kapitels widmet. Im allgemeinen Sinn ist ein Presenter jedes Objekt, das das Interface [api:Nette\Application\IPresenter] implementiert. - - -Lebenszyklus des Presenters -=========================== - -Die Aufgabe des Presenters ist es, einen Request zu bearbeiten und eine Antwort zurückzugeben (das kann eine HTML-Seite sein, ein Bild, eine Weiterleitung usw.). - -Zu Beginn wird ihm also ein Request übergeben. Das ist nicht direkt der HTTP-Request, sondern ein Objekt [api:Nette\Application\Request], in das der HTTP-Request mithilfe des Routers umgewandelt wurde. Mit diesem Objekt kommen wir üblicherweise nicht direkt in Berührung, denn der Presenter delegiert die Bearbeitung des Requests geschickt an andere Methoden, die wir uns nun ansehen. - -[* lifecycle.svg *] *** Lebenszyklus des Presenters .<> - -Das Diagramm zeigt eine Liste von Methoden, die der Reihe nach von oben nach unten aufgerufen werden, sofern sie existieren. Keine davon ist verpflichtend; Sie können einen völlig leeren Presenter ohne eine einzige Methode haben und darauf eine einfache statische Website aufbauen. - - -`__construct()` ---------------- - -Der Konstruktor gehört streng genommen nicht zum Lebenszyklus des Presenters, da er im Moment der Objekterzeugung aufgerufen wird. Wir erwähnen ihn jedoch wegen seiner Bedeutung. Der Konstruktor (zusammen mit der [Inject-Methode|best-practices:inject-method-attribute]) dient der Übergabe von Abhängigkeiten. - -Der Presenter sollte nicht die Geschäftslogik der Anwendung erledigen, nicht in die Datenbank schreiben oder aus ihr lesen, keine Berechnungen anstellen usw. Dafür sind Klassen in der Schicht zuständig, die wir Model nennen. Zum Beispiel kann eine Klasse `ArticleRepository` für das Laden und Speichern von Artikeln verantwortlich sein. Damit der Presenter mit ihr arbeiten kann, muss sie ihm [per Dependency Injection übergeben werden |dependency-injection:passing-dependencies]: - - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articles, - ) { - } -} -``` - - -`startup()` ------------ - -Unmittelbar nach dem Empfang des Requests wird die Methode `startup()` aufgerufen. Sie können sie zum Initialisieren von Properties, zum Prüfen von Benutzerberechtigungen usw. verwenden. Es ist erforderlich, dass diese Methode immer ihren Vorfahren aufruft: `parent::startup()`. - - -`action(args...)` .{toc: action()} --------------------------------------------------- - -Ähnlich wie die Methode `render()`. Während `render()` dazu gedacht ist, Daten für ein konkretes Template vorzubereiten, das anschließend gerendert wird, verarbeitet `action()` einen Request, ohne dass danach zwingend ein Template gerendert wird. Sie kann zum Beispiel Daten verarbeiten, einen Benutzer an- oder abmelden und so weiter, und dann [woandershin weiterleiten |#Weiterleitung]. - -Wichtig ist, dass `action()` *vor* `render()` aufgerufen wird. Dadurch können wir den Verlauf des Requests in der Action-Methode noch ändern, etwa das Template, das gerendert wird, oder sogar die aufzurufende Methode `render()` mit `setView('otherView')` austauschen. - -.{data-version:3.2.3} -Sie können mit der Methode `switch('otherAction')` sogar zu einer völlig anderen Aktion wechseln. Sie bricht die aktuelle Methode ab und führt stattdessen die Methoden `action()` und `render()` der neuen Aktion aus (und schaltet die automatische [Kanonisierung|#Kanonisierung] ab). Der Request selbst läuft weiter; unterbrochen wird nur die gerade laufende Methode. - -Der Methode werden die Parameter aus dem Request übergeben. Es ist möglich und empfehlenswert, für diese Parameter Typen anzugeben, z. B. `actionShow(int $id, ?string $slug = null)`. Fehlt der Parameter `id` oder ist er keine ganze Zahl, gibt der Presenter einen [Fehler 404 |#Fehler 404 usw.] zurück und endet. - - -`handle(args...)` .{toc: handle()} --------------------------------------------------- - -Diese Methode verarbeitet die sogenannten Signale, die wir im Kapitel über [Komponenten |components#Signal] kennenlernen. Sie ist vor allem für Komponenten und die Bearbeitung von AJAX-Requests gedacht. - -Der Methode werden, genau wie bei `action()`, die Parameter aus dem Request übergeben, samt Typprüfung. - - -`beforeRender()` ----------------- - -Die Methode `beforeRender` wird, wie ihr Name nahelegt, vor jeder Methode `render()` aufgerufen. Sie dient der gemeinsamen Konfiguration von Templates, der Übergabe von Variablen an das Layout und Ähnlichem. - - -`render(args...)` .{toc: render()} ----------------------------------------------- - -Hier bereiten wir das Template für das anschließende Rendern vor, übergeben ihm Daten usw. - -Der Methode werden, genau wie bei `action()`, die Parameter aus dem Request übergeben, samt Typprüfung. - -```php -public function renderShow(int $id): void -{ - // Daten aus dem Model holen und an das Template übergeben - $this->template->article = $this->articles->getById($id); -} -``` - - -`afterRender()` ---------------- - -Die Methode `afterRender` wird, wie der Name wiederum nahelegt, nach jeder Methode `render()` aufgerufen. Sie wird eher selten verwendet. - - -`shutdown()` ------------- - -Wird am Ende des Lebenszyklus des Presenters aufgerufen. - - -Events ------- - -Neben den Methoden `startup()`, `beforeRender()` und `shutdown()`, die im Rahmen des Lebenszyklus des Presenters aufgerufen werden, lassen sich weitere Funktionen definieren, die automatisch aufgerufen werden. Der Presenter definiert sogenannte [Events |nette:glossary#Events], und Sie fügen deren Handler in die Arrays `$onStartup`, `$onRender` und `$onShutdown` ein. - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct() - { - $this->onStartup[] = function () { - // ... - }; - } -} -``` - -Die Handler im Array `$onStartup` werden unmittelbar vor der Methode `startup()` aufgerufen, die Handler in `$onRender` zwischen `beforeRender()` und `render()` und schließlich die Handler in `$onShutdown` unmittelbar vor `shutdown()`. - - -**Ein Rat, bevor wir weitermachen:** Wie Sie sehen, kann ein Presenter mehrere Aktionen/Views bearbeiten, also mehrere Methoden `render()` haben. Wir empfehlen jedoch, Presenter mit einer oder möglichst wenigen Aktionen zu entwerfen. - - -Eine Antwort senden -=================== - -Die Antwort des Presenters ist typischerweise das [Rendern eines Templates in eine HTML-Seite|templates], es kann aber auch das Senden einer Datei sein, von JSON oder sogar eine Weiterleitung auf eine andere Seite. - -Zu jedem Zeitpunkt des Lebenszyklus können wir mit einer der folgenden Methoden eine Antwort senden und den Presenter zugleich beenden: - -- `redirect()`, `redirectPermanent()`, `redirectUrl()` und `forward()` führen eine [Weiterleitung |#Weiterleitung] aus -- `error()` beendet den Presenter [wegen eines Fehlers |#Fehler 404 usw.] -- `sendJson($data)` beendet den Presenter und [sendet Daten |#JSON senden] im JSON-Format -- `sendTemplate()` beendet den Presenter und [rendert sofort das Template |templates] -- `sendResponse($response)` beendet den Presenter und sendet eine [eigene Antwort |#Antworten] -- `terminate()` beendet den Presenter ohne Antwort - -Jede dieser Methoden beendet den Presenter sofort, indem sie die stille Beendigungs-Exception `Nette\Application\AbortException` wirft. - -Rufen Sie keine dieser Methoden auf, geht der Presenter automatisch zum Rendern des Templates über. Warum? Weil wir in 99 % der Fälle ein Template rendern wollen, macht der Presenter dieses Verhalten zum Standardverhalten und erleichtert uns die Arbeit. - - -Links erstellen -=============== - -Der Presenter hat die Methode `link()`, mit der URL-Links auf andere Presenter erstellt werden. Der erste Parameter ist der Ziel-Presenter & die Aktion, gefolgt von Argumenten, die sich auch als Array übergeben lassen: - -```php -$url = $this->link('Product:show', $id); - -$url = $this->link('Product:show', [$id, 'lang' => 'en']); -``` - -Im Template werden Links auf andere Presenter & Aktionen so erstellt: - -```latte -Produktdetail -``` - -Schreiben Sie einfach das vertraute Paar `Presenter:action` statt der tatsächlichen URL und ergänzen Sie eventuelle Parameter. Der Trick steckt in `n:href`, das Latte sagt, dieses Attribut zu verarbeiten und die echte URL zu erzeugen. In Nette müssen Sie überhaupt nicht über URLs nachdenken, sondern nur über Presenter und Aktionen. - -Mehr dazu finden Sie im Kapitel [Erstellen von URL-Links|creating-links]. - - -Weiterleitung -============= - -Zum Wechsel auf einen anderen Presenter dienen die Methoden `redirect()` und `forward()`. Sie haben eine sehr ähnliche Syntax wie die Methode [link() |#Links erstellen]. - -Die Methode `forward()` wechselt ohne HTTP-Weiterleitung sofort auf den neuen Presenter: - -```php -$this->forward('Product:show'); -``` - -Beispiel für eine temporäre Weiterleitung mit dem HTTP-Code 302 (oder 303, wenn die aktuelle Request-Methode POST ist): - -```php -$this->redirect('Product:show', $id); -``` - -Für eine dauerhafte Weiterleitung mit dem HTTP-Code 301 verwenden Sie dies: - -```php -$this->redirectPermanent('Product:show', $id); -``` - -Auf eine andere URL außerhalb der Anwendung leiten Sie mit der Methode `redirectUrl()` weiter. Als zweiter Parameter lässt sich der HTTP-Code angeben, Standard ist 302 (oder 303, wenn die aktuelle Request-Methode POST ist): - -```php -$this->redirectUrl('https://nette.org'); -``` - -Eine Weiterleitung beendet die Tätigkeit des Presenters sofort, indem sie die sogenannte stille Beendigungs-Exception `Nette\Application\AbortException` wirft. - -Vor der Weiterleitung lassen sich [#Flash-Meldungen] senden, also Meldungen, die nach der Weiterleitung im Template angezeigt werden. - - -Flash-Meldungen -=============== - -Das sind Meldungen, die typischerweise über das Ergebnis einer Operation informieren. Eine wichtige Eigenschaft von Flash-Meldungen ist, dass sie im Template auch nach einer Weiterleitung verfügbar bleiben. Nach der Anzeige bleiben sie noch 30 Sekunden aktiv - lädt der Benutzer die Seite zum Beispiel wegen eines Übertragungsfehlers neu, verschwindet die Meldung nicht sofort. - -Rufen Sie einfach die Methode [flashMessage() |api:Nette\Application\UI\Control::flashMessage()] auf, um die Übergabe an das Template kümmert sich der Presenter. Der erste Parameter ist der Text der Meldung, der optionale zweite Parameter ihr Typ (z. B. error, warning, info). Die Methode `flashMessage()` gibt eine Instanz der Flash-Meldung zurück, der sich weitere Informationen hinzufügen lassen. - -```php -$this->flashMessage('Das Element wurde gelöscht.'); -$this->redirect(/* ... */); // und weiterleiten -``` - -Im Template stehen diese Meldungen in der Variablen `$flashes` als Objekte `stdClass` zur Verfügung, die die Properties `message` (Text der Meldung) und `type` (Typ der Meldung) sowie gegebenenfalls die erwähnten eigenen Informationen enthalten. Wir rendern sie so: - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -Fehler 404 usw. -=============== - -Kann der Request nicht erfüllt werden, zum Beispiel weil der Artikel, den wir anzeigen wollen, nicht in der Datenbank existiert, werfen wir mit der Methode `error(string $message = '', int $httpCode = 404)` einen Fehler 404. - -```php -public function renderShow(int $id): void -{ - $article = $this->articles->getById($id); - if (!$article) { - $this->error(); - } - // ... -} -``` - -Der HTTP-Fehlercode lässt sich als zweiter Parameter übergeben, Standard ist 404. Die Methode funktioniert so, dass sie eine `Nette\Application\BadRequestException` wirft, woraufhin die `Application` die Steuerung an den Error-Presenter übergibt. Das ist ein Presenter, dessen Aufgabe es ist, eine Seite mit Informationen über den aufgetretenen Fehler anzuzeigen. Der Error-Presenter wird in der [Konfiguration der Anwendung|configuration] eingestellt. - - -JSON senden -=========== - -Die Methode `sendJson($data)` kodiert die übergebenen Daten in JSON, sendet sie als HTTP-Antwort und beendet den Presenter. Beispiel: - -```php -public function actionData(): void -{ - $data = ['hello' => 'nette']; - $this->sendJson($data); -} -``` - - -Parameter des Requests .{data-version:3.1.14} -============================================= - -Der Presenter und ebenso jede Komponente beziehen ihre Parameter aus dem HTTP-Request. Ihre Werte holen Sie sich mit den Methoden `getParameter($name)` oder `getParameters()`. Die Werte sind Strings oder Arrays von Strings, im Grunde rohe Daten direkt aus der URL. - -Für mehr Komfort empfehlen wir, auf die Parameter über Properties zuzugreifen. Kennzeichnen Sie sie einfach mit dem Attribut `#[Parameter]`: - -```php -use Nette\Application\Attributes\Parameter; // diese Zeile ist wichtig - -class HomePresenter extends Nette\Application\UI\Presenter -{ - #[Parameter] - public string $theme; // muss public sein -} -``` - -Für die Property empfehlen wir, den Datentyp anzugeben (z. B. `string`), Nette wandelt den Wert dann automatisch entsprechend um. Die Werte der Parameter lassen sich auch [validieren |#Validierung der Parameter]. - -Beim Erstellen eines Links lässt sich der Wert des Parameters direkt setzen: - -```latte -klicken -``` - - -Persistente Parameter -===================== - -Persistente Parameter dienen dazu, den Zustand über verschiedene Requests hinweg zu erhalten. Ihr Wert bleibt auch nach dem Klick auf einen Link derselbe. Anders als Daten in der Session werden sie in der URL übertragen. Und das geschieht vollautomatisch, sie müssen also nicht ausdrücklich in `link()` oder `n:href` angegeben werden. - -Ein Anwendungsbeispiel? Stellen Sie sich vor, Sie haben eine mehrsprachige Anwendung. Die aktuelle Sprache ist ein Parameter, der immer Teil der URL sein muss. Aber ihn in jedem Link anzugeben wäre unglaublich mühsam. Also machen Sie daraus den persistenten Parameter `lang`, und er wird automatisch mitgeführt. Praktisch! - -Einen persistenten Parameter in Nette zu erstellen ist ausgesprochen einfach. Legen Sie einfach eine public Property an und kennzeichnen Sie sie mit dem Attribut: (früher wurde `/** @persistent */` verwendet) - -```php -use Nette\Application\Attributes\Persistent; // diese Zeile ist wichtig - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; // muss public sein -} -``` - -Hat `$this->lang` zum Beispiel den Wert `'en'`, enthalten auch die mit `link()` oder `n:href` erstellten Links den Parameter `lang=en`. Und nach dem Klick auf den Link ist `$this->lang` wieder `'en'`. - -Für die Property empfehlen wir, den Datentyp anzugeben (z. B. `string`), und Sie können auch einen Standardwert angeben. Die Werte der Parameter lassen sich [validieren |#Validierung der Parameter]. - -Persistente Parameter werden üblicherweise zwischen allen Aktionen eines bestimmten Presenters übertragen. Damit sie auch über mehrere Presenter hinweg übertragen werden, müssen sie definiert werden entweder: - -- in einem gemeinsamen Vorfahren, von dem die Presenter erben -- oder in einem Trait, den die Presenter verwenden: - -```php -trait LanguageAware -{ - #[Persistent] - public string $lang; -} - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - use LanguageAware; -} -``` - -Beim Erstellen eines Links lässt sich der Wert eines persistenten Parameters ändern: - -```latte -Detail auf Tschechisch -``` - -Oder er lässt sich *zurücksetzen*, also aus der URL entfernen. Er nimmt dann seinen Standardwert an: - -```latte -klicken -``` - - -Gemeinsamer Parameterraum -========================= - -Die Parameter des Requests, die [persistenten Parameter |#Persistente Parameter] und die Parameter der Methoden `action`, `render` und `handle` (Signal) teilen sich einen einzigen Raum, in dem jeder über seinen Namen identifiziert wird. Erscheint derselbe Name in mehreren von ihnen, bezeichnen sie ein und denselben Wert. - -Das nutzt man oft zum Vorteil. Zum Beispiel sind der persistente Parameter `lang` und das Argument `$lang` einer Action- oder Signal-Methode ein und dasselbe - Sie können den aktuellen Wert eines persistenten Parameters lesen, indem Sie ihn einfach in der Signatur der Methode aufführen: - -```php -#[Persistent] -public string $lang; - -public function handleSearch(string $query, string $lang): void -{ - // $lang enthält den aktuellen Wert des persistenten Parameters lang -} -``` - -Weil dieser Raum gemeinsam ist, halten Sie die Namen der Parameter eindeutig, sofern sie sich nicht absichtlich einen Wert teilen sollen. Das gilt auch für Signale, die zusätzlich Parameter aus dem POST-Body des Requests lesen, siehe [Signale im Detail |components#Signale im Detail]. - - -Interaktive Komponenten -======================= - -Presenter haben ein eingebautes Komponentensystem. Komponenten sind eigenständige, wiederverwendbare Einheiten, die wir in Presenter einbetten. Das können [Formulare |forms:in-presenter] sein, Datagrids, Menüs - kurz alles, was sich sinnvoll wiederholt verwenden lässt. - -Wie werden Komponenten in Presenter eingebettet und anschließend verwendet? Das erfahren Sie im Kapitel [Komponenten |components]. Sie erfahren dort sogar, was sie mit Hollywood gemeinsam haben. - -Und woher bekomme ich Komponenten? Auf [Componette |https://componette.org/search/component] finden Sie Open-Source-Komponenten und viele weitere Erweiterungen für Nette, beigesteuert von Freiwilligen aus der Community des Frameworks. - - -Tiefer eintauchen -================= - -.[tip] -Was wir in diesem Kapitel bisher behandelt haben, dürfte für die meisten Anwendungsfälle genügen. Die folgenden Abschnitte sind für alle gedacht, die sich intensiver mit Presentern beschäftigen und absolut alles wissen wollen. - - -Validierung der Parameter -------------------------- - -Die aus der URL empfangenen Werte der [#Parameter des Requests] und [#Persistente Parameter] werden von der Methode `loadState()` in die Properties geschrieben. Sie prüft außerdem, ob der in der Property angegebene Datentyp passt; andernfalls antwortet sie mit dem Fehler 404 und die Seite wird nicht angezeigt. - -Vertrauen Sie den aus der URL empfangenen Parametern niemals blind, denn sie lassen sich vom Benutzer leicht überschreiben. So würden wir zum Beispiel prüfen, ob die Sprache `$this->lang` zu den unterstützten gehört. Ein geeigneter Weg dafür ist, die erwähnte Methode `loadState()` zu überschreiben: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; - - public function loadState(array $params): void - { - parent::loadState($params); // hier wird $this->lang gesetzt - // es folgt die eigene Wertprüfung: - if (!in_array($this->lang, ['en', 'cs'])) { - $this->error(); - } - } -} -``` - - -Request speichern und wiederherstellen --------------------------------------- - -Der vom Presenter bearbeitete Request ist ein Objekt [api:Nette\Application\Request], das die Methode `getRequest()` des Presenters zurückgibt. - -Der aktuelle Request lässt sich in der Session speichern oder umgekehrt aus ihr wiederherstellen und vom Presenter erneut ausführen. Das ist zum Beispiel nützlich, wenn ein Benutzer ein Formular ausfüllt und seine Anmeldung abläuft. Damit keine Daten verloren gehen, speichern wir den aktuellen Request vor der Weiterleitung auf die Anmeldeseite mit `$reqId = $this->storeRequest()` in der Session. Das gibt seinen Bezeichner als kurzen String zurück, den wir dann als Parameter an den Anmelde-Presenter übergeben. - -Nach der Anmeldung rufen wir die Methode `$this->restoreRequest($reqId)` auf, die den Request aus der Session holt. POST-Requests werden an ihn weitergeleitet, die übrigen (GET) werden auf die URL des Requests weitergeleitet. Die Methode prüft, ob der Request von demselben Benutzer erstellt wurde, der nun angemeldet ist. Meldet sich ein anderer Benutzer an oder ist der Schlüssel ungültig, tut sie nichts und das Programm läuft wie gewohnt weiter. - -Siehe die Anleitung [Wie man zu einer früheren Seite zurückkehrt |best-practices:restore-request]. - - -Kanonisierung -------------- - -Presenter haben eine wirklich ausgezeichnete Eigenschaft, die zu besserem SEO (Search Engine Optimization) beiträgt. Sie verhindern automatisch, dass derselbe Inhalt unter verschiedenen URLs existiert. Führen mehrere URLs zu einem bestimmten Ziel, z. B. `/index` und `/index?page=1`, bestimmt das Framework eine davon als primär (kanonisch) und leitet die übrigen mit dem HTTP-Code 301 dorthin weiter. Dadurch indexieren Suchmaschinen Ihre Seiten nicht doppelt und verwässern deren Page Rank nicht. - -Dieser Vorgang heißt Kanonisierung. Die kanonische URL ist diejenige, die der [Router|routing] erzeugt, typischerweise die erste passende Route in der Collection. - -Die Kanonisierung ist standardmäßig eingeschaltet und lässt sich mit `$this->autoCanonicalize = false` abschalten. - -Bei AJAX- oder POST-Requests findet keine Weiterleitung statt, da dies zu Datenverlust führen könnte oder keinen zusätzlichen SEO-Nutzen brächte. - -Die Kanonisierung lässt sich auch manuell mit der Methode `canonicalize()` auslösen. Ähnlich wie der Methode `link()` übergeben Sie ihr den Presenter, die Aktion und die Parameter. Sie erzeugt einen Link und vergleicht ihn mit der aktuellen URL-Adresse. Unterscheiden sie sich, leitet sie auf den erzeugten Link weiter. - -```php -public function actionShow(int $id, ?string $slug = null): void -{ - $realSlug = $this->facade->getSlugForId($id); - // leitet weiter, wenn $slug von $realSlug abweicht - $this->canonicalize('Product:show', [$id, $realSlug]); -} -``` - -Ein vollständiges Muster, das Route-Filter mit `canonicalize()` kombiniert, um SEO-freundliche URLs zu erzeugen, finden Sie unter [Schöne URLs mit Slugs |best-practices:pretty-urls]. - - -Antworten ---------- - -Die vom Presenter zurückgegebene Antwort ist ein Objekt, das das Interface [api:Nette\Application\Response] implementiert. Es stehen mehrere fertige Antworten zur Verfügung: - -- [api:Nette\Application\Responses\CallbackResponse] - sendet einen Callback -- [api:Nette\Application\Responses\FileResponse] - sendet eine Datei -- [api:Nette\Application\Responses\ForwardResponse] - forward() -- [api:Nette\Application\Responses\JsonResponse] - sendet JSON -- [api:Nette\Application\Responses\RedirectResponse] - Weiterleitung -- [api:Nette\Application\Responses\TextResponse] - sendet Text -- [api:Nette\Application\Responses\VoidResponse] - leere Antwort - -Antworten werden mit der Methode `sendResponse()` gesendet: - -```php -use Nette\Application\Responses; - -// Reiner Text -$this->sendResponse(new Responses\TextResponse('Hello Nette!')); - -// Sendet eine Datei -$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf')); - -// Sendet einen Callback -$callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) { - if ($httpResponse->getHeader('Content-Type') === 'text/html') { - echo '

    Hallo

    '; - } -}; -$this->sendResponse(new Responses\CallbackResponse($callback)); -``` - -Sie können auch eine eigene Antwort schreiben. Implementieren Sie einfach das Interface `Nette\Application\Response`, das eine einzige Methode `send()` hat, die den HTTP-Request und die HTTP-Response entgegennimmt. Das ist zum Beispiel nützlich, wenn Sie Daten streamen, die Sie nicht im Speicher halten wollen: - -```php -class CsvResponse implements Nette\Application\Response -{ - public function __construct( - private string $fileName, - private iterable $rows, - ) { - } - - public function send(Nette\Http\IRequest $request, Nette\Http\IResponse $response): void - { - $response->setContentType('text/csv', 'utf-8'); - $response->sendAsFile($this->fileName); - - $handle = fopen('php://output', 'w'); - foreach ($this->rows as $row) { - fputcsv($handle, $row); - } - - fclose($handle); - } -} -``` - -Im Presenter senden Sie sie dann wie gewohnt: `$this->sendResponse(new CsvResponse('export.csv', $rows));` - - -HTTP-Caching ------------- - -Die Methode `lastModified()` macht es leicht, HTTP-Caching zu nutzen. Sie übergeben ihr Datum und Uhrzeit der letzten Änderung des Inhalts (als Timestamp, String oder Objekt `DateTimeInterface`) und optional einen ETag-Validator (einen kurzen String, der die aktuelle Version des Inhalts identifiziert, etwa deren Hash) sowie eine Ablaufzeit. Hat der Browser bereits eine passende Version, sendet der Presenter die Antwort `304 Not Modified` und endet, sodass die Seite nicht unnötig gerendert und übertragen wird: - -```php -public function renderArticle(int $id): void -{ - $article = $this->articles->getById($id); - $this->lastModified($article->updatedAt); - // ... -} -``` - - -Fertigstellung des Templates .{data-version:3.3.0} --------------------------------------------------- - -Wenn der Presenter ein Template rendert, ruft die Methode `sendTemplate()` unmittelbar vor dem Rendern `completeTemplate()` auf. Diese Methode füllt die mit dem Attribut `#[TemplateVariable]` gekennzeichneten Variablen und ermittelt die Template-Datei (die Standardvariablen setzt bereits die `TemplateFactory` beim Erzeugen des Templates). Sie können diese protected-Methode überschreiben, um Variablen zu ergänzen, die alle Views gemeinsam haben, oder um eine andere Datei zu setzen: - -```php -protected function completeTemplate(Nette\Application\UI\Template $template): void -{ - parent::completeTemplate($template); - $template->siteName = 'Meine App'; -} -``` - - -Zugriffsbeschränkung mit `#[Requires]` .{data-version:3.2.3} ------------------------------------------------------------- - -Das Attribut `#[Requires]` bietet fortgeschrittene Möglichkeiten, den Zugriff auf Presenter und deren Methoden einzuschränken. Damit lassen sich HTTP-Methoden festlegen, ein AJAX-Request verlangen, der Zugriff auf denselben Origin beschränken und der Zugriff nur über Forwarding erlauben. Das Attribut lässt sich sowohl auf Presenter-Klassen als auch auf einzelne Methoden wie `action()`, `render()`, `handle()` und `createComponent()` anwenden. - -Sie können diese Einschränkungen angeben: -- auf HTTP-Methoden: `#[Requires(methods: ['GET', 'POST'])]` -- Erfordernis eines AJAX-Requests: `#[Requires(ajax: true)]` -- Zugriff nur vom selben Origin: `#[Requires(sameOrigin: true)]` -- Zugriff nur über Forwarding: `#[Requires(forward: true)]` -- Einschränkung auf bestimmte Aktionen: `#[Requires(actions: 'default')]` - -.[note] -Seit Version 3.3 wird die Übereinstimmung des Origins über den Browser-Header `Sec-Fetch-Site` geprüft (früher über ein SameSite-Cookie), was zuverlässiger ist und die exakte Übereinstimmung von Schema, Domain und Port prüft. - -Details finden Sie in der Anleitung [Wie man das Attribut Requires verwendet |best-practices:attribute-requires]. - - -Prüfung der HTTP-Methode ------------------------- - -Presenter in Nette prüfen bei jedem eingehenden Request automatisch die HTTP-Methode, vor allem aus Sicherheitsgründen. Standardmäßig sind die Methoden `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH` erlaubt. - -Wollen Sie zusätzlich zum Beispiel die Methode `OPTIONS` erlauben, verwenden Sie das Attribut `#[Requires]` (seit Nette Application v3.2.3): - -```php -#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])] -class MyPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Seit Version 3.1.13 erfolgt die Prüfung in `checkHttpMethod()`, die prüft, ob die im Request angegebene Methode im Array `$presenter->allowedMethods` enthalten ist. Seit Version 3.2.3 ist dieser Ansatz zugunsten von `#[Requires]` veraltet. Die Methode können Sie so überschreiben: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - protected function checkHttpMethod(): void - { - $this->allowedMethods[] = 'OPTIONS'; - parent::checkHttpMethod(); - } -} -``` - -Wichtig ist zu betonen: Wenn Sie die Methode `OPTIONS` aktivieren, müssen Sie sie anschließend in Ihrem Presenter auch angemessen behandeln. Diese Methode wird oft als sogenannter Preflight-Request verwendet, den der Browser automatisch vor dem eigentlichen Request sendet, wenn festgestellt werden muss, ob der Request nach der CORS-Richtlinie (Cross-Origin Resource Sharing) zulässig ist. Aktivieren Sie die Methode, ohne die richtige Antwort zu implementieren, kann das zu Inkonsistenzen und potenziellen Sicherheitsproblemen führen. - - -Veraltete Aktionen kennzeichnen .{data-version:3.2.3} ------------------------------------------------------ - -Das Attribut `#[Deprecated]` kennzeichnet Aktionen, Signale oder ganze Presenter als veraltet und für eine künftige Entfernung vorgesehen. Beim Erzeugen von Links auf veraltete Teile der Anwendung wirft Nette eine Warnung, um die Entwickler darauf aufmerksam zu machen. - -Das Attribut lässt sich entweder auf die gesamte Presenter-Klasse oder auf einzelne Methoden `action()`, `render()` und `handle()` anwenden. - - -Weiterführende Lektüre -====================== - -- [Inject-Methoden und -Attribute |best-practices:inject-method-attribute] -- [Presenter aus Traits zusammensetzen |best-practices:presenter-traits] -- [Einstellungen an Presenter übergeben |best-practices:passing-settings-to-presenters] -- [Wie man zu einer früheren Seite zurückkehrt |best-practices:restore-request] diff --git a/application/de/routing.texy b/application/de/routing.texy deleted file mode 100644 index f175e154aa..0000000000 --- a/application/de/routing.texy +++ /dev/null @@ -1,749 +0,0 @@ -Routing -******* - -
    - -Der Router kümmert sich um alles rund um URL-Adressen, damit Sie nicht mehr über sie nachdenken müssen. Wir zeigen Ihnen: - -- wie man den Router einstellt, damit die URLs so aussehen, wie Sie es möchten -- wir sprechen über SEO und Weiterleitungen -- und zeigen Ihnen, wie Sie einen eigenen Router schreiben - -
    - - -Benutzerfreundlichere URLs (auch cool oder pretty URLs genannt) sind besser nutzbar, leichter zu merken und tragen positiv zum SEO bei. Nette denkt daran und kommt Entwicklern voll entgegen. Sie können für Ihre Anwendung genau die Struktur der URL-Adressen entwerfen, die Sie möchten. Sie können sie sogar erst dann entwerfen, wenn die Anwendung bereits fertig ist, denn es sind dafür keine Eingriffe in den Code oder die Templates nötig. Sie wird nämlich auf elegante Weise an [einer einzigen Stelle |#Integration] definiert, im Router, und ist nicht in Form von Annotationen über alle Presenter verstreut. - -Der Router in Nette ist außergewöhnlich, weil er **bidirektional** ist. Er kann sowohl URLs aus HTTP-Requests dekodieren als auch Links erstellen. Er spielt daher eine entscheidende Rolle in [Nette Application |how-it-works#Nette Application], denn er entscheidet nicht nur, welcher Presenter und welche Aktion den aktuellen Request ausführen, sondern wird auch zum [Erzeugen von URLs |creating-links] im Template usw. verwendet. - -Der Router ist jedoch nicht auf diese Verwendung beschränkt; Sie können ihn auch in Anwendungen einsetzen, die überhaupt keine Presenter verwenden, für REST-APIs usw. Mehr dazu im Abschnitt [#Eigenständige Verwendung]. - - -Routensammlung -============== - -Den angenehmsten Weg, die Form der URL-Adressen in der Anwendung zu definieren, bietet die Klasse [api:Nette\Application\Routers\RouteList]. Die Definition besteht aus einer Liste sogenannter Routes, also Masken von URL-Adressen und den ihnen über eine einfache API zugeordneten Presentern und Aktionen. Die Routes müssen wir in keiner Weise benennen. - -```php -$router = new Nette\Application\Routers\RouteList; -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('article/', 'Article:view'); -// ... -``` - -Das Beispiel besagt: Öffnen wir im Browser `https://domain.com/rss.xml`, wird der Presenter `Feed` mit der Aktion `rss` angezeigt; bei `https://domain.com/article/12` der Presenter `Article` mit der Aktion `view` usw. Wird keine passende Route gefunden, reagiert Nette Application mit dem Werfen einer [BadRequestException |api:Nette\Application\BadRequestException], die dem Benutzer als Fehlerseite 404 Not Found angezeigt wird. - - -Reihenfolge der Routes ----------------------- - -Ganz **entscheidend ist die Reihenfolge**, in der die einzelnen Routes aufgeführt sind, denn sie werden der Reihe nach von oben nach unten ausgewertet. Es gilt die Regel, dass wir Routes **von speziell nach allgemein** deklarieren: - -```php -// FALSCH: 'rss.xml' wird von der ersten Route abgefangen, die diesen String als versteht -$router->addRoute('', 'Article:view'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// GUT -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('', 'Article:view'); -``` - -Auch beim Erzeugen von Links werden die Routes von oben nach unten ausgewertet: - -```php -// FALSCH: Ein Link auf 'Feed:rss' wird als 'admin/feed/rss' erzeugt -$router->addRoute('admin//', 'Admin:default'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// GUT -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('admin//', 'Admin:default'); -``` - -Wir werden Ihnen nicht verheimlichen, dass das korrekte Zusammenstellen der Routes eine gewisse Fertigkeit erfordert. Bis Sie diese beherrschen, wird Ihnen das [Routing-Panel |#Debugging des Routers] ein nützlicher Helfer sein. - - -Maske und Parameter -------------------- - -Die Maske beschreibt den relativen Pfad vom Wurzelverzeichnis der Website. Die einfachste Maske ist eine statische URL: - -```php -$router->addRoute('products', 'Products:default'); -``` - -Häufig enthalten Masken sogenannte **Parameter**. Diese werden in spitzen Klammern angegeben (z. B. ``) und an den Ziel-Presenter übergeben, zum Beispiel an die Methode `renderShow(int $year)` oder an den persistenten Parameter `$year`: - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -Das Beispiel besagt: Öffnen wir im Browser `https://example.com/chronicle/2020`, wird der Presenter `History` mit der Aktion `show` und dem Parameter `year: 2020` angezeigt. - -Parametern können wir direkt in der Maske einen Standardwert zuweisen, wodurch sie optional werden: - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -Die Route akzeptiert nun auch die URL `https://example.com/chronicle/`, die wiederum `History:show` mit dem Parameter `year: 2020` anzeigt. - -Parameter können natürlich auch der Name des Presenters und der Aktion sein. Zum Beispiel so: - -```php -$router->addRoute('/', 'Home:default'); -``` - -Die angegebene Route akzeptiert z. B. URLs in der Form `/article/edit` oder auch `/catalog/list` und versteht sie als die Presenter und Aktionen `Article:edit` bzw. `Catalog:list`. - -Zugleich gibt sie den Parametern `presenter` und `action` die Standardwerte `Home` und `default`, wodurch auch sie optional sind. Die Route akzeptiert also auch eine URL in der Form `/article` und versteht sie als `Article:default`. Oder umgekehrt: Ein Link auf `Product:default` erzeugt den Pfad `/product`, ein Link auf den Standard `Home:default` den Pfad `/`. - -Die Maske kann nicht nur den relativen Pfad vom Wurzelverzeichnis der Website beschreiben, sondern auch einen absoluten Pfad, wenn sie mit einem Schrägstrich beginnt, oder sogar eine ganze absolute URL, wenn sie mit zwei Schrägstrichen beginnt: - -```php -// relativ zum Document-Root -$router->addRoute('/', /* ... */); - -// absoluter Pfad (relativ zur Domain) -$router->addRoute('//', /* ... */); - -// absolute URL inklusive Domain (relativ zum Schema) -$router->addRoute('//.example.com//', /* ... */); - -// absolute URL inklusive Schema -$router->addRoute('https://.example.com//', /* ... */); -``` - - -Validierungsausdrücke ---------------------- - -Für jeden Parameter lässt sich eine Validierungsbedingung mit einem [regulären Ausdruck|https://www.php.net/manual/en/reference.pcre.pattern.syntax.php] angeben. Für den Parameter `id` legen wir zum Beispiel mit dem Regex `\d+` fest, dass er nur Ziffern enthalten darf: - -```php -$router->addRoute('/[/]', /* ... */); -``` - -Der Standard-Regex für alle Parameter ist `[^/]+`, also alles außer einem Schrägstrich. Soll ein Parameter auch Schrägstriche akzeptieren, setzen wir den Ausdruck auf `.+`: - -```php -// akzeptiert https://example.com/a/b/c, path ist dann 'a/b/c' -$router->addRoute('', /* ... */); -``` - - -Optionale Sequenzen -------------------- - -In der Maske lassen sich optionale Teile mit eckigen Klammern kennzeichnen. Jeder Teil der Maske kann optional sein und Parameter enthalten: - -```php -$router->addRoute('[/]', /* ... */); - -// Akzeptiert die Pfade: -// /en/download => lang => en, name => download -// /download => lang => null, name => download -``` - -Ist ein Parameter Teil einer optionalen Sequenz, wird natürlich auch er optional. Hat er keinen angegebenen Standardwert, ist er null. - -Optionale Teile können auch in der Domain vorkommen: - -```php -$router->addRoute('//[.]example.com//', /* ... */); -``` - -Sequenzen lassen sich beliebig verschachteln und kombinieren: - -```php -$router->addRoute( - '[[-]/][/page-]', - 'Home:default', -); - -// Akzeptiert die Pfade: -// /en/hello -// /en-us/hello -// /hello -// /hello/page-12 -``` - -Beim Erzeugen von URLs wird die kürzeste Variante bevorzugt, es wird also alles weggelassen, was weggelassen werden kann. Deshalb erzeugt zum Beispiel die Route `index[.html]` den Pfad `/index`. Dieses Verhalten lässt sich umkehren, indem man hinter die linke eckige Klammer ein Ausrufezeichen setzt: - -```php -// akzeptiert /hello und /hello.html, erzeugt /hello -$router->addRoute('[.html]', /* ... */); - -// akzeptiert /hello und /hello.html, erzeugt /hello.html -$router->addRoute('[!.html]', /* ... */); -``` - -Optionale Parameter (also Parameter mit einem Standardwert) ohne eckige Klammern verhalten sich im Grunde so, als wären sie folgendermaßen eingeklammert: - -```php -$router->addRoute('//', /* ... */); - -// entspricht diesem: -$router->addRoute('[/[/[]]]', /* ... */); -``` - -Wollen wir das Verhalten des abschließenden Schrägstrichs beeinflussen, damit zum Beispiel `/home` statt `/home/` erzeugt wird, erreichen wir das so: - -```php -$router->addRoute('[[/[/]]]', /* ... */); -``` - - -Wildcards ---------- - -In der Maske einer absoluten URL können wir die folgenden Wildcards verwenden, um zum Beispiel die Domain nicht in die Maske schreiben zu müssen, die sich zwischen Entwicklungs- und Produktionsumgebung unterscheiden kann: - -- `%tld%` = Top-Level-Domain, z. B. `com` oder `org` -- `%sld%` = Second-Level-Domain, z. B. `example` -- `%domain%` = Domain ohne Subdomains, z. B. `example.com` -- `%host%` = gesamter Host, z. B. `www.example.com` -- `%basePath%` = Pfad zum Wurzelverzeichnis - -```php -$router->addRoute('//www.%domain%/%basePath%//', /* ... */); -$router->addRoute('//www.%sld%.%tld%/%basePath%//', /* ... */); -``` - - -Erweiterte Schreibweise ------------------------ - -Das Ziel der Route, üblicherweise im Format `Presenter:action` geschrieben, lässt sich auch mit einem Array angeben, das die einzelnen Parameter und ihre Standardwerte definiert: - -```php -$router->addRoute('/[/]', [ - 'presenter' => 'Home', - 'action' => 'default', -]); -``` - -Für eine genauere Angabe lässt sich eine noch ausführlichere Form verwenden, in der wir neben Standardwerten auch weitere Eigenschaften der Parameter setzen können, etwa einen regulären Ausdruck zur Validierung (siehe den Parameter `id`): - -```php -use Nette\Routing\Route; - -$router->addRoute('/[/]', [ - 'presenter' => [ - Route::Value => 'Home', - ], - 'action' => [ - Route::Value => 'default', - ], - 'id' => [ - Route::Pattern => '\d+', - ], -]); -``` - -Wichtig ist: Sind im Array definierte Parameter nicht in der Pfadmaske aufgeführt, lassen sich ihre Werte nicht ändern, auch nicht über Query-Parameter, die in der URL hinter dem Fragezeichen angegeben werden. - -Das ist nützlich für **feste Parameter** - um einer bestimmten Seite eine kurze, einprägsame URL zu geben. Damit zum Beispiel `/tos` immer `Article:view` mit `id: 123` öffnet: - -```php -$router->addRoute('tos', [ - 'presenter' => 'Article', - 'action' => 'view', - 'id' => 123, -]); -``` - - -Filter und Übersetzungen ------------------------- - -Den Quellcode der Anwendung schreiben wir auf Englisch, wenn die Website aber tschechische URLs haben soll, dann erzeugt ein einfaches Routing wie: - -```php -$router->addRoute('/', 'Home:default'); -``` - -englische URLs, etwa `/product/123` oder `/cart`. Wollen wir, dass Presenter und Aktionen in der URL durch tschechische Wörter dargestellt werden (z. B. `/produkt/123` oder `/kosik`), können wir ein Übersetzungswörterbuch verwenden. Um es zu schreiben, brauchen wir bereits die "gesprächigere" Variante des zweiten Parameters: - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterTable => [ - // String in der URL => Presenter - 'produkt' => 'Product', - 'kosik' => 'Cart', - 'katalog' => 'Catalog', - ], - ], - 'action' => [ - Route::Value => 'default', - Route::FilterTable => [ - 'seznam' => 'list', - ], - ], -]); -``` - -Mehrere Schlüssel im Übersetzungswörterbuch können auf denselben Presenter führen. So entstehen für ihn verschiedene Aliase. Der letzte Schlüssel gilt als die kanonische Variante (also die, die in der erzeugten URL steht). - -Die Übersetzungstabelle lässt sich auf diese Weise für jeden Parameter verwenden. Existiert keine Übersetzung, wird der ursprüngliche Wert genommen. Dieses Verhalten können wir mit `Route::FilterStrict => true` ändern, die Route weist die URL dann zurück, wenn der Wert nicht im Wörterbuch steht. - -Neben dem Übersetzungswörterbuch in Form eines Arrays lassen sich eigene Übersetzungsfunktionen einsetzen. - -```php -use Nette\Routing\Route; - -$router->addRoute('//', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterIn => function (string $s): string { /* ... */ }, - Route::FilterOut => function (string $s): string { /* ... */ }, - ], - 'action' => 'default', - 'id' => null, -]); -``` - -Die Funktion `Route::FilterIn` wandelt zwischen dem Parameter in der URL und dem String um, der dann an den Presenter übergeben wird; die Funktion `FilterOut` sorgt für die Umwandlung in die Gegenrichtung. - -Die Parameter `presenter`, `action` und `module` haben bereits vordefinierte Filter, die zwischen dem PascalCase- bzw. camelCase-Stil und dem in URLs verwendeten kebab-case umwandeln. Der Standardwert der Parameter wird in der Form geschrieben, in der er an die Anwendung übergeben wird (PascalCase bei Presenter und Modul, camelCase bei der Aktion), im Fall eines Presenters schreiben wir also ``, nicht ``. - - -Allgemeine Filter ------------------ - -Neben Filtern für konkrete Parameter können wir auch allgemeine Filter definieren, die ein assoziatives Array aller Parameter erhalten, das sie beliebig verändern und dann zurückgeben können. Allgemeine Filter werden unter dem leeren Schlüssel definiert. - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => 'Home', - 'action' => 'default', - '' => [ - Route::FilterIn => function (array $params): array { /* ... */ }, - Route::FilterOut => function (array $params): array { /* ... */ }, - ], -]); -``` - -Allgemeine Filter bieten die Möglichkeit, das Verhalten der Route auf absolut beliebige Weise zu verändern. Wir können sie zum Beispiel nutzen, um Parameter anhand anderer Parameter zu verändern. Etwa `` und `` anhand des aktuellen Werts des Parameters `` zu übersetzen. - -Hat ein Parameter einen eigenen Filter definiert und existiert zugleich ein allgemeiner Filter, wird das eigene `FilterIn` vor dem allgemeinen ausgeführt und umgekehrt das allgemeine `FilterOut` vor dem eigenen. Innerhalb des allgemeinen Filters sind die Werte der Parameter `presenter` und `action` also im PascalCase- bzw. camelCase-Stil geschrieben. - -Eine praktische Anwendung dieser Filter - das Erzeugen SEO-freundlicher URLs wie `/article/123-how-to-bake-bread` ohne jede Änderung an den Templates - finden Sie unter [Schöne URLs mit Slugs |best-practices:pretty-urls]. - - -Flag OneWay ------------ - -Einwegrouten dienen dazu, die Funktionsfähigkeit alter URLs zu erhalten, die die Anwendung nicht mehr erzeugt, aber weiterhin akzeptiert. Wir kennzeichnen sie mit dem Flag `OneWay`: - -```php -// alte URL /product-info?id=123 -$router->addRoute('product-info', 'Product:detail', oneWay: true); -// neue URL /product/123 -$router->addRoute('product/', 'Product:detail'); -``` - -Beim Aufruf der alten URL leitet der Presenter automatisch auf die neue URL weiter, damit Suchmaschinen diese Seiten nicht doppelt indexieren (siehe [#SEO und Kanonisierung]). - - -Dynamisches Routing mit Callbacks ---------------------------------- - -Dynamisches Routing mit Callbacks erlaubt es, Routes direkt Funktionen (Callbacks) zuzuweisen, die beim Aufruf des jeweiligen Pfads ausgeführt werden. Diese flexible Funktionalität ermöglicht es, schnell und effizient verschiedene Endpunkte für Ihre Anwendung zu erstellen: - -```php -$router->addRoute('test', function () { - echo 'Sie befinden sich auf der Adresse /test'; -}); -``` - -In der Maske können Sie auch Parameter definieren, die automatisch an Ihren Callback übergeben werden: - -```php -$router->addRoute('', function (string $lang) { - echo match ($lang) { - 'cs' => 'Willkommen in der tschechischen Version unserer Website!', - 'en' => 'Willkommen in der englischen Version unserer Website!', - }; -}); -``` - -Neben den Parametern aus der Maske kann der Callback auch Services aus dem DI-Container erhalten. Sie werden anhand des Parametertyps übergeben. Zusätzlich erhält der Parameter `$presenter` eine Instanz von [MicroPresenter |api:NetteModule\MicroPresenter], die die Route verarbeitet: - -```php -$router->addRoute('', function (string $lang, Nette\Http\Request $httpRequest, NetteModule\MicroPresenter $presenter) { - // ... -}); -``` - - -Module ------- - -Haben wir mehrere Routes, die zu einem gemeinsamen [Modul |directory-structure#Presenter und Templates] gehören, verwenden wir `withModule()`. Das angegebene Modul wird dem Presenter jeder Route in der Gruppe automatisch vorangestellt und verschwindet vollständig aus der URL: - -```php -$router = new RouteList; -$router->withModule('Forum') // die folgenden Routes sind Teil des Moduls Forum - ->addRoute('rss', 'Feed:rss') // Presenter ist Forum:Feed - ->addRoute('/') - - ->withModule('Admin') // die folgenden Routes sind Teil des Moduls Forum:Admin - ->addRoute('sign:in', 'Sign:in'); -``` - -Eine Alternative ist der Parameter `module`, der ebenfalls ein festes Modul setzt und es aus der URL heraushält: - -```php -// URL manage/dashboard/default wird auf den Presenter Admin:Dashboard abgebildet -$router->addRoute('manage//', [ - 'module' => 'Admin', -]); -``` - -Der Name eines Presenters ist erst zusammen mit seinem Modul vollständig, z. B. `Front:Admin:ProductList`. Immer wenn ein solcher vollständiger Name in einem URL-Parameter landet, kodiert ihn der Router nach zwei einfachen Regeln: Jeder Doppelpunkt `:` (der Modultrenner) wird zu einem **Punkt**, und jede Wortgrenze in einem PascalCase-Namen wird zu einem **Bindestrich**. `Front:Admin:ProductList` erscheint in der URL also als `front.admin.product-list` und wird genauso wieder dekodiert. Genau deshalb erzeugt eine modulare Anwendung ohne eines der obigen Werkzeuge URLs voller Punkte. - -Sowohl `withModule()` als auch der Parameter `module` vermeiden das gerade dadurch, dass sie den bekannten Modulpräfix vom Presenter-Namen abschneiden, bevor er in die URL gelangt: Da das Modul eine Konstante ist, muss es überhaupt nicht kodiert werden. - -Manchmal wollen wir, dass sich das Modul selbst ändert und in der URL erscheint, und greifen daher direkt in der Maske zu ``. Achten Sie dabei auf eine entscheidende Kleinigkeit: **`` verschlingt den gesamten Modulpfad** - alles bis zum letzten Doppelpunkt im Presenter-Namen. Beim Presenter `Shop:Admin:Product` ist das also das Modul `Shop:Admin` und der Presenter `Product`, und weil Doppelpunkte zu Punkten werden, erhalten wir: - - -Subdomains ----------- - -Routensammlungen lassen sich nach Subdomains aufteilen: - -```php -$router = new RouteList; -$router->withDomain('example.com') - ->addRoute('rss', 'Feed:rss') - ->addRoute('/'); -``` - -Im Domainnamen lassen sich auch [#Wildcards] verwenden: - -```php -$router = new RouteList; -$router->withDomain('example.%tld%') - // ... -``` - - -Pfadpräfix ----------- - -Routensammlungen lassen sich nach dem Pfad in der URL aufteilen: - -```php -$router = new RouteList; -$router->withPath('eshop') - ->addRoute('rss', 'Feed:rss') // passt auf die URL /eshop/rss - ->addRoute('/'); // passt auf die URL /eshop// -``` - - -Kombinationen -------------- - -Die obigen Gruppierungen lassen sich miteinander kombinieren: - -```php -$router = (new RouteList) - ->withDomain('admin.example.com') - ->withModule('Admin') - ->addRoute(/* ... */) - ->addRoute(/* ... */) - ->end() - ->withModule('Images') - ->addRoute(/* ... */) - ->end() - ->end() - ->withDomain('example.com') - ->withPath('export') - ->addRoute(/* ... */) - // ... -``` - - -Query-Parameter ---------------- - -Masken können auch Query-Parameter enthalten (Parameter hinter dem Fragezeichen in der URL). Für sie lässt sich kein Validierungsausdruck definieren, wohl aber der Name ändern, unter dem sie an den Presenter übergeben werden: - -```php -// wir wollen den Query-Parameter 'cat' in der Anwendung unter dem Namen 'categoryId' verwenden -$router->addRoute('product ? id= & cat=', /* ... */); -``` - - -Foo-Parameter -------------- - -Jetzt gehen wir tiefer. Foo-Parameter sind im Grunde unbenannte Parameter, die es erlauben, einen regulären Ausdruck abzugleichen. Ein Beispiel ist eine Route, die `/index`, `/index.html`, `/index.htm` und `/index.php` akzeptiert: - -```php -$router->addRoute('index', /* ... */); -``` - -Es lässt sich auch ausdrücklich der String festlegen, der beim Erzeugen der URL verwendet wird. Der String muss direkt hinter dem Fragezeichen stehen. Die folgende Route ähnelt der vorherigen, erzeugt aber `/index.html` statt `/index`, weil der String `.html` als Wert für die Erzeugung gesetzt ist: - -```php -$router->addRoute('index', /* ... */); -``` - - -Integration -=========== - -Um den erstellten Router in die Anwendung einzubinden, müssen wir dem DI-Container von ihm erzählen. Am einfachsten ist es, eine Factory vorzubereiten, die das Router-Objekt erzeugt, und dem Container in der Konfiguration zu sagen, dass er sie verwenden soll. Nehmen wir an, wir schreiben dafür die Methode `App\Core\RouterFactory::createRouter()`: - -```php -namespace App\Core; - -use Nette\Application\Routers\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute(/* ... */); - return $router; - } -} -``` - -Dann schreiben wir in die [Konfiguration |dependency-injection:services]: - -```neon -services: - - App\Core\RouterFactory::createRouter -``` - -Eventuelle Abhängigkeiten, etwa von einer Datenbank usw., werden der Factory-Methode über [Autowiring|dependency-injection:autowiring] als ihre Parameter übergeben: - -```php -public static function createRouter(Nette\Database\Connection $db): RouteList -{ - // ... -} -``` - - -SimpleRouter -============ - -Ein weitaus einfacherer Router als die Routensammlung ist der [SimpleRouter |api:Nette\Application\Routers\SimpleRouter]. Wir verwenden ihn, wenn wir keine besonderen Anforderungen an die Form der URL haben, wenn `mod_rewrite` (oder seine Alternativen) nicht verfügbar ist oder wenn wir uns mit pretty URLs noch nicht befassen wollen. - -Er erzeugt Adressen ungefähr in dieser Form: - -``` -http://example.com/?presenter=Product&action=detail&id=123 -``` - -Der Parameter des Konstruktors von `SimpleRouter` ist der Standard-Presenter & die Standardaktion, also die Aktion, die ausgeführt wird, wenn wir z. B. `http://example.com/` ohne weitere Parameter öffnen. - -```php -// der Standard-Presenter wird 'Home' und die Aktion 'default' sein -$router = new Nette\Application\Routers\SimpleRouter('Home:default'); -``` - -Wir empfehlen, den SimpleRouter direkt in der [Konfiguration |dependency-injection:services] zu definieren: - -```neon -services: - - Nette\Application\Routers\SimpleRouter('Home:default') -``` - - -SEO und Kanonisierung -===================== - -Das Framework trägt zum SEO (Search Engine Optimization) bei, indem es verhindert, dass derselbe Inhalt unter verschiedenen URLs existiert. Führen mehrere Adressen zu einem bestimmten Ziel, z. B. `/index` und `/index.html`, bestimmt das Framework die erste als primär (kanonisch) und leitet die übrigen mit dem HTTP-Code 301 dorthin weiter. Dadurch indexieren Suchmaschinen die Seiten nicht doppelt und verwässern deren Page Rank nicht. - -Dieser Vorgang heißt Kanonisierung. Die kanonische URL ist diejenige, die der Router erzeugt, also die erste passende Route in der Collection ohne das Flag OneWay. Deshalb führen wir in der Collection die **primären Routes zuerst** auf. - -Die Kanonisierung führt der Presenter durch, mehr im Kapitel [Kanonisierung |presenters#Kanonisierung]. - - -HTTPS -===== - -Um das Protokoll HTTPS zu verwenden, muss es beim Hosting aktiviert und der Server korrekt konfiguriert sein. - -Die Weiterleitung der gesamten Website auf HTTPS muss auf Serverebene eingerichtet werden, zum Beispiel über die Datei `.htaccess` im Wurzelverzeichnis unserer Anwendung, mit dem HTTP-Code 301. Die Einstellung kann sich je nach Hosting unterscheiden und sieht ungefähr so aus: - -``` - - RewriteEngine On - ... - RewriteCond %{HTTPS} off - RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] - ... - -``` - -Der Router erzeugt URLs mit demselben Protokoll, mit dem die Seite geladen wurde, es muss also nichts weiter eingestellt werden. - -Brauchen wir jedoch ausnahmsweise, dass verschiedene Routes unter verschiedenen Protokollen laufen, geben wir das in der Maske der Route an: - -```php -// Erzeugt eine HTTP-Adresse -$router->addRoute('http://%host%//', /* ... */); - -// Erzeugt eine HTTPS-Adresse -$router->addRoute('https://%host%//', /* ... */); -``` - - -Debugging des Routers -===================== - -Das in der [Tracy Bar |tracy:] angezeigte Routing-Panel ist ein nützlicher Helfer, der eine Liste der Routes anzeigt und außerdem die Parameter, die der Router aus der URL gewonnen hat. - -Der grüne Balken mit dem Symbol ✓ stellt die Route dar, die die aktuelle URL verarbeitet hat; blaue Farbe und das Symbol ≈ kennzeichnen Routes, die die URL ebenfalls verarbeitet hätten, wenn ihnen die grüne nicht zuvorgekommen wäre. Weiter sehen wir den aktuellen Presenter & die aktuelle Aktion. - -[* routing-debugger.webp *] - -Kommt es zugleich wegen der [Kanonisierung |#SEO und Kanonisierung] zu einer unerwarteten Weiterleitung, lohnt sich ein Blick in den Balken *redirect* im Panel, wo Sie herausfinden, wie der Router die URL ursprünglich verstanden hat und warum er weitergeleitet hat. - -.[note] -Beim Debuggen des Routers empfehlen wir, im Browser die Developer Tools zu öffnen (Strg+Umschalt+I oder Cmd+Option+I) und im Panel Network den Cache abzuschalten, damit die Weiterleitungen nicht darin gespeichert werden. - - -Leistung -======== - -Die Anzahl der Routes beeinflusst die Geschwindigkeit des Routers. Ihre Anzahl sollte auf keinen Fall einige Dutzend überschreiten. Hat Ihre Website eine zu komplizierte URL-Struktur, können Sie einen [#Eigener Router] schreiben. - -Hat der Router keine Abhängigkeiten, etwa von einer Datenbank, und nimmt seine Factory keine Argumente entgegen, können wir seine kompilierte Form direkt in den DI-Container serialisieren und die Anwendung damit etwas beschleunigen. - -```neon -routing: - cache: true -``` - - -Eigener Router -============== - -Die folgenden Zeilen sind für sehr fortgeschrittene Benutzer gedacht. Sie können einen eigenen Router erstellen und ihn ganz natürlich in die Routensammlung einbinden. Der Router ist eine Implementierung des Interfaces [api:Nette\Routing\Router] mit zwei Methoden: - -```php -use Nette\Http\IRequest as HttpRequest; -use Nette\Http\UrlScript; - -class MyRouter implements Nette\Routing\Router -{ - public function match(HttpRequest $httpRequest): ?array - { - // ... - } - - public function constructUrl(array $params, UrlScript $refUrl): ?string - { - // ... - } -} -``` - -Die Methode `match` verarbeitet den aktuellen Request [$httpRequest |http:request], aus dem sich nicht nur die URL, sondern auch Header usw. gewinnen lassen, in ein Array mit dem Namen des Presenters und seinen Parametern. Kann sie den Request nicht verarbeiten, gibt sie null zurück. Bei der Verarbeitung des Requests müssen wir mindestens den Presenter zurückgeben; die Aktion ist optional und ist standardmäßig `default`, wenn sie nicht angegeben wird. Der Name des Presenters ist vollständig und enthält eventuelle Module: - -```php -[ - 'presenter' => 'Front:Home', - 'action' => 'default', -] -``` - -Die Methode `constructUrl` konstruiert umgekehrt aus dem Array der Parameter die resultierende absolute URL. Sie kann dabei Informationen aus dem Parameter [`$refUrl`|api:Nette\Http\UrlScript] nutzen, der die aktuelle URL ist. - -Zur Routensammlung fügen Sie ihn mit `add()` hinzu: - -```php -$router = new Nette\Application\Routers\RouteList; -$router->add($myRouter); -$router->addRoute(/* ... */); -// ... -``` - - -Eigenständige Verwendung -======================== - -Mit eigenständiger Verwendung meinen wir den Einsatz der Fähigkeiten des Routers in einer Anwendung, die Nette Application und Presenter nicht verwendet. Fast alles, was wir in diesem Kapitel gezeigt haben, gilt auch dafür, mit diesen Unterschieden: - -- für Routensammlungen verwenden wir die Klasse [api:Nette\Routing\RouteList] -- als einfachen Router die Klasse [api:Nette\Routing\SimpleRouter] -- weil das Paar `Presenter:action` nicht existiert, verwenden wir die [#Erweiterte Schreibweise] - -Wir erstellen also wieder eine Methode, die uns den Router zusammenbaut, z. B.: - -```php -namespace App\Core; - -use Nette\Routing\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute('rss.xml', [ - 'controller' => 'RssFeedController', - ]); - $router->addRoute('article/', [ - 'controller' => 'ArticleController', - ]); - // ... - return $router; - } -} -``` - -Wenn Sie einen DI-Container verwenden, was wir empfehlen, fügen Sie die Methode wieder in die Konfiguration ein und holen sich dann den Router samt HTTP-Request aus dem Container: - -```php -$router = $container->getByType(Nette\Routing\Router::class); -$httpRequest = $container->getByType(Nette\Http\IRequest::class); -``` - -Oder erzeugen Sie die Objekte direkt: - -```php -$router = App\Core\RouterFactory::createRouter(); -$httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); -``` - -Nun bleibt nur noch, den Router seine Arbeit tun zu lassen: - -```php -$params = $router->match($httpRequest); -if ($params === null) { - // keine passende Route gefunden, Fehler 404 senden - exit; -} - -// die gewonnenen Parameter verarbeiten -$controller = $params['controller']; -// ... -``` - -Und umgekehrt den Router zum Konstruieren eines Links verwenden: - -```php -$params = ['controller' => 'ArticleController', 'id' => 123]; -$url = $router->constructUrl($params, $httpRequest->getUrl()); -``` - - -{{composer: nette/routing}} -{{repo: nette/routing}} -{{api: https://api.nette.org/routing/}} diff --git a/application/de/templates.texy b/application/de/templates.texy deleted file mode 100644 index 5b4d74fdc4..0000000000 --- a/application/de/templates.texy +++ /dev/null @@ -1,419 +0,0 @@ -Templates -********* - -.[perex] -Nette verwendet das Templating-System [Latte |latte:]. Latte wird verwendet, weil es das sicherste Templating-System für PHP ist und zugleich das intuitivste. Sie müssen nicht viel Neues lernen; Kenntnisse von PHP und einigen wenigen Tags genügen. - -Üblicherweise setzt sich eine Seite aus einem Layout-Template und dem Template der konkreten Aktion zusammen. So kann ein Layout-Template aussehen; beachten Sie die Blöcke `{block}` und den Tag `{include}`: - -```latte - - - - {block title}Meine App{/block} - - -
    ...
    - {include content} -
    ...
    - - -``` - -Und so würde das Template der Aktion aussehen: - -```latte -{block title}Startseite{/block} - -{block content} -

    Startseite

    -... -{/block} -``` - -Es definiert den Block `content`, der im Layout anstelle von `{include content}` eingefügt wird, und definiert außerdem den Block `title` neu, der `{block title}` im Layout überschreibt. Versuchen Sie sich das Ergebnis vorzustellen. - - -Suche nach Templates --------------------- - -In Presentern müssen Sie nicht angeben, welches Template gerendert werden soll; das Framework leitet den Pfad selbst ab und erspart Ihnen das Schreiben. - -Wenn Sie eine Verzeichnisstruktur verwenden, bei der jeder Presenter sein eigenes Verzeichnis hat, legen Sie das Template einfach in dieses Verzeichnis unter dem Namen der Aktion (also des Views). Für die Aktion `default` verwenden Sie zum Beispiel das Template `default.latte`: - -/--pre -app/ -└── Presentation/ - └── Home/ - ├── HomePresenter.php - └── default.latte -\-- - -Wenn Sie eine Struktur verwenden, bei der die Presenter gemeinsam in einem Verzeichnis liegen und die Templates im Ordner `templates`, speichern Sie es entweder in der Datei `..latte` oder `/.latte`: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── Home/ - │ └── default.latte ← 1. Variante - └── Home.default.latte ← 2. Variante -\-- - -Das Verzeichnis `templates` kann auch eine Ebene höher liegen, also auf derselben Ebene wie das Verzeichnis mit den Presenter-Klassen. - -Wird das Template nicht gefunden, antwortet der Presenter mit dem [Fehler 404 - Seite nicht gefunden |presenters#Fehler 404 usw.]. - -Den View ändern Sie mit `$this->setView('otherView')`. Es lässt sich auch direkt die Template-Datei mit `$this->template->setFile('/path/to/template.latte')` angeben. - -.[note] -Die Dateien, in denen nach Templates gesucht wird, lassen sich durch Überschreiben der Methode [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()] ändern, die ein Array möglicher Dateinamen zurückgibt. - - -Suche nach dem Layout-Template ------------------------------- - -Nette sucht auch die Layout-Datei automatisch. - -Wenn Sie eine Verzeichnisstruktur verwenden, bei der jeder Presenter sein eigenes Verzeichnis hat, legen Sie das Layout entweder in den Ordner mit dem Presenter, falls es nur für ihn gilt, oder eine Ebene höher, falls es mehreren Presentern gemeinsam ist: - -/--pre -app/ -└── Presentation/ - ├── @layout.latte ← gemeinsames Layout - └── Home/ - ├── @layout.latte ← nur für den Presenter Home - ├── HomePresenter.php - └── default.latte -\-- - -Wenn Sie eine Struktur verwenden, bei der die Presenter gemeinsam in einem Verzeichnis liegen und die Templates im Ordner `templates`, wird das Layout an diesen Orten erwartet: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── @layout.latte ← gemeinsames Layout - ├── Home/ - │ └── @layout.latte ← nur für Home, 1. Variante - └── Home.@layout.latte ← nur für Home, 2. Variante -\-- - -Liegt der Presenter in einem Modul, wird entsprechend der Verschachtelung der Module auch in den höheren Verzeichnisebenen gesucht. - -Den Namen des Layouts ändern Sie mit `$this->setLayout('layoutAdmin')`, es wird dann in der Datei `@layoutAdmin.latte` erwartet. Sie können die Datei des Layout-Templates auch direkt mit `$this->setLayout('/path/to/template.latte')` angeben. - -Mit `$this->setLayout(false)` oder dem Tag `{layout none}` im Template wird die Suche nach dem Layout abgeschaltet. - -.[note] -Die Dateien, in denen nach Layout-Templates gesucht wird, lassen sich durch Überschreiben der Methode [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()] ändern, die ein Array möglicher Dateinamen zurückgibt. - - -Variablen im Template ---------------------- - -Variablen übergibt man an Templates, indem man sie in `$this->template` schreibt. Im Template stehen sie dann als lokale Variablen zur Verfügung: - -```php -$this->template->article = $this->articles->getById($id); -``` - -Um den Wert einer Property automatisch als Variable an das Template zu übergeben, kennzeichnen Sie sie mit dem Attribut `#[TemplateVariable]` und public-Sichtbarkeit: .{data-version:3.2.9} - -```php -use Nette\Application\Attributes\TemplateVariable; - -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - #[TemplateVariable] - public string $siteName = 'Mein Blog'; -} -``` - -Übergeben Sie dem Template eine Variable gleichen Namens, überschreibt `#[TemplateVariable]` sie nicht. - - -Standardvariablen ------------------ - -Presenter und Komponenten übergeben Templates automatisch mehrere nützliche Variablen: - -- `$basePath` ist der absolute URL-Pfad zum Wurzelverzeichnis (z. B. `/eshop`) -- `$baseUrl` ist die absolute URL zum Wurzelverzeichnis (z. B. `http://localhost/eshop`) -- `$user` ist ein Objekt, das [den Benutzer repräsentiert |security:authentication] -- `$presenter` ist der aktuelle Presenter -- `$control` ist die aktuelle Komponente oder der Presenter -- `$flashes` ist ein Array von [Meldungen |presenters#Flash-Meldungen], die mit der Funktion `flashMessage()` gesendet wurden - -Wenn Sie eine eigene Template-Klasse verwenden, werden diese Variablen übergeben, sofern Sie eine Property dafür anlegen. - - -Typsichere Templates --------------------- - -Bei der Entwicklung robuster Anwendungen ist es nützlich, ausdrücklich festzulegen, welche Variablen das Template erwartet und welche Typen sie haben. Das bringt Typprüfung in PHP, intelligente Hinweise in Ihrer IDE und ermöglicht der statischen Analyse, Fehler aufzudecken. - -Wie definiert man eine solche Liste? Einfach als Klasse mit Properties, die die Variablen des Templates darstellen. Benennen Sie sie ähnlich wie den Presenter, nur mit `Template` am Ende: - -```php -/** - * @property-read ArticleTemplate $template - */ -class ArticlePresenter extends Nette\Application\UI\Presenter -{ -} - -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - public Model\Article $article; - public Nette\Security\User $user; - - // und weitere Variablen -} -``` - -Das Objekt `$this->template` im Presenter ist nun eine Instanz der Klasse `ArticleTemplate`. PHP prüft dadurch beim Schreiben die deklarierten Typen. - -Nette wählt die Template-Klasse automatisch. Zuerst sucht es eine Klasse namens `Template`, z. B. `ArticleEditTemplate` für die Aktion `edit`, und erst wenn diese nicht existiert, greift es auf `Template` zurück. - -Die Annotation `@property-read` ist für die IDE und die statische Analyse gedacht und ermöglicht Code-Vervollständigung, siehe "PhpStorm und Code-Vervollständigung für $this⁠-⁠>⁠template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template. - -[* phpstorm-completion.webp *] - -Code-Vervollständigung lässt sich auch direkt in Templates nutzen. Installieren Sie einfach das Latte-Plugin für PhpStorm und geben Sie am Anfang des Templates den Klassennamen der Template-Parameter an, mehr im Kapitel [Latte: Typsystem |latte:type-system]: - -```latte -{templateType App\Presentation\Article\ArticleTemplate} -... -``` - -Dasselbe gilt für Komponenten. Halten Sie sich einfach an die Namenskonvention und legen Sie für eine Komponente wie `FifteenControl` eine Parameterklasse `FifteenTemplate` an. - -Wenn Sie eine andere Parameterklasse verwenden müssen, nutzen Sie die Methode `createTemplate()`: - -```php -public function renderDefault(): void -{ - $template = $this->createTemplate(SpecialTemplate::class); - $template->foo = 123; - // ... - $this->sendTemplate($template); -} -``` - -.{data-version:3.3.0} -Wenn Sie beeinflussen wollen, wie das Template vor dem Rendern fertiggestellt wird - zum Beispiel um Variablen zu ergänzen, die alle Aktionen gemeinsam haben -, können Sie im Presenter die Methode `completeTemplate()` überschreiben. Sie wird unmittelbar vor dem Rendern des Templates aufgerufen: - -```php -protected function completeTemplate(Nette\Application\UI\Template $template): void -{ - parent::completeTemplate($template); - $template->siteName = 'Mein Blog'; -} -``` - - -Links erstellen ---------------- - -Im Template werden Links auf andere Presenter & Aktionen so erstellt: - -```latte -Produktdetail -``` - -Das Attribut `n:href` ist für HTML-Tags `` sehr praktisch. Wollen wir den Link anderswo ausgeben, zum Beispiel im Text, verwenden wir `{link}`: - -```latte -Die URL lautet: {link Home:default} -``` - -Mehr dazu finden Sie im Kapitel [Erstellen von URL-Links|creating-links]. - - -Eigene Filter, Tags usw. ------------------------- - -Das Templating-System Latte lässt sich um eigene Filter, Funktionen, Tags und weitere Elemente erweitern. Dafür stehen drei Wege zur Verfügung, von schnellen Ad-hoc-Lösungen bis zu architektonischen Mustern für ganze Anwendungen. - -**Ad hoc in Methoden des Presenters** - -Der schnellste Weg ist, Filter oder Funktionen direkt im Code des Presenters oder der Komponente zu ergänzen. In Presentern eignen sich dafür die Methoden `beforeRender()` oder `render()`: - -```php -protected function beforeRender(): void -{ - // einen Filter hinzufügen - $this->template->addFilter('money', fn($val) => number_format($val, 2) . ' €'); - - // eine Funktion hinzufügen - $this->template->addFunction('isWeekend', fn($date) => $date->format('N') >= 6); -} -``` - -Im Template: - -```latte -

    Preis: {$price|money}

    - -{if isWeekend($now)} ... {/if} -``` - -Für komplexere Logik können Sie das Objekt `Latte\Engine` direkt konfigurieren: - -```php -protected function beforeRender(): void -{ - $latte = $this->template->getLatte(); - $latte->setFeature(Latte\Feature::MigrationWarnings); -} -``` - -**Mit Attributen** - -Ein eleganterer Weg ist, Filter und Funktionen als Methoden direkt in der [Parameterklasse des Templates|#Typsichere Templates] des Presenters oder der Komponente zu definieren und mit Attributen zu kennzeichnen: - -```php -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - #[Latte\Attributes\TemplateFilter] - public function money(float $val): string - { - return number_format($val, 2) . ' €'; - } - - #[Latte\Attributes\TemplateFunction] - public function isWeekend(DateTimeInterface $date): bool - { - return $date->format('N') >= 6; - } -} -``` - -Latte findet und registriert die mit diesen Attributen gekennzeichneten Methoden automatisch. Der Name des Filters bzw. der Funktion im Template entspricht dem Methodennamen. Diese Methoden müssen public sein. - -**Global mit Extensions** - -Die bisherigen Wege eignen sich für Filter und Funktionen, die nur in bestimmten Presentern oder Komponenten gebraucht werden, nicht anwendungsweit. Für die gesamte Anwendung eignet sich am besten das Erstellen einer [Extension |latte:extending-latte#Latte Extension]. Diese Klasse bündelt alle Latte-Erweiterungen Ihres Projekts an einer Stelle. Ein kurzes Beispiel: - -```php -namespace App\Presentation\Accessory; - -final class LatteExtension extends Latte\Extension -{ - public function __construct( - private App\Model\Facade $facade, - private Nette\Security\User $user, - // ... - ) { - } - - public function getFilters(): array - { - return [ - 'timeAgoInWords' => $this->filterTimeAgoInWords(...), - 'money' => $this->filterMoney(...), - // ... - ]; - } - - public function getFunctions(): array - { - return [ - 'canEditArticle' => - fn($article) => $this->facade->canEditArticle($article, $this->user->getId()), - // ... - ]; - } - - private function filterTimeAgoInWords(DateTimeInterface $time): string - { - // ... - } - - // ... -} -``` - -Registrieren Sie die Extension über die [Konfiguration |configuration#Latte-Templates]: - -```neon -latte: - extensions: - - App\Presentation\Accessory\LatteExtension -``` - -Extensions bieten mehrere Vorteile: Unterstützung für Dependency Injection, Zugriff auf die Model-Schicht Ihrer Anwendung und zentrale Verwaltung aller Erweiterungen. Sie unterstützen außerdem eigene Tags, Provider, Compiler-Pässe und mehr. - - -Alle Templates einrichten -------------------------- - -Der Service `TemplateFactory`, der alle Templates erzeugt, bietet ein öffentliches Array von Callbacks `$onCreate`. Diese werden bei jedem Erzeugen eines beliebigen Templates aufgerufen, sodass Sie Filter, Funktionen oder Variablen für alle Templates der Anwendung von einer einzigen Stelle aus einrichten können. Jedes Callback erhält das neu erzeugte Template. Lassen Sie sich den Service `TemplateFactory` [übergeben |dependency-injection:passing-dependencies] und registrieren Sie die Callbacks, z. B. beim Start der Anwendung: - -```php -$templateFactory->onCreate[] = function (Nette\Bridges\ApplicationLatte\Template $template): void { - $template->addFilter('money', fn($val) => number_format($val, 2) . ' €'); -}; -``` - - -Übersetzen ----------- - -Wenn Sie eine mehrsprachige Anwendung programmieren, werden Sie manche Texte im Template in verschiedenen Sprachen ausgeben müssen. Das Nette Framework definiert dafür das Übersetzungs-Interface [api:Nette\Localization\Translator] mit der einzigen Methode `translate()`. Sie nimmt die Nachricht `$message` entgegen, die üblicherweise ein String ist, sowie beliebige weitere Parameter. Ihre Aufgabe ist es, den übersetzten String zurückzugeben. Nette enthält keine Standardimplementierung; Sie können nach Ihren Bedürfnissen aus mehreren fertigen Lösungen wählen, die auf [Componette |https://componette.org/search/localization] verfügbar sind. In deren Dokumentation erfahren Sie, wie der Translator konfiguriert wird. - -Templates lassen sich mit einem Translator einrichten, den wir uns [übergeben lassen |dependency-injection:passing-dependencies], und zwar mit der Methode `setTranslator()`: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator); -} -``` - -Alternativ lässt sich der Translator über die [Konfiguration |configuration#Latte-Templates] setzen: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -Der Translator lässt sich dann zum Beispiel als Filter `|translate` verwenden, samt weiteren Parametern, die an die Methode `translate()` übergeben werden (siehe `foo, bar`): - -```latte -
    {='Warenkorb'|translate} -{$item|translate} -{$item|translate, foo, bar} -``` - -Oder als Unterstrich-Tag: - -```latte -{_'Warenkorb'} -{_$item} -{_$item, foo, bar} -``` - -Für die Übersetzung eines Abschnitts des Templates gibt es den Paar-Tag `{translate}` (seit Latte 2.11, zuvor wurde der Tag `{_}` verwendet): - -```latte -{translate}Bestellen{/translate} -{translate foo, bar}Bestellen{/translate} -``` - -Der Translator wird normalerweise zur Laufzeit beim Rendern des Templates aufgerufen. Latte in Version 3 kann jedoch alle statischen Texte bereits während der Kompilierung des Templates übersetzen. Das spart Leistung, denn jeder String wird nur einmal übersetzt, und die entstandene Übersetzung wird in die kompilierte Form geschrieben. Im Cache-Verzeichnis entstehen dadurch mehrere kompilierte Versionen des Templates, eine für jede Sprache. Dazu genügt es, die Sprache als zweiten Parameter anzugeben: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator, $lang); -} -``` - -Statischer Text bedeutet zum Beispiel `{_'hello'}` oder `{translate}hello{/translate}`. Nicht statische Texte wie `{_$foo}` werden weiterhin zur Laufzeit übersetzt. diff --git a/application/de/upgrading.texy b/application/de/upgrading.texy deleted file mode 100644 index a3a0914585..0000000000 --- a/application/de/upgrading.texy +++ /dev/null @@ -1,47 +0,0 @@ -Upgrade -******* - - -Upgrade auf Version 3.0 -======================= - -Nette 3.0 ergänzt Typdeklarationen bei Methodenparametern und Rückgabewerten. Wenn Sie eine solche Methode in einer von Nette geerbten Klasse überschreiben (zum Beispiel in einem Presenter oder einer Komponente), müssen Sie dieselben Typdeklarationen ergänzen, sonst meldet PHP den Fehler "Declaration must be compatible". - -Das Interface `Nette\Application\IRouter` hat sich geändert. Die Methode `match()` gibt nun ein Array von Parametern zurück und `constructUrl()` nimmt eines entgegen, statt eines Objekts `Nette\Application\Request`. - -Nette prüft nun, ob jedes Signal vom selben Origin gesendet wird (also von derselben Domain und Subdomain). Diese Same-Origin-Policy ist ein kritischer Sicherheitsmechanismus, der mögliche Angriffsvektoren reduziert. Wenn Sie andere Origins zulassen wollen, ergänzen Sie bei der Handler-Methode die Annotation `@crossOrigin`: - -```php -/** - * @crossOrigin - */ -public function handleXy(): void -{ -} -``` - -Dasselbe gilt für das Absenden von Formularen. Wenn Sie das Absenden von anderen Origins zulassen wollen, gehen Sie so vor: - -```php -$form = new Nette\Application\UI\Form; -$form->allowCrossOrigin(); -``` - -Der Konstruktor von `Nette\ComponentModel\Component` wurde seit Jahren nicht mehr verwendet und in Version 3.0 entfernt. Es handelt sich um einen BC Break: Wenn Sie in einer Komponente oder einem Presenter, der von `Nette\Application\UI\Presenter` erbt, den Konstruktor des Vorfahren aufrufen, müssen Sie diesen Aufruf entfernen. - - -Upgrade auf Version 2.4 -======================= - -- `Route` und `SimpleRouter` erzeugen nun dasselbe Schema HTTP/HTTPS, über das die Website aufgerufen wurde. Eine Route, die ein bestimmtes Protokoll erfordert, lässt sich mit dem Schema definieren, z. B. `Route('http://domain.cz/')`. -- Bei Parametern der render/action-Methoden vom Typ bool (also mit dem Standardwert true oder false) und bei persistenten Parametern wird nun zwischen `false` und `null` unterschieden. Ist der Parameter nicht in der URL enthalten, ist sein Wert nun `null` (früher `false`). -- Die von `Presenter::getReflection()` zurückgegebene Klasse ist kein Nachfahre von `Nette\Reflection\ClassType` mehr, und `getReflection()->getMethod()` ist kein Nachfahre von `Nette\Reflection\Method` mehr. -- Das Flag `SECURED` und `Route::$defaultFlags` sind veraltet. - - -Upgrade auf Version 2.3 -======================= - -- Routes und Presenter-Namen sind **case-sensitive**. Nette warnt Sie, wenn Sie bei einem Presenter-Namen die falsche Groß-/Kleinschreibung verwenden; aus Performance-Gründen wird die Maske der Route nicht geprüft, überprüfen Sie sie daher selbst. -- `Route::addStyle()` und `Route::setStyleProperty()` sind veraltet und lösen nun `E_USER_DEPRECATED` aus. -- Die Template-Endung `.phtml` und die alte Link-Syntax werden nicht mehr unterstützt. diff --git a/application/en/@home.texy b/application/en/@home.texy deleted file mode 100644 index b7407ce96a..0000000000 --- a/application/en/@home.texy +++ /dev/null @@ -1,88 +0,0 @@ -Nette Application -***************** - -.[perex] -Nette Application is the core of the Nette framework, providing powerful tools for creating modern web applications. It offers a range of exceptional features that significantly simplify development and improve code security and maintainability. - - -Installation ------------- - -Download and install the library using [Composer|best-practices:composer]: - -```shell -composer require nette/application -``` - - -Why choose Nette Application? ------------------------------ - -Nette has always been a pioneer in web technologies. - -**Bidirectional Router:** Nette features an advanced routing system unique in its bidirectionality - it not only translates URLs to application actions but can also generate URLs in reverse. This means: -- You can modify the URL structure of the entire application at any time without needing to modify templates -- URLs are automatically canonicalized, improving SEO -- Routing is defined in one place, not scattered in annotations - -**Components and Signals:** The built-in component system inspired by Delphi and React.js is unique among PHP frameworks: -- Enables creating reusable UI elements -- Supports hierarchical component composition -- Offers elegant AJAX request handling using signals -- Rich library of ready-made components on [Componette](https://componette.org) - -**AJAX and Snippets:** Nette introduced a revolutionary way of working with AJAX in 2009, long before similar solutions like Hotwire for Ruby on Rails or Symfony UX Turbo: -- Snippets allow updating only parts of the page without needing to write JavaScript -- Automatic integration with the component system -- Smart invalidation of page sections -- Minimal data transfer - -**Intuitive [Latte|latte:] Templates:** The most secure templating system for PHP with advanced features: -- Automatic XSS protection with context-sensitive escaping -- Extensible with custom filters, functions, and tags -- Template inheritance and snippets for AJAX -- Excellent PHP 8.x support with type system - -**Dependency Injection:** Nette fully utilizes Dependency Injection: -- Automatic dependency passing (autowiring) -- Configuration using clear NEON format -- Support for component factories - - -Main Benefits -------------- - -- **Security**: Automatic protection against [vulnerabilities|nette:vulnerability-protection] like XSS, CSRF, etc. -- **Productivity**: Less writing, more features thanks to smart design -- **Debugging**: [Tracy debugger|tracy:] with routing panel -- **Performance**: Smart cache, lazy loading of components -- **Flexibility**: Easy URL modification even after application completion -- **Components**: Unique system of reusable UI elements -- **Modern**: Full support for PHP 8.3+ and type system - - -Getting Started ---------------- - -1. [How Do Applications Work? |how-it-works] - Understanding the basic architecture -2. [Presenters |presenters] - Working with presenters and actions -3. [Templates |templates] - Creating templates in Latte -4. [Routing |routing] - Configuring URL addresses -5. [Interactive Components |components] - Using the component system - - -PHP Compatibility ------------------ - -| version | compatible with PHP -|-----------------------|------------------- -| Nette Application 3.3 | PHP 8.3 - 8.5 -| Nette Application 3.2 | PHP 8.1 - 8.5 -| 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 - -Valid for the latest patch versions. - -{{maintitle: Nette Application: PHP Framework for Web Applications}} -{{description: The core of the Nette framework: a bidirectional router, presenters, reusable components with signals and secure Latte templates for modern web applications.}} diff --git a/application/en/@left-menu.texy b/application/en/@left-menu.texy deleted file mode 100644 index 64ae948bd7..0000000000 --- a/application/en/@left-menu.texy +++ /dev/null @@ -1,24 +0,0 @@ -Nette Application -***************** -- [Overview |@home] -- [How Do Applications Work? |how-it-works] -- [Bootstrapping] -- [Presenters] -- [Templates] -- [Directory Structure |directory-structure] -- [Routing] -- [Creating URL Links |creating-links] -- [Interactive Components |components] -- [AJAX & Snippets |ajax] -- [Multiplier |multiplier] -- [Configuration] -- [Upgrading] - - -Further Reading -*************** -- [Why Use Nette?|www:10-reasons-why-nette] -- [Installation |nette:installation] -- [Create Your First Application! |quickstart:] -- [Best practices |best-practices:] -- [Troubleshooting |nette:troubleshooting] diff --git a/application/en/@meta.texy b/application/en/@meta.texy deleted file mode 100644 index 42471908b0..0000000000 --- a/application/en/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Documentation}} diff --git a/application/en/ajax.texy b/application/en/ajax.texy deleted file mode 100644 index fb4a3e0cf3..0000000000 --- a/application/en/ajax.texy +++ /dev/null @@ -1,268 +0,0 @@ -AJAX & Snippets -*************** - -
    - -In the era of modern web applications, where functionality is often distributed between the server and the browser, AJAX is an essential connecting element. What options does the Nette Framework offer in this area? -- sending parts of the template, known as snippets -- passing variables between PHP and JavaScript -- tools for debugging AJAX requests - -
    - - -AJAX Request -============ - -An AJAX request does not fundamentally differ from a classic HTTP request. A presenter is called with specific parameters. It is up to the presenter to decide how to respond to the request - it can return data in JSON format, send a part of HTML code, an XML document, etc. - -On the browser side, we initiate an AJAX request using the `fetch()` function: - -```js -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -.then(response => response.json()) -.then(payload => { - // process the response -}); -``` - -On the server side, an AJAX request is recognized by the `$httpRequest->isAjax()` method of the service [encapsulating the HTTP request |http:request]. It uses the `X-Requested-With` HTTP header for detection, so it is crucial to send it. Within the presenter, you can use the `$this->isAjax()` method. - -If you want to send data in JSON format, use the [`sendJson()` |presenters#Sending a Response] method. The method also terminates the presenter's activity. - -```php -public function actionExport(): void -{ - $this->sendJson($this->model->getData()); -} -``` - -If you plan to respond with a special template designed for AJAX, you can do it as follows: - -```php -public function handleClick($param): void -{ - if ($this->isAjax()) { - $this->template->setFile('path/to/ajax.latte'); - } - // ... -} -``` - - -Snippets -======== - -Snippets are the most powerful tool Nette offers for connecting the server with the client. With them, you can turn an ordinary application into an AJAX one with minimal effort and just a few lines of code. The Fifteen example demonstrates how it all works, and its code can be found on [GitHub |https://github.com/nette-examples/fifteen]. - -Snippets allow you to update only parts of the page, instead of reloading the entire page. This is not only faster and more efficient but also provides a more comfortable user experience. Snippets might remind you of Hotwire for Ruby on Rails or Symfony UX Turbo. Interestingly, Nette introduced snippets 14 years earlier. - -How do snippets work? When the page is first loaded (a non-AJAX request), the entire page, including all snippets, is loaded. When the user interacts with the page (e.g., clicks a button, submits a form, etc.), an AJAX request is initiated instead of reloading the entire page. The code in the presenter performs the action and decides which snippets need updating. Nette renders these snippets and sends them as a JSON payload containing an array with snippets. The handling code in the browser then inserts the received snippets back into the page. Thus, only the code of the changed snippets is transferred, saving bandwidth and speeding up loading compared to transferring the entire page content. If no snippet is invalidated using `redrawControl()`, Nette returns the entire page even for an AJAX request - snippets are sent only when something is invalidated. - - -Naja ----- - -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 bundlers like Webpack, Rollup, Vite, Parcel, and others): - -```shell -npm install naja -``` - -…or insert it directly into the page template: - -```latte - -``` - -First, you need to [initialize |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization] the library: - -```js -naja.initialize(); -``` - -To turn an ordinary link (signal) or form submission into an AJAX request, simply mark the relevant link, form, or button with the `ajax` class: - -```latte -Go - -
    - -
    - -or - -
    - -
    -``` - - -Redrawing Snippets ------------------- - -Every object of the [Control |components] class (including the Presenter itself) keeps track of whether changes have occurred that require it to be redrawn. The `redrawControl()` method is used for this purpose: - -```php -public function handleLogin(string $user): void -{ - // after login, it is necessary to redraw the relevant part - $this->redrawControl(); - // ... -} -``` - -Nette allows for even finer control over what needs to be redrawn. The method can accept the name of the snippet as an argument. Thus, it is possible to invalidate (meaning: force redrawing) at the level of template parts. If the entire component is invalidated, every snippet within it will also be redrawn: - -```php -// invalidates the 'header' snippet -$this->redrawControl('header'); -``` - -You can also cancel a pending invalidation using the second parameter `$redraw`: calling `$this->redrawControl('header', redraw: false)` marks the snippet as not needing a redraw. The full signature is `redrawControl(?string $snippet = null, bool $redraw = true)`. - - -Snippets in Latte ------------------ - -Using snippets in Latte is extremely easy. To define a part of the template as a snippet, simply wrap it with the `{snippet}` and `{/snippet}` tags: - -```latte -{snippet header} -

    Hello ...

    -{/snippet} -``` - -The snippet creates a `
    ` element in the HTML page with a special generated `id`. When the snippet is redrawn, the content of this element is updated. Therefore, it is necessary that when the page is initially rendered, all snippets are also rendered, even if they might be empty at the beginning. - -You can also create a snippet using an element other than `
    ` with an n:attribute: - -```latte -
    -

    Hello ...

    -
    -``` - - -Snippet Areas -------------- - -Snippet names can also be expressions: - -```latte -{foreach $items as $id => $item} -
  • {$item}
  • -{/foreach} -``` - -On its own, this is a non-functional intermediate step: rendered outside a static `{snippet}` or `{snippetArea}`, a dynamic snippet triggers an `E_USER_WARNING` with the message *Dynamic snippets are allowed only inside static snippet/snippetArea.* We fix that below. - -This creates several snippets like `item-0`, `item-1`, etc. If we were to directly invalidate a dynamic snippet (e.g., `item-1`), nothing would be redrawn. The reason is that snippets truly function as excerpts, and only they themselves are rendered directly. However, in the template, there is technically no snippet named `item-1`. It only comes into existence when the code surrounding the snippet, i.e., the foreach loop, is executed. Therefore, we mark the part of the template that needs to be executed using the `{snippetArea}` tag: - -```latte -
      - {foreach $items as $id => $item} -
    • {$item}
    • - {/foreach} -
    -``` - -And we request the redrawing of both the individual snippet and the entire parent area: - -```php -$this->redrawControl('itemsContainer'); -$this->redrawControl('item-1'); -``` - -At the same time, it is advisable to ensure that the `$items` array contains only the items that should be redrawn. - -If we include another template containing snippets into the main template using the `{include}` tag, it is necessary to wrap the template inclusion within a `snippetArea` again and invalidate it along with the snippet: - -```latte -{snippetArea include} - {include 'included.latte'} -{/snippetArea} -``` - -```latte -{* included.latte *} -{snippet item} - ... -{/snippet} -``` - -```php -$this->redrawControl('include'); -$this->redrawControl('item'); -``` - - -Snippets in Components ----------------------- - -You can create snippets within [components|components], and Nette will automatically redraw them. However, there is a limitation: to redraw snippets, Nette calls the `render()` method without any parameters. Therefore, passing parameters in the template will not work: - -```latte -OK -{control productGrid} - -will not work: -{control productGrid, $arg, $arg} -{control productGrid:paginator} -``` - - -Sending Custom Data -------------------- - -Along with snippets, you can send any additional data to the client. Simply write them into the `payload` object: - -```php -public function actionDelete(int $id): void -{ - // ... - if ($this->isAjax()) { - $this->payload->message = 'Success'; - } -} -``` - - -Redirecting ------------ - -During an AJAX request, the `redirect()` and `redirectUrl()` methods do not send an HTTP redirect. Instead, they write the target URL into the payload (the data object sent in the AJAX response), specifically its `payload.redirect` property, and send it; the actual redirection is then performed by the client-side library (Naja). - - -Passing Parameters -================== - -When sending parameters to a component via an AJAX request, whether they are signal parameters or persistent parameters, we must specify their global name in the request, which includes the component's name. The `getParameterId()` method returns the full parameter name. - -```js -let url = new URL({link //foo!}); -url.searchParams.set({$control->getParameterId('bar')}, bar); - -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -``` - -And the handle method with corresponding parameters in the component: - -```php -public function handleFoo(int $bar): void -{ -} -``` - - -Further Reading -=============== - -- [Dynamic Snippets |best-practices:dynamic-snippets] - -{{maintitle: AJAX and Snippets in Nette: Update Parts of a Page}} -{{description: Redraw only what changed, without reloading the page: snippets in Latte templates, AJAX requests with the Naja library and passing parameters from the browser.}} diff --git a/application/en/bootstrapping.texy b/application/en/bootstrapping.texy deleted file mode 100644 index 84644a836d..0000000000 --- a/application/en/bootstrapping.texy +++ /dev/null @@ -1,315 +0,0 @@ -Bootstrapping -************* - -
    - -Bootstrapping is the process of initializing the application environment, creating a dependency injection (DI) container, and starting the application. We will discuss: - -- how the Bootstrap class initializes the environment -- how applications are configured using NEON files -- how to distinguish between production and development mode -- how to create and configure the DI container - -
    - - -Applications, whether web-based or scripts run from the command line, begin their execution with some form of environment initialization. In the old days, a file named perhaps `include.inc.php` was responsible for this, included by the initial file. In modern Nette applications, it has been replaced by the `Bootstrap` class, which, as part of the application, can be found in the `app/Bootstrap.php` file. It might look like this, for example: - -```php -namespace App; - -use Nette; -use Nette\Bootstrap\Configurator; - -class Bootstrap -{ - private Configurator $configurator; - private string $rootDir; - - public function __construct() - { - $this->rootDir = dirname(__DIR__); - // The configurator is responsible for setting up the application environment and services. - $this->configurator = new Configurator; - // Set the directory for temporary files generated by Nette (e.g., compiled templates) - $this->configurator->setTempDirectory($this->rootDir . '/temp'); - } - - public function bootWebApplication(): Nette\DI\Container - { - $this->initializeEnvironment(); - $this->setupContainer(); - return $this->configurator->createContainer(); - } - - private function initializeEnvironment(): void - { - // Nette is smart, and the development mode turns on automatically, - // or you can enable it for a specific IP address by uncommenting the following line: - // $this->configurator->setDebugMode('secret@23.75.345.200'); - - // Enables Tracy: the ultimate "swiss army knife" debugging tool. - $this->configurator->enableTracy($this->rootDir . '/log'); - - // RobotLoader: automatically loads all classes in the chosen directory - $this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); - } - - private function setupContainer(): void - { - // Load configuration files - $this->configurator->addConfig($this->rootDir . '/config/common.neon'); - } -} -``` - - -index.php -========= - -In the case of web applications, the initial file is `index.php`, located in the [public directory |directory-structure#Public Directory www] `www/`. It instructs the Bootstrap class to initialize the environment and create the DI container. Then, it retrieves the `Application` service from the container, which runs the web application: - -```php -$bootstrap = new App\Bootstrap; -// Initialize the environment + create a DI container -$container = $bootstrap->bootWebApplication(); -// DI container creates a Nette\Application\Application object -$application = $container->getByType(Nette\Application\Application::class); -// Start the Nette application and process the incoming request -$application->run(); -``` - -.[note] -The `$application` object emits [events |nette:glossary#Events] as it processes the request - `onStartup`, `onRequest`, `onPresenter`, `onResponse`, `onShutdown`, and `onError` (on an unhandled exception). You can attach handlers to them, which is handy for logging or application-wide monitoring. - -As you can see, the [api:Nette\Bootstrap\Configurator] class helps with setting up the environment and creating the dependency injection (DI) container. We will now introduce it in more detail. - - -Development vs Production Mode -============================== - -Nette behaves differently depending on whether it is running on a development or production server: - -🛠️ Development Mode: - - Displays the Tracy debug bar with useful information (SQL queries, execution time, memory used) - - In case of an error, displays a detailed error page with function calls and variable contents - - Automatically refreshes the cache when Latte templates, configuration files, etc., are changed - - -🚀 Production Mode: - - Does not display any debugging information; all errors are written to the log - - In case of an error, displays an ErrorPresenter or a generic "Server Error" page - - Cache is never automatically refreshed! - - Optimized for speed and security - - -Mode selection is done by autodetection, so usually, there is no need to configure anything or manually switch modes: - -- development mode: on localhost (IP address `127.0.0.1` or `::1`) if no proxy is present (i.e., its HTTP header is not detected) -- production mode: everywhere else - -If we want to enable development mode in other cases, for example, for programmers accessing from a specific IP address, we use `setDebugMode()`: - -```php -$this->configurator->setDebugMode('23.75.345.200'); // an array of IP addresses can also be provided -``` - -We strongly recommend combining an IP address with a cookie. Store a secret token, e.g., `secret1234`, in the `nette-debug` cookie, and this way, activate development mode for programmers accessing from a specific IP address who also have the mentioned token in their cookie: - -```php -$this->configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -We can also disable development mode completely, even for localhost: - -```php -$this->configurator->setDebugMode(false); -``` - -Note that the value `true` forces development mode on, which should **never** happen on a production server. - -Autodetection is handled internally by the static method `Configurator::detectDebugMode()`, which you can also call yourself, for example to detect development mode outside the configurator. It accepts an optional whitelist of IP addresses or computer names and returns whether the current request should run in development mode: - -```php -$debug = Nette\Bootstrap\Configurator::detectDebugMode('23.75.345.200'); -``` - - -Debugging Tool Tracy -==================== - -For easy debugging, we will enable the excellent tool [Tracy |tracy:]. In development mode, it visualizes errors, and in production mode, it logs errors to the specified directory: - -```php -$this->configurator->enableTracy($this->rootDir . '/log'); -``` - - -Temporary Files -=============== - -Nette uses cache for the DI container, RobotLoader, templates, etc. Therefore, it is necessary to set the path to the directory where the cache will be stored: - -```php -$this->configurator->setTempDirectory($this->rootDir . '/temp'); -``` - -On Linux or macOS, set [write permissions |nette:troubleshooting#Setting Directory Permissions] for the `log/` and `temp/` directories. - - -RobotLoader -=========== - -Usually, we will want to automatically load classes using [RobotLoader |robot-loader:], so we need to start it and let it load classes from the directory where `Bootstrap.php` is located (i.e., `__DIR__`), and all its subdirectories: - -```php -$this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); -``` - -An alternative approach is to load classes solely through [Composer |best-practices:composer] following PSR-4. - - -Timezone -======== - -You can set the default timezone via the configurator. - -```php -$this->configurator->setTimeZone('Europe/Prague'); -``` - - -DI Container Configuration -========================== - -Part of the booting process is the creation of the DI container, or object factory, which is the heart of the entire application. It is actually a PHP class generated by Nette and stored in the cache directory. The factory produces key application objects, and using configuration files, we instruct it how to create and set them up, thereby influencing the behavior of the entire application. - -Configuration files are usually written in the [NEON format |neon:format]. In a separate chapter, you can read about [what can be configured |nette:configuring]. - -.[tip] -In development mode, the container is automatically updated whenever the code or configuration files change. In production mode, it is generated only once, and changes are not checked to maximize performance. - -While `createContainer()` builds the container and returns its instance, the `loadContainer()` method returns only the name of the generated container class, which you can then instantiate yourself. This is useful in advanced scenarios. - -Configuration files are loaded using `addConfig()`: - -```php -$this->configurator->addConfig($this->rootDir . '/config/common.neon'); -``` - -If we want to add more configuration files, we can call the `addConfig()` function multiple times. - -```php -$configDir = $this->rootDir . '/config'; -$this->configurator->addConfig($configDir . '/common.neon'); -$this->configurator->addConfig($configDir . '/services.neon'); -if (PHP_SAPI === 'cli') { - $this->configurator->addConfig($configDir . '/cli.php'); -} -``` - -The name `cli.php` is not a typo; configuration can also be written in a PHP file that returns it as an array. - -We can also add other configuration files in [the `includes` section |dependency-injection:configuration#Including Files]. - -If items with the same keys appear in configuration files, they will be overwritten, or in the case of [arrays, merged |dependency-injection:configuration#Merging]. A file included later has higher priority than the previous one. The file in which the `includes` section is listed has higher priority than the files included within it. - - -Static Parameters ------------------ - -Parameters used in configuration files can be defined [in the `parameters` section |dependency-injection:configuration#Parameters] and also passed (or overridden) using the `addStaticParameters()` method (whose older, now deprecated alias is `addParameters()`). It is important that different parameter values will cause the generation of additional DI containers, i.e., additional classes. - -```php -$this->configurator->addStaticParameters([ - 'projectId' => 23, -]); -``` - -The `projectId` parameter can be referenced in the configuration using the standard notation `%projectId%`. - - -Dynamic Parameters ------------------- - -We can also add dynamic parameters to the container, whose different values, unlike static parameters, will not cause the generation of new DI containers. - -```php -$this->configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -This way, we can easily add, for example, environment variables, which can then be referenced in the configuration using the notation `%env.variable%`. - -```php -$this->configurator->addDynamicParameters([ - 'env' => getenv(), -]); -``` - - -Default Parameters ------------------- - -You can use these parameters in the configuration files: - -- `%appDir%` is the absolute path to the directory containing the `Bootstrap.php` file -- `%wwwDir%` is the absolute path to the directory containing the entry file `index.php` -- `%tempDir%` is the absolute path to the directory for temporary files -- `%vendorDir%` is the absolute path to the directory where Composer installs libraries -- `%rootDir%` is the absolute path to the root directory of the project -- `%baseUrl%` is the absolute URL to the root directory (a dynamic parameter resolved at runtime; outside an HTTP request it is derived from [http: baseUrl |http:configuration#Application Base URL]) -- `%debugMode%` indicates whether the application is in debug mode -- `%consoleMode%` indicates whether the request came through the command line - - -Imported Services ------------------ - -Now we go deeper. Although the purpose of the DI container is to create objects, occasionally there might be a need to insert an existing object into the container. We do this by defining the service with the `imported: true` flag. - -```neon -services: - myservice: - type: App\Model\MyCustomService - imported: true -``` - -And in the bootstrap, we insert the object into the container: - -```php -$this->configurator->addServices([ - 'myservice' => new App\Model\MyCustomService('foobar'), -]); -``` - - -Different Environments -====================== - -Feel free to modify the `Bootstrap` class according to your needs. You can add parameters to the `bootWebApplication()` method to distinguish between web projects. Or you can add other methods, such as `bootTestEnvironment()` which initializes the environment for unit tests, `bootConsoleApplication()` for scripts called from the command line, etc. - -```php -public function bootTestEnvironment(): Nette\DI\Container -{ - Tester\Environment::setup(); // Nette Tester initialization - $this->setupContainer(); - return $this->configurator->createContainer(); -} - -public function bootConsoleApplication(): Nette\DI\Container -{ - $this->configurator->setDebugMode(false); - $this->initializeEnvironment(); - $this->setupContainer(); - return $this->configurator->createContainer(); -} -``` - -{{maintitle: Bootstrapping a Nette App: From index.php to DI Container}} -{{description: See how a Nette application starts, from index.php and the Configurator class through development mode, Tracy and temporary files to the NEON configuration.}} diff --git a/application/en/components.texy b/application/en/components.texy deleted file mode 100644 index 31d270bf88..0000000000 --- a/application/en/components.texy +++ /dev/null @@ -1,507 +0,0 @@ -Interactive Components -********************** - -
    - -Components are separate reusable objects that we embed into pages. They can be forms, datagrids, polls, essentially anything that makes sense to use repeatedly. We will show: - -- how to use components? -- how to write them? -- what are signals? - -
    - -Nette has a built-in component system. Something similar might be familiar to veterans from Delphi or ASP.NET Web Forms; React or Vue.js is built on something remotely similar. However, in the world of PHP frameworks, this is a unique feature. - -At the same time, components fundamentally influence the approach to application development. You can compose pages from pre-prepared units. Need a datagrid in your administration? Find it on [Componette |https://componette.org/search/component], a repository of open-source add-ons (not just components) for Nette, and simply insert it into the presenter. - -You can incorporate any number of components into the presenter. And you can embed other components within some components. This creates a component tree, with the presenter as its root. - - -Factory Methods -=============== - -How are components inserted into the presenter and subsequently used? Usually via factory methods. - -A component factory is an elegant way to create components only when they are actually needed (lazy / on demand). The whole magic lies in implementing a method named `createComponent()`, where `` is the name of the component being created, which creates and returns the component. - -```php .{file:DefaultPresenter.php} -class DefaultPresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentPoll(): PollControl - { - $poll = new PollControl; - $poll->items = $this->items; - return $poll; - } -} -``` - -Because all components are created in separate methods, the code becomes clearer. - -.[note] -Component names always start with a lowercase letter, even though they are capitalized in the method name. - -We never call factories directly; they are called automatically the first time we use the component. Thanks to this, the component is created at the right moment and only if it is actually needed. If we don't use the component (e.g., during an AJAX request where only part of the page is transferred, or when caching the template), it won't be created at all, saving server performance. - -```php .{file:DefaultPresenter.php} -// we access the component and if it was the first time, -// createComponentPoll() is called which creates it -$poll = $this->getComponent('poll'); -// alternative syntax: $poll = $this['poll']; -``` - -In the template, it is possible to render a component using the [{control} |#Rendering] tag. Therefore, there is no need to manually pass components to the template. - -```latte -

    Please Vote

    - -{control poll} -``` - -.[tip] -To dynamically create a variable number of components, use [Multiplier |multiplier]. - -Factory methods `createComponent()` don't work only in presenters. You can nest a component inside another component the same way, composing them into a tree - handy for example for a separately rendered form inside a component. - - -Hollywood Style -=============== - -Components commonly use a fresh technique we like to call the Hollywood style. You surely know the cliché often heard by participants in film auditions: "Don't call us, we'll call you." And that's precisely what it's about. - -In Nette, instead of constantly having to ask questions ("was the form submitted?", "was it valid?", or "did the user press this button?"), you tell the framework "when this happens, call this method" and leave further work to it. If you program in JavaScript, you are intimately familiar with this style of programming. You write functions that are called when a certain event occurs. And the language passes the appropriate parameters to them. - -This completely changes the perspective on writing applications. The more tasks you can leave to the framework, the less work you have. And the less you might overlook. - - -Writing a Component -=================== - -By the term component, we usually mean a descendant of the [api:Nette\Application\UI\Control] class. (It would be more accurate to use the term "controls", but that has a different meaning in some languages, and "components" has become more established.) The presenter [api:Nette\Application\UI\Presenter] itself is also a descendant of the `Control` class. - -```php .{file:PollControl.php} -use Nette\Application\UI\Control; - -class PollControl extends Control -{ -} -``` - - -Rendering -========= - -We already know that the `{control componentName}` tag is used to render a component. It actually calls the `render()` method of the component, in which we take care of the rendering. We have available, just like in the presenter, a [Latte template|templates] in the `$this->template` variable, to which we pass parameters. Unlike in the presenter, we must specify the template file and have it rendered: - -```php .{file:PollControl.php} -public function render(): void -{ - // insert some parameters into the template - $this->template->param = $value; - // and render it - $this->template->render(__DIR__ . '/poll.latte'); -} -``` - -The `{control}` tag allows passing parameters to the `render()` method: - -```latte -{control poll, $id, $message} -``` - -```php .{file:PollControl.php} -public function render(int $id, string $message): void -{ - // ... -} -``` - -Sometimes a component may consist of several parts that we want to render separately. For each of them, we create our own rendering method, here in the example, `renderPaginator()`: - -```php .{file:PollControl.php} -public function renderPaginator(): void -{ - // ... -} -``` - -And in the template, we then invoke it using: - -```latte -{control poll:paginator} -``` - -For a better understanding, it's good to know how this tag translates into PHP code. - -```latte -{control poll} -{control poll:paginator, 123, 'hello'} -``` - -translates to: - -```php -$control->getComponent('poll')->render(); -$control->getComponent('poll')->renderPaginator(123, 'hello'); -``` - -The `getComponent()` method returns the `poll` component, and the `render()` method, or `renderPaginator()` if a different rendering method is specified in the tag after the colon, is called on this component. - -.[caution] -Beware, if **`=>`** appears in the parameters outside square brackets, all parameters will be wrapped in an array and passed as the first argument: - -```latte -{control poll, id: 123, message: 'hello'} -``` - -translates to: - -```php -$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']); -``` - -Rendering a sub-component: - -```latte -{control cartControl-someForm} -``` - -translates to: - -```php -$control->getComponent("cartControl-someForm")->render(); -``` - -Components, like presenters, automatically pass several useful variables to templates: - -- `$basePath` is the absolute URL path to the root directory (e.g., `/eshop`) -- `$baseUrl` is the absolute URL to the root directory (e.g., `http://localhost/eshop`) -- `$user` is an object [representing the user |security:authentication] -- `$presenter` is the current presenter -- `$control` is the current component -- `$flashes` is an array of [messages |#Flash Messages] sent by the `flashMessage()` function - - -Signal -====== - -We already know that navigation in a Nette application consists of linking or redirecting to `Presenter:action` pairs. But what if we just want to perform an action on the **current page**? For example, change the sorting of columns in a table; delete an item; switch light/dark mode; submit a form; vote in a poll; etc. - -This type of request is called a signal. And just as actions invoke `action()` or `render()` methods, signals call `handle()` methods. While the concept of action (or view) relates purely to presenters, signals concern all components. And thus also presenters, because `UI\Presenter` is a descendant of `UI\Control`. - -```php -public function handleClick(int $x, int $y): void -{ - // ... processing of signal ... -} -``` - -A link that calls a signal is created in the usual way, i.e., in the template with the `n:href` attribute or the `{link}` tag, in the code with the `link()` method. More in the chapter [Creating URL Links |creating-links#Links to Signal]. - -```latte -click here -``` - -A signal is always called on the current presenter and action; it is not possible to invoke it on another presenter or action. - -Thus, a signal causes the page to reload just like the original request, but additionally calls the signal handling method with the appropriate parameters. If the method does not exist, an [api:Nette\Application\UI\BadSignalException] exception is thrown, which is displayed to the user as a 403 Forbidden error page. - - -Snippets and AJAX -================= - -Signals might remind you a bit of AJAX: handlers that are invoked on the current page. And you are right, signals are indeed often called using AJAX, and subsequently, only the changed parts of the page are transferred to the browser. These are called snippets. More information can be found on the [page dedicated to AJAX |ajax]. - - -Flash Messages -============== - -A component has its own storage for flash messages, independent of the presenter. These are messages that, for example, inform about the result of an operation. An important feature of flash messages is that they are available in the template even after redirection. Even after being displayed, they remain active for another 30 seconds - for example, in case the user refreshes the page due to a transmission error - the message won't disappear immediately. - -Sending is handled by the [flashMessage |api:Nette\Application\UI\Control::flashMessage()] method. The first parameter is the message text (`string`, `Stringable`) or an `stdClass` object representing the message. The optional second parameter is its type (error, warning, info, etc.). The `flashMessage()` method returns an instance of the flash message as an `stdClass` object, to which further information can be added. - -```php -$this->flashMessage('Item was deleted.'); -$this->redirect(/* ... */); // and redirect -``` - -These messages are available to the template in the `$flashes` variable as `stdClass` objects, which contain the properties `message` (message text), `type` (message type), and can contain the aforementioned user information. We render them like this, for example: - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -Redirection After Processing a Signal -===================================== - -Processing a component's signal is often followed by a redirect. This is similar to forms - after submitting them, we also redirect to prevent data resubmission if the page is refreshed in the browser. - -```php -$this->redirect('this'); // redirects to the current presenter and action -``` - -Because a component is a reusable element and typically should not have a direct link to specific presenters, the `redirect()` and `link()` methods automatically interpret the parameter as a component signal: - -```php -$this->redirect('click'); // redirects to the 'click' signal of the same component -``` - -If you need to redirect to another presenter or action, you can do it through the presenter: - -```php -$this->getPresenter()->redirect('Product:show'); // redirects to another presenter/action -``` - - -Persistent Parameters -===================== - -Persistent parameters are used to maintain state in components across different requests. Their value remains the same even after clicking a link. Unlike session data, they are transferred in the URL. And this happens completely automatically, including links created in other components on the same page. - -For example, you have a component for content pagination. There might be several such components on a page. And we want all components to remain on their current page after clicking a link. Therefore, we make the page number (`page`) a persistent parameter. - -Creating a persistent parameter in Nette is extremely simple. Just create a public property and mark it with the attribute (previously `/** @persistent */` was used): - -```php -use Nette\Application\Attributes\Persistent; // this line is important - -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; // must be public -} -``` - -We recommend specifying the data type for the property (e.g., `int`), and you can also provide a default value. Parameter values can be [validated |#Validation of Persistent Parameters]. - -When creating a link, the value of a persistent parameter can be changed: - -```latte -next -``` - -Or it can be *reset*, i.e., removed from the URL. It will then assume its default value: - -```latte -reset -``` - - -Persistent Components -===================== - -Not only parameters but also components can be persistent. Their persistent parameters are then transferred even between different actions of the presenter or between multiple presenters. We mark persistent components with an attribute on the presenter class. For example, we mark the `calendar` and `poll` components like this: - -```php -use Nette\Application\Attributes\Persistent; - -#[Persistent('calendar', 'poll')] -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Subcomponents within these components do not need to be marked; they become persistent too. - -The older annotation `@persistent` still works, but it is deprecated and triggers a warning: - -```php -/** - * @persistent(calendar, poll) - */ -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Components with Dependencies -============================ - -How to create components with dependencies without "cluttering" the presenters that will use them? Thanks to the smart features of the DI container in Nette, similar to using classic services, most of the work can be left to the framework. - -Let's take an example of a component that has a dependency on the `PollFacade` service: - -```php -class PollControl extends Control -{ - public function __construct( - private int $id, // ID of the poll for which we are creating the component - private PollFacade $facade, - ) { - } - - public function handleVote(int $voteId): void - { - $this->facade->vote($this->id, $voteId); - // ... - } -} -``` - -If we were writing a classic service, there would be nothing to discuss. The DI container would invisibly handle passing all dependencies. However, with components, we usually handle them by creating a new instance directly in the presenter within the [#factory methods] `createComponent…()`. But passing all dependencies of all components into the presenter just to pass them on to the components is cumbersome. And the amount of code written... - -The logical question is, why don't we simply register the component as a classic service, pass it to the presenter, and then return it in the `createComponent…()` method? However, this approach is inappropriate because we want the ability to create the component multiple times if needed. - -The correct solution is to write a factory for the component, i.e., a class that creates the component for us: - -```php -class PollControlFactory -{ - public function __construct( - private PollFacade $facade, - ) { - } - - public function create(int $id): PollControl - { - return new PollControl($id, $this->facade); - } -} -``` - -We register this factory in our container in the configuration: - -```neon -services: - - PollControlFactory -``` - -and finally, we use it in our presenter: - -```php -class PollPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private PollControlFactory $pollControlFactory, - ) { - } - - protected function createComponentPollControl(): PollControl - { - $pollId = 1; // we can pass our parameter - return $this->pollControlFactory->create($pollId); - } -} -``` - -What's great is that Nette DI can [generate |dependency-injection:factory] such simple factories, so instead of writing its entire code, you just need to write its interface: - -```php -interface PollControlFactory -{ - public function create(int $id): PollControl; -} -``` - -And that's all. Nette internally implements this interface and injects it into the presenter, where we can use it. It magically adds the `$id` parameter and an instance of the `PollFacade` class to our component. - - -Components in Depth -=================== - -Components in Nette Application represent reusable parts of a web application that we embed into pages, and which this entire chapter is dedicated to. What exactly are the capabilities of such a component? - -1) It is renderable in a template -2) It knows [which part of itself |ajax#Snippets] to render during an AJAX request (snippets) -3) It has the ability to store its state in the URL (persistent parameters) -4) It has the ability to react to user actions (signals) -5) It creates a hierarchical structure (where the root is the presenter) - -Each of these functions is handled by one of the classes in the inheritance line. Rendering (1 + 2) is handled by [api:Nette\Application\UI\Control], integration into the [lifecycle |presenters#Presenter Life Cycle] (3, 4) by the [api:Nette\Application\UI\Component] class, and the creation of the hierarchical structure (5) by the [Container and Component |component-model:] classes. - -``` -Nette\ComponentModel\Component { IComponent } -| -+- Nette\ComponentModel\Container { IContainer } - | - +- Nette\Application\UI\Component { SignalReceiver, StatePersistent } - | - +- Nette\Application\UI\Control { Renderable } - | - +- Nette\Application\UI\Presenter { IPresenter } -``` - - -Component Lifecycle -------------------- - -[* lifecycle-component.svg *] *** *Component lifecycle* .<> - - -Validation of Persistent Parameters ------------------------------------ - -The values of [#persistent parameters] received from URLs are written to properties by the `loadState()` method. It also checks whether the data type specified for the property matches; otherwise, it responds with a 404 error and the page is not displayed. - -Never blindly trust persistent parameters, as they can be easily overwritten by the user in the URL. This is how we check, for example, if the page number `$this->page` is greater than 0. A suitable way is to override the mentioned `loadState()` method: - -```php -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; - - public function loadState(array $params): void - { - parent::loadState($params); // $this->page is set here - // follows the custom value check: - if ($this->page < 1) { - $this->error(); - } - } -} -``` - -The opposite process, i.e., collecting values from persistent properties, is handled by the `saveState()` method. - - -Connecting to the Presenter ---------------------------- - -At the moment a component becomes part of the presenter hierarchy, its callbacks stored in the array `$onAnchor` are invoked. From that point on, the component has the presenter available, can safely create links, read persistent parameters, and so on. - -```php -$control->onAnchor[] = function ($control): void { - // the component now has the presenter available -}; -``` - - -Signals in Depth ----------------- - -A signal causes the page to reload exactly like the original request (except when called via AJAX) and invokes the `signalReceived($signal)` method, whose default implementation in the `Nette\Application\UI\Component` class attempts to call a method composed of the words `handle`. Further processing is up to the given object. Objects inheriting from `Component` (i.e., `Control` and `Presenter`) react by trying to call the `handle` method with the appropriate parameters. - -In other words: the definition of the `handle` function is taken, along with all parameters that came with the request, and parameters from the URL are assigned to the arguments by name, and an attempt is made to call the method. For example, the value from the `id` parameter in the URL is passed as the `$id` argument, `something` from the URL is passed as `$something`, etc. And if the method does not exist, the `signalReceived` method throws an [exception |api:Nette\Application\UI\BadSignalException]. - -Besides the parameters from the URL, a signal also reads the parameters sent in the **POST body of the request**. This comes in handy because signals are often invoked via JavaScript, where it is natural to send data using the POST method. However, if a parameter of the same name arrives both from the URL and from the POST body, the value **from the URL takes precedence**. Therefore, avoid giving a POST field the same name as a URL or route parameter, otherwise the URL value would silently override it. The signal parameters share a common space with the action and persistent parameters, see [Shared Parameter Space |presenters#Shared Parameter Space]. - -A signal can be received by any component, presenter, or object that implements the `SignalReceiver` interface and is connected to the component tree. - -The main recipients of signals will be `Presenters` and visual components inheriting from `Control`. A signal is intended to serve as a sign for an object that it should do something - a poll should count a vote from the user, a news block should expand and display twice as many news items, a form has been submitted and should process data, and so on. - -The URL for a signal is created using the [Component::link() |api:Nette\Application\UI\Component::link()] method. As the `$destination` parameter, we pass the string `{signal}!` and as `$args`, an array of arguments we want to pass to the signal. The signal is always called on the current presenter and action with the current parameters; the signal parameters are just added. Additionally, the **parameter `?do` which specifies the signal** is added. - -Its format is either `{signal}` or `{signalReceiver}-{signal}`. `{signalReceiver}` is the name of the component in the presenter. Therefore, a hyphen cannot be used in the component name - it is used to separate the component name and the signal, although it is possible to nest multiple components this way. - -The [isSignalReceiver()|api:Nette\Application\UI\Presenter::isSignalReceiver()] method checks whether the component (first argument) is the recipient of the signal (second argument). The second argument can be omitted - then it checks if the component is the recipient of any signal. If the second parameter is set to `true`, it verifies whether the specified component or any of its descendants is the recipient. - -At any stage preceding `handle`, we can execute the signal manually by calling the [processSignal()|api:Nette\Application\UI\Presenter::processSignal()] method, which takes care of handling the signal - it takes the component identified as the signal recipient (if no recipient is specified, it is the presenter itself) and sends the signal to it. - -Example: - -```php -if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) { - $this->processSignal(); -} -``` - -This executes the signal prematurely, and it will not be called again. - -{{maintitle: Nette Components: Reusable UI with Signals and AJAX}} -{{description: Write a form, data grid or poll once and reuse it on any page. Factory methods, rendering, signals, AJAX snippets, flash messages and persistent parameters.}} diff --git a/application/en/configuration.texy b/application/en/configuration.texy deleted file mode 100644 index 2fe9536e5d..0000000000 --- a/application/en/configuration.texy +++ /dev/null @@ -1,189 +0,0 @@ -Application Configuration -************************* - -.[perex] -Overview of configuration options for Nette Application. - - -Application -=========== - -```neon -application: - # show the "Nette Application" panel in Tracy BlueScreen? - debugger: ... # (bool) enabled if Tracy is available - - # in production, exceptions are always handled by the error-presenter; - # this option only enables that behavior in development mode too - catchExceptions: ... # (bool) defaults to false - i.e. off in dev, always on in production - - # name of the error-presenter - errorPresenter: Error # (string|array) defaults to 'Nette:Error' - - # defines aliases for presenters and actions - aliases: ... - - # defines the rules for translating the presenter name to a class - mapping: ... - - # suppress warnings for invalid links? - # effective only in development mode - silentLinks: ... # (bool) defaults to false -``` - -Since `nette/application` version 3.2, it is possible to define a pair of error presenters: - -```neon -application: - errorPresenter: - 4xx: Error4xx # for Nette\Application\BadRequestException - 5xx: Error5xx # for other exceptions -``` - -Splitting them is useful because both situations are fundamentally different. A `BadRequestException` (codes 4xx) means that the application is fine and only the visitor asked for something that does not exist. Therefore, you can use a full-featured presenter that displays a friendly message in the layout of your website. On the contrary, a 5xx error means that something in the application has broken and you do not know what. Keep the 5xx presenter as minimal as possible, so that nothing else can fail while rendering it - ideally, it should not touch the database, the layout, or the logged-in user. - -The `silentLinks` option determines how Nette behaves in development mode when link generation fails (for example, because the presenter does not exist, etc.). The default value `false` means that Nette triggers an `E_USER_WARNING` error. Setting it to `true` suppresses this error message. In a production environment, `E_USER_WARNING` is always triggered. This behavior can also be influenced by setting the presenter variable [$invalidLinkMode |creating-links#Invalid Links]. - -[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 ------------------------------------- - -Nette automatically adds presenters as services to the DI container, which significantly speeds up their creation. How Nette locates presenters can be configured: - -```neon -application: - # look for presenters in Composer class map? - scanComposer: ... # (bool) defaults to true - - # a mask that the class and file name must match - scanFilter: ... # (string) defaults to '*Presenter' - - # in which directories to look for presenters? - scanDirs: # (string[]|false) defaults to '%appDir%' - - %vendorDir%/mymodule -``` - -The directories listed in `scanDirs` do not override the default value `%appDir%`, but complement it, so `scanDirs` will contain both paths `%appDir%` and `%vendorDir%/mymodule`. If we want to omit the default directory, we use an [exclamation mark |dependency-injection:configuration#Merging]: - -```neon -application: - scanDirs!: - - %vendorDir%/mymodule -``` - -Directory scanning can be turned off by setting the value to `false`. Presenters are then no longer registered as services, so they can't be adjusted via the [decorator |dependency-injection:configuration#Decorator] section and their creation is slower. We therefore do not recommend completely suppressing automatic registration, as it will reduce application performance. - - -Latte Templates -=============== - -This setting globally affects the behavior of Latte in components and presenters. - -```neon -latte: - # show the Latte panel in the Tracy Bar for the main template (true) or for all components (all)? - debugger: ... # (true|false|'all') enabled if Tracy is available (debug mode only) - - # generate templates with declare(strict_types=1) header - strictTypes: ... # (bool) defaults to false - - # enable [strict parser mode |latte:develop#strict mode] - strictParsing: ... # (bool) default is false - - # limits the scope of variables to the loop body - scopedLoopVariables: ... # (bool) default is false - - # removes indentation caused by nesting in paired tags - dedent: ... # (bool) default is false - - # enable [checking of generated code |latte:develop#Checking Generated Code] - phpLinter: ... # (string) default is null - - # set the locale - locale: cs_CZ # (string) default is null - - # class of the $this->template object - templateClass: App\MyTemplateClass # defaults to Nette\Bridges\ApplicationLatte\DefaultTemplate -``` - -You can add new [extensions |latte:extending-latte#Latte Extension] using: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - - -Routing -======= - -Basic settings: - -```neon -routing: - # show the routing panel in Tracy Bar? - debugger: ... # (bool) enabled if Tracy is available (debug mode only) - - # serialize the router into the DI container - cache: ... # (bool) defaults to false -``` - -Routing is usually defined in the [RouterFactory |routing#Route Collection] class. Alternatively, routes can also be defined in the configuration using `mask: action` pairs, but this method does not offer much flexibility: - -```neon -routing: - routes: - 'detail/': Admin:Home:default - '/': Front:Home:default -``` - - -Constants -========= - -Creating PHP constants. - -```neon -constants: - Foobar: 'baz' -``` - -The `Foobar` constant will be created after the application starts. - -.[note] -Constants should not serve as globally available variables. To pass values to objects, use [dependency injection |dependency-injection:passing-dependencies]. - - -PHP -=== - -Setting PHP directives. An overview of all directives can be found at [php.net |https://www.php.net/manual/en/ini.list.php]. - -```neon -php: - date.timezone: Europe/Prague -``` - - -DI Services -=========== - -These services are added to the DI container: - -| Name | Type | Description -|----------------------------|---------------------------------------------------|----------------------------------------- -| `application.application` | [api:Nette\Application\Application] | the [application runner |how-it-works#Nette Application] -| `application.linkGenerator` | [api:Nette\Application\LinkGenerator] | [LinkGenerator |creating-links#LinkGenerator] -| `application.presenterFactory` | [api:Nette\Application\IPresenterFactory] | presenter factory -| `application.###` | [api:Nette\Application\UI\Presenter] | individual presenters -| `routing.router` | [api:Nette\Routing\Router] | router -| `latte.latteFactory` | [api:Nette\Bridges\ApplicationLatte\LatteFactory] | factory for `Latte\Engine` object -| `latte.templateFactory` | [api:Nette\Application\UI\TemplateFactory] | factory for [`$this->template` |templates] - -{{maintitle: Nette Application Configuration: Options in NEON}} -{{description: Configure Nette Application in NEON: the error presenter, presenter mapping, Latte templates, routing, constants, PHP settings and DI services.}} diff --git a/application/en/creating-links.texy b/application/en/creating-links.texy deleted file mode 100644 index 191169cdfd..0000000000 --- a/application/en/creating-links.texy +++ /dev/null @@ -1,322 +0,0 @@ -Creating URL Links -****************** - -
    - -Creating links in Nette is as simple as pointing a finger. Just aim, and the framework will do all the work for you. We will show: - -- how to create links in templates and elsewhere -- how to distinguish a link to the current page -- what to do with invalid links - -
    - - -Thanks to [bidirectional routing |routing], you will never have to hardcode URLs of your application into templates or code, which might change later or be complicated to assemble. In the link, just specify the presenter and action, pass any parameters, and the framework will generate the URL itself. Actually, it's very similar to calling a function. You'll like this. - - -In the Presenter Template -========================= - -Most often, we create links in templates, and the `n:href` attribute is a great helper: - -```latte -detail -``` - -Notice that instead of the HTML attribute `href`, we used the [n:attribute |latte:syntax#n:attributes] `n:href`. Its value is not a URL, as would be the case with the `href` attribute, but the name of the presenter and action. - -Clicking on a link is, simply put, something like calling the `ProductPresenter::renderShow()` method. And if it has parameters in its signature, we can call it with arguments: - -```latte -product detail -``` - -It is also possible to pass named parameters. The following link passes the parameter `lang` with the value `en`: - -```latte -product detail -``` - -If the `ProductPresenter::renderShow()` method does not have `$lang` in its signature, it can retrieve the parameter's value using `$lang = $this->getParameter('lang')` or from a [property |presenters#Request Parameters]. - -If the parameters are stored in an array, they can be expanded using the `...` operator: - -```latte -{var $args = [$product->id, lang => en]} -product detail -``` - -So-called [persistent parameters |presenters#Persistent Parameters] are also automatically passed in links. - -The `n:href` attribute is very handy for HTML `` tags. If we want to print the link elsewhere, for example in text, we use `{link}`: - -```latte -URL is: {link Home:default} -``` - - -In the Code -=========== - -The `link()` method is used to create a link in the presenter: - -```php -$url = $this->link('Product:show', $product->id); -``` - -Parameters can also be passed as an array, where named parameters can also be specified: - -```php -$url = $this->link('Product:show', [$product->id, 'lang' => 'en']); -``` - -Links can also be created without a presenter, using the [#LinkGenerator] and its `link()` method. - -Sometimes you need to create a link now but generate the actual URL only later. For that, there is the `lazyLink()` method, which returns a `Nette\Application\UI\Link` object. The advantage is that you can pass this object on, for example into a template, and before it is rendered you can still adjust its parameters using the `setParameter()` method. The URL itself is assembled only when the object is converted to a string: - -```php -$link = $this->lazyLink('Product:show', $id); -// ... -echo $link; // the URL is generated only here -``` - - -Links to Presenter -================== - -If the target of the link is a presenter and action, it has this syntax: - -``` -[//] [[[[:]module:]presenter:]action | this] [#fragment] -``` - -This format is supported by all Latte tags and all presenter methods that work with links, i.e., `n:href`, `{link}`, `{plink}`, `link()`, `lazyLink()`, `isLinkCurrent()`, `redirect()`, `redirectPermanent()`, `forward()`, `canonicalize()`, and also [#LinkGenerator]. So even if `n:href` is used in the examples, any of these functions could be there. - -The basic form is therefore `Presenter:action`: - -```latte -home page -``` - -If we are linking to an action of the current presenter, we can omit its name: - -```latte -home page -``` - -If the target action is `default`, we can omit it, but the colon must remain: - -```latte -home page -``` - -Links can also point to other [modules |directory-structure#Presenters and Templates]. Here, links are distinguished as relative to a nested submodule, or absolute. The principle is analogous to disk paths, only colons are used instead of slashes. Assuming the current presenter is part of the `Front` module, we would write: - -```latte -link to Front:Shop:Product:show -link to Admin:Product:show -``` - -A special case is a link [to itself |#Link to Current Page], where we specify `this` as the target. - -```latte -refresh -``` - -We can link to a specific part of the page via a so-called fragment after the hash sign `#`: - -```latte -link to Home:default and fragment #main -``` - -.{data-version:3.3.0} -The fragment can also be set dynamically as an argument with the `#` key. Its value is automatically encoded and takes precedence over the fragment specified in the destination: - -```php -$this->link('Home:default', ['#' => $fragment]); -``` - - -Absolute Paths -============== - -Links generated using `link()` or `n:href` are always absolute paths (i.e., they start with `/`), but not absolute URLs with protocol and domain like `https://domain`. - -To generate an absolute URL, add two slashes at the beginning (e.g., `n:href="//Home:"`). Alternatively, you can switch the presenter to generate only absolute links by setting `$this->absoluteUrls = true`. - -The `|absoluteUrl` filter can also be used in the template to convert a relative path to an absolute path. - - -Link to Current Page -==================== - -The target `this` creates a link to the current page: - -```latte -refresh -``` - -At the same time, all parameters specified in the signature of the `action()` or `render()` method are transferred (if `action()` is not defined). So if we are on the `Product:show` page with `id: 123`, the link to `this` will also pass this parameter. - -Of course, it is possible to specify parameters directly: - -```latte -refresh -``` - -The `isLinkCurrent()` function checks if the link target is identical to the current page. This can be used, for example, in a template to distinguish links, etc. - -The parameters are the same as for the `link()` method, but it is also possible to use the wildcard `*` instead of a specific action, which means any action of the given presenter. - -```latte -{if !isLinkCurrent('Admin:login')} - Login -{/if} - -
  • - ... -
  • -``` - -In combination with `n:href` in a single element, a shorthand form can be used: - -```latte -... -``` - -The wildcard `*` can only be used instead of the action, not the presenter. - -To determine if we are in a specific module or its submodule, use the `isModuleCurrent(moduleName)` method. - -```latte -
  • - ... -
  • -``` - - -Changing Link Base .{data-version:3.2.7} -======================================== - -By default, relative links are derived from the current presenter. This can be changed using `{linkBase}`: - -```latte -{linkBase Admin:Dashboard} -product detail -``` - -The link will lead to `Admin:Dashboard:Product:show`. Only relative links are affected - absolute links starting with a colon and links to the current presenter (`this`, `show`) remain unchanged. - -`{linkBase}` applies to the entire template and is especially useful in layout templates, where it ensures consistent links regardless of the calling presenter. -The tag must be placed at the beginning of the template, otherwise it throws a `CompileException`. - - -Links to Signal -=============== - -The target of a link doesn't have to be just a presenter and action, but also a [signal |components#Signal] (it calls the `handle()` method). Then the syntax is as follows: - -``` -[//] [sub-component:]signal! [#fragment] -``` - -The signal is thus distinguished by an exclamation mark: - -```latte -signal -``` - -You can also create a link to a signal of a subcomponent (or sub-subcomponent): - -```latte -signal -``` - - -Links in Component -================== - -Because [components|components] are separate reusable units that should not have any ties to surrounding presenters, links work a bit differently here. The Latte attribute `n:href` and the tag `{link}`, as well as component methods like `link()` and others, **always consider the link target as the signal name**. Therefore, it is not even necessary to include an exclamation mark: - -```latte -signal, not an action -``` - -If we wanted to link to presenters in the component template, we would use the `{plink}` tag: - -```latte -home -``` - -or in the code - -```php -$this->getPresenter()->link('Home:default') -``` - - -Aliases .{data-version:3.2.3} -============================= - -Sometimes it can be useful to assign an easily memorable alias to a Presenter:action pair. For example, naming the homepage `Front:Home:default` simply as `home` or `Admin:Dashboard:default` as `admin`. - -Aliases are defined in the [configuration|configuration] under the key `application › aliases`: - -```neon -application: - aliases: - home: Front:Home:default - admin: Admin:Dashboard:default - sign: Front:Sign:in -``` - -In links, they are then written using an at sign, for example: - -```latte -administration -``` - -They are also supported in all methods that work with links, such as `redirect()` and similar. - - -Invalid Links -============= - -It may happen that we create an invalid link - either because it leads to a non-existent presenter, or because it passes more parameters than the target method accepts in its signature, or when a URL cannot be generated for the target action. How to handle invalid links is set in the presenter using `$this->invalidLinkMode`. It can take a combination of these values (constants): - -- `Presenter::InvalidLinkSilent` - silent mode, returns the character # as the URL -- `Presenter::InvalidLinkWarning` - an E_USER_WARNING warning is thrown, which will be logged in production mode, but will not interrupt script execution -- `Presenter::InvalidLinkTextual` - visual warning, prints the error directly into the link -- `Presenter::InvalidLinkException` - throws InvalidLinkException - -The default setting is `InvalidLinkWarning` in production mode and `InvalidLinkWarning | InvalidLinkTextual` in development mode. `InvalidLinkWarning` in the production environment does not cause script interruption, but the warning will be logged. In the development environment, [Tracy |tracy:] catches it and displays a bluescreen. `InvalidLinkTextual` works by returning an error message as the URL, starting with the characters `#error:`. To make such links noticeable at first glance, add the following to your CSS: - -```css -a[href^="#error:"] { - background: red; - color: white; -} -``` - -If we do not want warnings to be produced in the development environment, we can suppress them directly in the [configuration|configuration]. - -```neon -application: - silentLinks: true -``` - - -LinkGenerator -============= - -How to create links with similar convenience to the `link()` method, but without the presence of a presenter? That's what [api:Nette\Application\LinkGenerator] is for. - -LinkGenerator is a service that you can have passed via the constructor and then create links using its `link()` method. - -There is a difference compared to presenters. LinkGenerator creates all links directly as absolute URLs. Furthermore, there is no "current presenter", so it is not possible to specify only the action name `link('default')` as the target or use relative paths to modules. - -Invalid links always throw `Nette\Application\UI\InvalidLinkException`. - -{{maintitle: Creating Links in Nette: Templates, Presenters, LinkGenerator}} -{{description: Link to presenters, actions and signals with n:href and link(), highlight the current page in a menu, catch invalid links and generate URLs outside presenters.}} diff --git a/application/en/directory-structure.texy b/application/en/directory-structure.texy deleted file mode 100644 index 2c60db28d2..0000000000 --- a/application/en/directory-structure.texy +++ /dev/null @@ -1,529 +0,0 @@ -Directory Structure of the Application -************************************** - -
    - -How to design a clear and scalable directory structure for projects in Nette Framework? We will show you proven practices that will help you organize your code. You will learn: - -- how to **logically structure** the application into directories -- how to design the structure so that it **scales well** as the project grows -- what are the **possible alternatives** and their advantages or disadvantages - -
    - - -It is important to mention that Nette Framework itself does not enforce any specific structure. It is designed to be easily adaptable to any needs and preferences. - - -Basic Project Structure -======================= - -Although Nette Framework does not dictate any fixed directory structure, there is a proven default arrangement in the form of the [Web Project|https://github.com/nette/web-project]: - -/--pre -web-project/ -├── app/ ← application directory -├── assets/ ← SCSS, JS files, images..., alternatively resources/ -├── bin/ ← scripts for command line -├── config/ ← configuration -├── log/ ← logged errors -├── temp/ ← temporary files, cache -├── tests/ ← tests -├── vendor/ ← libraries installed by Composer -└── www/ ← public directory (document-root) -\-- - -You can modify this structure freely according to your needs - rename or move folders. Then you just need to adjust the relative paths to directories in `Bootstrap.php` and possibly `composer.json`. Nothing more is needed, no complex reconfiguration, no changes to constants. Nette has smart autodetection and automatically recognizes the application's location, including its base URL. - - -Code Organization Principles -============================ - -When you first explore a new project, you should be able to quickly orient yourself. Imagine clicking on the `app/Model/` directory and seeing this structure: - -/--pre -app/Model/ -├── Services/ -├── Repositories/ -└── Entities/ -\-- - -From this, you only learn that the project uses some services, repositories, and entities. You learn nothing about the actual purpose of the application. - -Let's look at a different approach - **organization by domains**: - -/--pre -app/Model/ -├── Cart/ -├── Payment/ -├── Order/ -└── Product/ -\-- - -Here it's different - at first glance, it's clear that this is an e-shop. The directory names themselves reveal what the application can do - it works with payments, orders, and products. - -The first approach (organization by class type) brings several problems in practice: code that is logically related is fragmented across different folders, and you have to jump between them. Therefore, we will organize by domains. - - -Namespaces ----------- - -It is customary for the directory structure to correspond to the namespaces in the application. This means that the physical location of files matches their namespace. For example, a class located in `app/Model/Product/ProductRepository.php` should have the namespace `App\Model\Product`. This principle helps in navigating the code and simplifies autoloading. - - -Singular vs Plural in Names ---------------------------- - -Notice that we use singular for the main application directories: `app`, `config`, `log`, `temp`, `www`. The same applies inside the application: `Model`, `Core`, `Presentation`. This is because each represents a single cohesive concept. - -Similarly, `app/Model/Product` represents everything related to products. We don't call it `Products` because it's not a folder full of products (that would contain files like `nokia.php`, `samsung.php`). It's a namespace containing classes for working with products - `ProductRepository.php`, `ProductService.php`. - -The folder `app/Tasks` is plural because it contains a set of separate executable scripts - `CleanupTask.php`, `ImportTask.php`. Each of them is an independent unit. - -For consistency, we recommend using: -- Singular for namespaces representing a functional unit (even if working with multiple entities) -- Plural for collections of independent units -- In case of uncertainty, or if you don't want to think about it, choose singular - - -Public Directory `www/` -======================= - -This directory is the only one accessible from the web (the document-root). You might often encounter the name `public/` instead of `www/` - it's just a matter of convention and does not affect the application's functionality. The directory contains: -- Application [entry point |bootstrapping#index.php] `index.php` -- `.htaccess` file with rules for mod_rewrite (for Apache) -- Static files (CSS, JavaScript, images) -- Uploaded files - -For proper application security, it is crucial to have the [document-root configured correctly |nette:troubleshooting#How to Change or Remove www Directory from URL]. - -.[note] -Never place the `node_modules/` folder in this directory - it contains thousands of files that might be executable and should not be publicly accessible. - - -Application Directory `app/` -============================ - -This is the main directory containing the application code. Basic structure: - -/--pre -app/ -├── Core/ ← infrastructure concerns -├── Model/ ← business logic -├── Presentation/ ← presenters and templates -├── Tasks/ ← command scripts -└── Bootstrap.php ← application bootstrap class -\-- - -`Bootstrap.php` is the [application startup class|bootstrapping] that initializes the environment, loads configuration, and creates the DI container. - -Let's now look at the individual subdirectories in more detail. - - -Presenters and Templates -======================== - -The presentation part of the application is located in the `app/Presentation` directory. An alternative is the shorter `app/UI`. This is the place for all presenters, their templates, and any associated helper classes. - -We organize this layer according to domains. In a complex project combining an e-shop, blog, and API, the structure would look like this: - -/--pre -app/Presentation/ -├── Shop/ ← e-shop frontend -│ ├── Product/ -│ ├── Cart/ -│ └── Order/ -├── Blog/ ← blog -│ ├── Home/ -│ └── Post/ -├── Admin/ ← administration -│ ├── Dashboard/ -│ └── Products/ -└── Api/ ← API endpoints - └── V1/ -\-- - -Conversely, for a simple blog, we would use the following structure: - -/--pre -app/Presentation/ -├── Front/ ← website frontend -│ ├── Home/ -│ └── Post/ -├── Admin/ ← administration -│ ├── Dashboard/ -│ └── Posts/ -├── Error/ -└── Export/ ← RSS, sitemaps, etc. -\-- - -Folders like `Home/` or `Dashboard/` contain presenters and templates. Folders like `Front/`, `Admin/`, or `Api/` are called **modules**. Technically, these are regular directories used for the logical partitioning of the application. - -Each folder containing a presenter includes the presenter file itself and its templates. For example, the `Dashboard/` folder contains: - -/--pre -Dashboard/ -├── DashboardPresenter.php ← presenter -└── default.latte ← template -\-- - -This directory structure is reflected in the class namespaces. For example, `DashboardPresenter` is located in the `App\Presentation\Admin\Dashboard` namespace (see [#Presenter Mapping]): - -```php -namespace App\Presentation\Admin\Dashboard; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -We refer to the `Dashboard` presenter within the `Admin` module in the application using colon notation as `Admin:Dashboard`. Its `default` action is then referred to as `Admin:Dashboard:default`. For nested modules, we use multiple colons, for example, `Shop:Order:Detail:default`. - - -Flexible Structure Development ------------------------------- - -One of the great advantages of this structure is how elegantly it adapts to the growing needs of the project. As an example, let's take the part generating XML feeds. Initially, we have a simple form: - -/--pre -Export/ -├── ExportPresenter.php ← one presenter for all exports -├── sitemap.latte ← template for sitemap -└── feed.latte ← template for RSS feed -\-- - -Over time, more feed types are added, and we need more logic for them... No problem! The `Export/` folder simply becomes a module: - -/--pre -Export/ -├── Sitemap/ -│ ├── SitemapPresenter.php -│ └── sitemap.latte -└── Feed/ - ├── FeedPresenter.php - ├── amazon.latte ← feed for Amazon - └── ebay.latte ← feed for eBay -\-- - -This transformation is completely smooth - just create new subfolders, divide the code into them and update links (e.g. from `Export:feed` to `Export:Feed:amazon`). Thanks to this, we can gradually expand the structure as needed; the nesting level is not limited in any way. - -For example, if in the administration you have many presenters related to order management, such as `OrderDetail`, `OrderEdit`, `OrderDispatch`, etc., you can create a module (folder) named `Order` for better organization, which will contain (folders for) presenters `Detail`, `Edit`, `Dispatch`, and others. - - -Template Location ------------------ - -In the previous examples, we saw that templates are located directly in the folder with the presenter: - -/--pre -Dashboard/ -├── DashboardPresenter.php ← presenter -├── DashboardTemplate.php ← optional template class -└── default.latte ← template -\-- - -This location proves to be the most convenient in practice - you have all related files readily available. - -Alternatively, you can place templates in a `templates/` subfolder. Nette supports both variants. You can even place templates completely outside the `Presentation/` folder. Everything about template location options can be found in the chapter [Finding Templates |templates#Template Lookup]. - - -Helper Classes and Components ------------------------------ - -Presenters and templates often come with other helper files. We place them logically according to their scope: - -1. **Directly with the presenter** in the case of specific components for that presenter: - -/--pre -Product/ -├── ProductPresenter.php -├── ProductGrid.php ← component for product listing -└── FilterForm.php ← form for filtering -\-- - -2. **For the module** - we recommend using the `Accessory` folder, which is placed conveniently at the beginning alphabetically: - -/--pre -Front/ -├── Accessory/ -│ ├── NavbarControl.php ← components for frontend -│ └── TemplateFilters.php -├── Product/ -└── Cart/ -\-- - -3. **For the entire application** - in `Presentation/Accessory/`: -/--pre -app/Presentation/ -├── Accessory/ -│ ├── LatteExtension.php -│ └── TemplateFilters.php -├── Front/ -└── Admin/ -\-- - -Alternatively, you can place helper classes like `LatteExtension.php` or `TemplateFilters.php` in the infrastructure folder `app/Core/Latte/`. And components in `app/Components`. The choice depends on team conventions. - - -Model - Heart of the Application -================================ - -The model contains all the business logic of the application. The rule for its organization is again - structure by domains: - -/--pre -app/Model/ -├── Payment/ ← everything about payments -│ ├── PaymentFacade.php ← main entry point -│ ├── PaymentRepository.php -│ ├── Payment.php ← entity -├── Order/ ← everything about orders -│ ├── OrderFacade.php -│ ├── OrderRepository.php -│ ├── Order.php -└── Shipping/ ← everything about shipping -\-- - -In the model, you typically encounter these types of classes: - -**Facades**: represent the main entry point into a specific domain within the application. They act as an orchestrator, coordinating cooperation between various services to implement complete use-cases (like "create order" or "process payment"). Beneath its orchestration layer, the facade hides implementation details from the rest of the application, thereby providing a clean interface for working with the given domain. - -```php -class OrderFacade -{ - public function createOrder(Cart $cart): Order - { - // validation - // order creation - // email sending - // writing to statistics - } -} -``` - -**Services**: focus on specific business operations within a domain. Unlike facades, which orchestrate entire use-cases, a service implements specific business logic (like price calculations or payment processing). Services are typically stateless and can be used either by facades as building blocks for more complex operations or directly by other parts of the application for simpler tasks. - -```php -class PricingService -{ - public function calculateTotal(Order $order): Money - { - // price calculation - } -} -``` - -**Repositories**: handle all communication with the data storage, typically a database. Their task is to load and save entities and implement methods for searching them. A repository shields the rest of the application from the implementation details of the database and provides an object-oriented interface for working with data. - -```php -class OrderRepository -{ - public function find(int $id): ?Order - { - } - - public function findByCustomer(int $customerId): array - { - } -} -``` - -**Entities**: objects representing the main business concepts in the application, which have their own identity and change over time. Typically, these are classes mapped to database tables using an ORM (like Nette Database Explorer or Doctrine). Entities can contain business rules related to their data and validation logic. - -```php -// Entity mapped to the 'orders' database table -class Order extends Nette\Database\Table\ActiveRow -{ - public function addItem(Product $product, int $quantity): void - { - $this->related('order_items')->insert([ - 'product_id' => $product->id, - 'quantity' => $quantity, - 'unit_price' => $product->price, - ]); - } -} -``` - -**Value Objects**: immutable objects representing values without their own identity - for example, a monetary amount or an email address. Two instances of a value object with the same values are considered identical. - - -Infrastructure Code -=================== - -The `Core/` folder (or alternatively `Infrastructure/`) is home to the technical foundation of the application. Infrastructure code typically includes: - -/--pre -app/Core/ -├── Router/ ← routing and URL management -│ └── RouterFactory.php -├── Security/ ← authentication and authorization -│ ├── Authenticator.php -│ └── Authorizator.php -├── Logging/ ← logging and monitoring -│ ├── SentryLogger.php -│ └── FileLogger.php -├── Cache/ ← caching layer -│ └── FullPageCache.php -└── Integration/ ← integration with external services - ├── Slack/ - └── Stripe/ -\-- - -For smaller projects, a flat structure is naturally sufficient: - -/--pre -Core/ -├── RouterFactory.php -├── Authenticator.php -└── QueueMailer.php -\-- - -This is code that: - -- Handles technical infrastructure (routing, logging, caching) -- Integrates external services (Sentry, Elasticsearch, Redis) -- Provides basic services for the entire application (mail, database) -- Is mostly independent of a specific domain - cache or logger works the same for an e-shop or a blog. - -Are you wondering whether a certain class belongs here or in the model? The key difference is that code in `Core/`: - -- Knows nothing about the domain (products, orders, articles) -- Can usually be transferred to another project -- Solves "how it works" (how to send an email), not "what it does" (which email to send) - -An example for better understanding: - -- `App\Core\MailerFactory` - creates instances of the class for sending emails, handles SMTP settings -- `App\Model\OrderMailer` - uses `MailerFactory` to send emails about orders, knows their templates and when they should be sent - - -Command Scripts -=============== - -Applications often need to perform activities outside of regular HTTP requests - whether it's background data processing, maintenance, or periodic tasks. Simple scripts in the `bin/` directory are used for execution, while the actual implementation logic is placed in `app/Tasks/` (or `app/Commands/`). - -Example: - -/--pre -app/Tasks/ -├── Maintenance/ ← maintenance scripts -│ ├── CleanupCommand.php ← deleting old data -│ └── DbOptimizeCommand.php ← database optimization -├── Integration/ ← integration with external systems -│ ├── ImportProducts.php ← import from supplier system -│ └── SyncOrders.php ← order synchronization -└── Scheduled/ ← regular tasks - ├── NewsletterCommand.php ← sending newsletters - └── ReminderCommand.php ← customer notifications -\-- - -What belongs in the model and what in command scripts? For example, the logic for sending a single email is part of the model, while the bulk sending of thousands of emails belongs in `Tasks/`. - -Tasks are usually run from the command line or via cron: the script in `bin/` creates the DI container using the [bootConsoleApplication() |bootstrapping#Different Environments] method and pulls the required service from it. They can also be run via an HTTP request, but security must be considered. The presenter that runs the task needs to be secured, for example, only for logged-in users or with a strong token and access from allowed IP addresses. For long-running tasks, it is necessary to increase the script time limit and use `session_write_close()` to avoid locking the session. - - -Other Possible Directories -========================== - -In addition to the mentioned basic directories, you can add other specialized folders according to project needs. Let's look at the most common ones and their use: - -/--pre -app/ -├── Api/ ← API logic independent of the presentation layer -├── Database/ ← migration scripts and seeders for test data -├── Components/ ← shared visual components across the entire application -├── Event/ ← useful if using an event-driven architecture -├── Mail/ ← email templates and related logic -└── Utils/ ← helper classes -\-- - -For shared visual components used in presenters across the application, you can use the `app/Components` or `app/Controls` folder: - -/--pre -app/Components/ -├── Form/ ← shared form components -│ ├── SignInForm.php -│ └── UserForm.php -├── Grid/ ← components for data listings -│ └── DataGrid.php -└── Navigation/ ← navigation elements - ├── Breadcrumbs.php - └── Menu.php -\-- - -This is where components with more complex logic belong. If you want to share components between multiple projects, it is advisable to extract them into a separate Composer package. - -In the `app/Mail` directory, you can place email communication management: - -/--pre -app/Mail/ -├── templates/ ← email templates -│ ├── order-confirmation.latte -│ └── welcome.latte -└── OrderMailer.php -\-- - - -Presenter Mapping -================= - -Mapping defines the rules for deriving the class name from the presenter name. We specify them in the [configuration|configuration] under the key `application › mapping`. - -On this page, we have shown that we place presenters in the `app/Presentation` folder (or `app/UI`). Since Nette Application 3.3, this is the default convention that does not need to be configured. If you use a different structure or want to specify the mapping explicitly, the default setting corresponds to this line: - -```neon -application: - mapping: App\Presentation\*\**Presenter -``` - -How does mapping work? For a better understanding, let's first imagine an application without modules. We want the presenter classes to fall under the `App\Presentation` namespace, so that the `Home` presenter maps to the `App\Presentation\HomePresenter` class. This is achieved with this configuration: - -```neon -application: - mapping: App\Presentation\*Presenter -``` - -Mapping works by replacing the asterisk in the mask `App\Presentation\*Presenter` with the presenter name `Home`, resulting in the final class name `App\Presentation\HomePresenter`. Simple! - -However, as you see in the examples in this and other chapters, we place presenter classes in eponymous subdirectories, for example, the `Home` presenter maps to the class `App\Presentation\Home\HomePresenter`. We achieve this by using double asterisks `**` (requires Nette Application 3.2.3): - -```neon -application: - mapping: App\Presentation\**Presenter -``` - -Now we proceed to mapping presenters into modules. We can define specific mapping for each module: - -```neon -application: - mapping: - Front: App\Presentation\Front\**Presenter - Admin: App\Presentation\Admin\**Presenter - Api: App\Api\*Presenter -``` - -According to this configuration, the presenter `Front:Home` maps to the class `App\Presentation\Front\Home\HomePresenter`, while the presenter `Api:OAuth` maps to the class `App\Api\OAuthPresenter`. - -Since the `Front` and `Admin` modules have a similar mapping pattern, and there will likely be more such modules, it is possible to create a general rule that replaces them. A new asterisk for the module is added to the class mask: - -```neon -application: - mapping: - *: App\Presentation\*\**Presenter - Api: App\Api\*Presenter -``` - -It also works for deeper nested directory structures, such as the presenter `Admin:User:Edit`, where the segment with the asterisk repeats for each module level, resulting in the class `App\Presentation\Admin\User\Edit\EditPresenter`. - -An alternative notation is to use an array consisting of three segments instead of a string. For the examples shown above, this notation is equivalent to the previous one: - -```neon -application: - mapping: - *: [App\Presentation, *, **Presenter] - Api: [App\Api, '', *Presenter] -``` - -{{maintitle: Nette Directory Structure: Organize Your PHP Project}} -{{description: A clear, scalable structure for Nette projects: where presenters, templates, the model and infrastructure code belong, and how presenter mapping works.}} diff --git a/application/en/how-it-works.texy b/application/en/how-it-works.texy deleted file mode 100644 index 0445eed8ac..0000000000 --- a/application/en/how-it-works.texy +++ /dev/null @@ -1,203 +0,0 @@ -How Do Applications Work? -************************* - -
    - -You are currently reading the foundational chapter of the Nette documentation. You will learn the complete principles behind how web applications work, from A to Z, from the moment a request is born until the PHP script completes execution. After reading, you will understand: - -- how it all works -- what Bootstrap, Presenter, and the DI container are -- what the directory structure looks like - -
    - - -Directory Structure -=================== - -Open the example skeleton of a web application called [WebProject|https://github.com/nette/web-project]. As you read, you can refer to the files being discussed. - -The directory structure looks something like this: - -/--pre -web-project/ -├── app/ ← application directory -│ ├── Core/ ← core classes necessary for operation -│ │ └── RouterFactory.php ← URL address configuration -│ ├── Presentation/ ← presenters, templates & co. -│ │ ├── @layout.latte ← layout template -│ │ └── Home/ ← Home presenter directory -│ │ ├── HomePresenter.php ← Home presenter class -│ │ └── default.latte ← template for default action -│ └── Bootstrap.php ← booting class Bootstrap -├── assets/ ← resources (SCSS, TypeScript, source images) -├── bin/ ← scripts executed from the command line -├── config/ ← configuration files -│ ├── common.neon -│ └── services.neon -├── log/ ← logged errors -├── temp/ ← temporary files, cache, … -├── vendor/ ← libraries installed by Composer -│ ├── ... -│ └── autoload.php ← autoloading of all installed packages -├── www/ ← public directory, document root of the project -│ ├── assets/ ← compiled static files (CSS, JS, images, ...) -│ ├── .htaccess ← mod_rewrite rules -│ └── index.php ← initial file that launches the application -└── .htaccess ← prohibits access to all directories except www -\-- - -You can change the directory structure in any way, rename or move folders; it is completely flexible. Nette also features smart autodetection and automatically recognizes the application's location, including its URL base. - -For slightly larger applications, we can organize presenter and template folders into [subdirectories |directory-structure#Presenters and Templates] and group classes into namespaces, which we call modules. - -The `www/` directory represents the public directory or document-root of the project. You can rename it without needing to configure anything else on the application side. You just need to [configure the hosting |nette:troubleshooting#How to Change or Remove www Directory from URL] so that the document-root points to this directory. - -You can also download WebProject directly, including Nette, using [Composer |best-practices:composer]: - -```shell -composer create-project nette/web-project -``` - -On Linux or macOS, set [write permissions |nette:troubleshooting#Setting Directory Permissions] for the `log/` and `temp/` directories. - -The WebProject application is ready to run; there is no need to configure anything at all, and you can view it directly in the browser by accessing the `www/` folder. - - -HTTP Request -============ - -Everything starts when a user opens a page in their browser. The browser sends an HTTP request to the server. This request targets a single PHP file located in the public directory `www/`, which is `index.php`. Let's assume the request is for the address `https://example.com/product/123`. Thanks to appropriate [server configuration |nette:troubleshooting#How to Configure a Server for Nice URLs], even this URL is mapped to the `index.php` file, which is then executed. - -Its task is to: - -1) initialize the environment -2) obtain the factory -3) run the Nette application, which handles the request - -What factory? We're not producing tractors, we're building websites! Hold on, it will be explained shortly. - -By 'environment initialization', we mean, for example, activating [Tracy|tracy:], which is an amazing tool for logging or visualizing errors. On a production server, it logs errors; in a development environment, it displays them directly. Thus, initialization also includes determining whether the site is running in production or development mode. Nette uses [smart autodetection |bootstrapping#Development vs Production Mode] for this: if you run the site on localhost, it operates in development mode. You don't need to configure anything, and the application is immediately ready for both development and live deployment. These steps are performed and described in detail in the chapter about the [Bootstrap class|bootstrapping]. - -The third point (yes, we skipped the second, but we'll return to it) is launching the application. Handling HTTP requests in Nette is the responsibility of the `Nette\Application\Application` class (hereafter `Application`). So, when we say run the application, we specifically mean calling the aptly named `run()` method on an object of this class. - -Nette acts as a mentor, guiding you to write clean applications according to proven methodologies. One of the most established of these is **dependency injection**, abbreviated as DI. We don't want to burden you with explaining DI right now; there's a [dedicated chapter|dependency-injection:introduction] for that. The essential consequence is that key objects are typically created by an object factory known as the **DI container** (or DIC). Yes, this is the factory mentioned earlier. It also produces the `Application` object for us, which is why we need the container first. We obtain it using the `Configurator` class, let it create the `Application` object, call the `run()` method on it, and thus the Nette application starts. This is precisely what happens in the [index.php |bootstrapping#index.php] file. - - -Nette Application -================= - -The `Application` class has a single task: to respond to the HTTP request. - -Applications written in Nette are divided into many so-called presenters (you might encounter the term 'controller' in other frameworks, which is essentially the same thing). These are classes, each representing a specific page of the website: e.g., the homepage, a product in an e-shop, a login form, a sitemap feed, etc. An application can have anywhere from one to thousands of presenters. - -The `Application` starts by asking the so-called router to decide which presenter should handle the current request. The router determines the responsibility. It examines the input URL `https://example.com/product/123` and, based on its configuration, decides that this task belongs, for example, to the `Product` **presenter**, which should perform the `show` **action** for the product with `id: 123`. It's good practice to write the presenter + action pair separated by a colon, like `Product:show`. - -Thus, the router transformed the URL into the pair `Presenter:action` + parameters, in our case `Product:show` + `id: 123`. You can see what such a router looks like in the file `app/Core/RouterFactory.php`, and we describe it in detail in the [Routing |Routing] chapter. - -Let's move on. The `Application` now knows the name of the presenter and can proceed. It does this by creating an instance of the `ProductPresenter` class, which contains the code for the `Product` presenter. More precisely, it asks the DI container to create the presenter, because creating objects is its responsibility. - -The presenter might look like this: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ProductRepository $repository, - ) { - } - - public function renderShow(int $id): void - { - // obtain data from the model and pass it to the template - $this->template->product = $this->repository->getProduct($id); - } -} -``` - -The presenter takes over handling the request. The task is clear: execute the `show` action with `id: 123`. In presenter terminology, this means the `renderShow()` method is called, receiving `123` in the `$id` parameter. - -A presenter can handle multiple actions, meaning it can have multiple `render()` methods. However, we recommend designing presenters with one or as few actions as possible. - -So, the `renderShow(123)` method was called. Its code is a fictional example, but it demonstrates how data is passed to the template, specifically by writing to `$this->template`. - -Subsequently, the presenter returns a response. This could be an HTML page, an image, an XML document, sending a file from the disk, JSON, or perhaps a redirect to another page. Importantly, if we don't explicitly specify how to respond (which is the case with `ProductPresenter`), the response will be to render a template into an HTML page. Why? Because in 99% of cases, we want to render a template. Therefore, the presenter adopts this behavior as the default to simplify our work. That's the essence of Nette. - -We don't even need to specify which template to render; the framework deduces the path automatically. In the case of the `show` action, it simply attempts to load the `show.latte` template located in the same directory as the `ProductPresenter` class. It also tries to find the layout in the `@layout.latte` file (more details on [template lookup |templates#Template Lookup]). - -Then, the templates are rendered. This completes the task of the presenter and the entire application. If the template doesn't exist, a 404 error page is returned. You can learn more about presenters on the [Presenters|presenters] page. - -[* request-flow.svg *] - -To be sure, let's recap the entire process with a slightly different URL: - -1) The URL is `https://example.com` -2) The application boots, the DI container is created, and `Application::run()` is executed. -3) The router decodes the URL into the pair `Home:default`. -4) An instance of the `HomePresenter` class is created. -5) The `renderDefault()` method is called (if it exists). -6) The template, e.g., `default.latte`, is rendered along with the layout, e.g., `@layout.latte`. - - -You might have encountered many new concepts just now, but we believe they make sense. Developing applications in Nette is remarkably straightforward. - - -Templates -========= - -Speaking of templates, Nette uses the [Latte |latte:] templating system. That's why template files have the `.latte` extension. Latte is used primarily because it's the most secure templating system for PHP, and also the most intuitive. You don't need to learn much new; knowledge of PHP and a few tags is sufficient. You'll find everything you need [in the documentation |templates]. - -In the template, you [create links |creating-links] to other presenters & actions like this: - -```latte -product detail -``` - -Simply write the familiar `Presenter:action` pair instead of the actual URL and include any necessary parameters. The trick lies in `n:href`, which tells Nette to process this attribute. It will then generate: - -```latte -product detail -``` - -URL generation is handled by the aforementioned router. Routers in Nette are exceptional because they can perform not only the transformation from a URL to a `Presenter:action` pair but also the reverse: generating a URL from the presenter name, action, and parameters. Thanks to this, you can completely change the URL format throughout your entire finished application in Nette without altering a single character in the templates or presenters - simply by modifying the router. This also enables so-called canonization, another unique Nette feature that enhances SEO (Search Engine Optimization) by automatically preventing duplicate content from existing on different URLs. Many programmers find this capability astounding. - - -Interactive Components -====================== - -We need to tell you one more thing about presenters: they have a built-in component system. Those with more experience might recall something similar from Delphi or ASP.NET Web Forms; React or Vue.js are built on somewhat related concepts. In the world of PHP frameworks, this is a completely unique feature. - -Components are independent, reusable units that we embed into pages (i.e., presenters). These can be [forms |forms:in-presenter], [datagrids |https://componette.org/contributte/datagrid/], menus, polls - essentially anything that makes sense to reuse. We can create our own components or utilize some from the [vast selection |https://componette.org] of open-source components. - -Components fundamentally influence the approach to application development. They open up new possibilities for composing pages from pre-prepared units. And they also have something in common with [Hollywood |components#Hollywood Style]. - - -DI Container and Configuration -============================== - -The DI container, or object factory, is the heart of the entire application. - -Don't worry, it's not some magical black box, as the preceding lines might suggest. In reality, it's a rather mundane PHP class generated by Nette and stored in the cache directory. It contains many methods named like `createServiceAbcd()`, each capable of creating and returning a specific object. Yes, there's also a `createServiceApplication__application()` method that produces the `Nette\Application\Application` instance we needed in `index.php` to run the application. There are also methods for creating individual presenters, and so on. - -The objects created by the DI container are, for some reason, called services. - -What's truly special about this class is that you don't program it - the framework does. It actually generates the PHP code and saves it to disk. You simply provide instructions on which objects the container should be able to create and how exactly. These instructions are written in [configuration files |bootstrapping#DI Container Configuration], which use the [NEON|neon:format] format and thus have the `.neon` extension. - -Configuration files serve purely to instruct the DI container. So, for example, if you specify the `expiration: 14 days` option in the [session |http:configuration#Session] section, the DI container, when creating the `Nette\Http\Session` object representing the session, will call its `setExpiration('14 days')` method, thereby making the configuration a reality. - -There's an entire chapter prepared for you describing what can be [configured |nette:configuring] and how to [define your own services |dependency-injection:services]. - -Once you delve a bit into service creation, you'll encounter the term [autowiring |dependency-injection:autowiring]. This is a feature that will simplify your life incredibly. It can automatically pass objects where you need them (for example, in the constructors of your classes) without you having to do anything. You'll discover that the DI container in Nette is a small miracle. - - -What Next? -========== - -We've covered the fundamental principles of Nette applications. While it's been a surface-level overview so far, you'll soon delve deeper and, in time, create amazing web applications. Where to go next? Have you tried the [Create Your First Application|quickstart:] tutorial yet? - -In addition to what's described above, Nette offers a whole arsenal of [useful classes|utils:], a [database layer|database:], etc. Try clicking through the documentation. Or visit the [blog|https://blog.nette.org]. You'll discover many interesting things. - -May the framework bring you much joy 💙 - -{{maintitle: How Nette Applications Work: From Request to Response}} -{{description: The foundational chapter of the Nette docs: follow a web application step by step from the HTTP request through router and presenter to the rendered template.}} diff --git a/application/en/multiplier.texy b/application/en/multiplier.texy deleted file mode 100644 index d459ebda66..0000000000 --- a/application/en/multiplier.texy +++ /dev/null @@ -1,66 +0,0 @@ -Multiplier: Dynamic Components -****************************** - -.[perex] -A tool for dynamic creation of interactive components. - -Let's start with a typical example: imagine a product list in an e-shop where you want an 'Add to Cart' form for each item. One possible approach is to wrap the entire listing in a single form. However, a much more convenient method is offered by [api:Nette\Application\UI\Multiplier]. - -Multiplier allows you to conveniently define a factory for multiple components. It works on the principle of nested components - any component inheriting from [api:Nette\ComponentModel\Container] can contain other components. - -.[tip] -See the chapter on the [component model |components#Components in Depth] in the documentation. - -The essence of Multiplier is that it acts as a parent that can dynamically create its children using a callback passed in the constructor. See the example: - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function () { - $form = new Nette\Application\UI\Form; - $form->addInteger('amount', 'Amount:') - ->setRequired(); - $form->addSubmit('send', 'Add to cart'); - return $form; - }); -} -``` - -Now, in the template, we can simply render the form for each product - and each one will truly be a unique component. - -```latte -{foreach $items as $item} -

    {$item->title}

    - {$item->description} - - {control "shopForm-$item->id"} -{/foreach} -``` - -The argument passed in the `{control}` tag follows a format that means: - -1. Get the component `shopForm`. -2. From it, get the child named `$item->id`. - -During the first call of point **1**, the `shopForm` component doesn't exist yet, so its factory `createComponentShopForm` is called. Then, on the obtained component (an instance of Multiplier), the factory for the specific form is called - which is the anonymous function we passed to the Multiplier's constructor. - -In the next iteration of the foreach loop, the `createComponentShopForm` method will not be called again (as the component already exists). However, because we are looking for a different child (since `$item->id` will be different in each iteration), the anonymous function will be called again, returning a new form. - -The only thing left is to ensure that the form adds the correct product to the cart - currently, the form is identical for every product. A feature of Multiplier (and generally of any component factory in Nette Framework) helps us here: every factory receives the name of the component being created as its first argument. Additionally, a Multiplier factory receives the Multiplier instance itself as its second argument. In our case, the first argument will be `$item->id`, which is precisely the information we need. So, we just need to slightly modify the form creation: - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function (string $itemId) { - $form = new Nette\Application\UI\Form; - $form->addInteger('amount', 'Amount:') - ->setRequired(); - $form->addHidden('itemId', $itemId); - $form->addSubmit('send', 'Add to cart'); - return $form; - }); -} -``` - -{{maintitle: Nette Multiplier: Create Components Dynamically}} -{{description: Need an Add to Cart form for every product? Multiplier creates as many independent components as you need, so each list item gets its own form or control.}} diff --git a/application/en/presenters.texy b/application/en/presenters.texy deleted file mode 100644 index c331db5633..0000000000 --- a/application/en/presenters.texy +++ /dev/null @@ -1,598 +0,0 @@ -Presenters -********** - -
    - -We will explore how presenters and templates are written in Nette. After reading, you will understand: - -- how presenters work -- what persistent parameters are -- how templates are rendered - -
    - -[We already know |how-it-works#Nette Application] that a presenter is a class representing a specific page of a web application, such as the homepage, a product in an e-shop, a login form, a sitemap feed, etc. An application can have anywhere from one to thousands of presenters. In other frameworks, they are also known as controllers. - -Usually, the term presenter refers to a descendant of the [api:Nette\Application\UI\Presenter] class, which is suitable for generating web interfaces and will be the focus of the rest of this chapter. In a general sense, a presenter is any object implementing the [api:Nette\Application\IPresenter] interface. - - -Presenter Life Cycle -==================== - -The presenter's task is to handle a request and return a response (which could be an HTML page, an image, a redirect, etc.). - -So, initially, a request is passed to it. This isn't the direct HTTP request, but a [api:Nette\Application\Request] object, into which the HTTP request was transformed with the help of the router. We usually don't interact directly with this object, as the presenter cleverly delegates request processing to other methods, which we will now explore. - -[* lifecycle.svg *] *** Presenter Life Cycle .<> - -The diagram shows a list of methods that are called sequentially from top to bottom, if they exist. None of them are mandatory; you can have a completely empty presenter without a single method and build a simple static website upon it. - - -`__construct()` ---------------- - -The constructor doesn't strictly belong to the presenter's life cycle, as it's called at the moment the object is created. However, we mention it due to its importance. The constructor (along with the [inject method|best-practices:inject-method-attribute]) is used for passing dependencies. - -The presenter should not handle the application's business logic, write to or read from the database, perform calculations, etc. That's the responsibility of classes in the layer we call the model. For example, an `ArticleRepository` class might be responsible for loading and saving articles. For the presenter to work with it, it needs to have it [passed via dependency injection |dependency-injection:passing-dependencies]: - - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articles, - ) { - } -} -``` - - -`startup()` ------------ - -Immediately after receiving the request, the `startup()` method is invoked. You can use it to initialize properties, check user permissions, etc. It is required that this method always calls its parent: `parent::startup()`. - - -`action(args...)` .{toc: action()} --------------------------------------------------- - -Similar to the `render()` method. While `render()` is intended to prepare data for a specific template that will subsequently be rendered, `action()` processes a request without necessarily rendering a template afterwards. For example, it might process data, log a user in or out, and so on, and then [redirect elsewhere |#Redirection]. - -It's important that `action()` is called *before* `render()`. This allows us to potentially change the course of the request within the action method, for instance, by changing the template that will be rendered or even the `render()` method that will be called, using `setView('otherView')`. - -.{data-version:3.2.3} -You can even switch to an entirely different action using the `switch('otherAction')` method. It aborts the current method and instead runs the new action's `action()` and `render()` methods (and turns off automatic [canonicalization|#Canonization]). The request itself continues; only the currently running method is interrupted. - -Parameters from the request are passed to the method. It's possible and recommended to specify types for these parameters, e.g., `actionShow(int $id, ?string $slug = null)`. If the `id` parameter is missing or is not an integer, the presenter returns a [404 error |#Error 404 etc] and terminates. - - -`handle(args...)` .{toc: handle()} --------------------------------------------------- - -This method processes so-called signals, which we'll learn about in the chapter dedicated to [components |components#Signal]. It's primarily intended for components and handling AJAX requests. - -Parameters from the request are passed to the method, just like with `action()`, including type checking. - - -`beforeRender()` ----------------- - -The `beforeRender` method, as its name suggests, is called before every `render()` method. It's used for common template configuration, passing variables to the layout, and similar tasks. - - -`render(args...)` .{toc: render()} ----------------------------------------------- - -This is where we prepare the template for subsequent rendering, pass data to it, etc. - -Parameters from the request are passed to the method, just like with `action()`, including type checking. - -```php -public function renderShow(int $id): void -{ - // obtain data from the model and pass it to the template - $this->template->article = $this->articles->getById($id); -} -``` - - -`afterRender()` ---------------- - -The `afterRender` method, as the name suggests again, is called after every `render()` method. It's used rather rarely. - - -`shutdown()` ------------- - -Called at the end of the presenter's life cycle. - - -Events ------- - -In addition to the `startup()`, `beforeRender()`, and `shutdown()` methods, which are called as part of the presenter's life cycle, other functions can be defined to be called automatically. The presenter defines so-called [events |nette:glossary#Events], and you add their handlers to the `$onStartup`, `$onRender`, and `$onShutdown` arrays. - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct() - { - $this->onStartup[] = function () { - // ... - }; - } -} -``` - -Handlers in the `$onStartup` array are called just before the `startup()` method, `$onRender` handlers between `beforeRender()` and `render()`, and finally `$onShutdown` handlers just before `shutdown()`. - - -**A piece of advice before we continue:** As you can see, a presenter can handle multiple actions/views, meaning it can have multiple `render()` methods. However, we recommend designing presenters with one or as few actions as possible. - - -Sending a Response -================== - -The presenter's response is typically [rendering a template into an HTML page|templates], but it can also be sending a file, JSON, or even redirecting to another page. - -At any point during the life cycle, we can use one of the following methods to send a response and simultaneously terminate the presenter: - -- `redirect()`, `redirectPermanent()`, `redirectUrl()`, and `forward()` perform a [redirect |#Redirection] -- `error()` terminates the presenter [due to an error |#Error 404 etc] -- `sendJson($data)` terminates the presenter and [sends data |#Sending JSON] in JSON format -- `sendTemplate()` terminates the presenter and immediately [renders the template |templates] -- `sendResponse($response)` terminates the presenter and sends a [custom response |#Responses] -- `terminate()` terminates the presenter without a response - -Each of these methods immediately terminates the presenter by throwing a silent termination exception `Nette\Application\AbortException`. - -If you don't call any of these methods, the presenter automatically proceeds to render the template. Why? Because in 99% of cases, we want to render a template, so the presenter adopts this behavior as the default to simplify our work. - - -Creating Links -============== - -The presenter has a `link()` method used to create URL links to other presenters. The first parameter is the target presenter & action, followed by arguments, which can be passed as an array: - -```php -$url = $this->link('Product:show', $id); - -$url = $this->link('Product:show', [$id, 'lang' => 'en']); -``` - -In the template, links to other presenters & actions are created like this: - -```latte -product detail -``` - -Simply write the familiar `Presenter:action` pair instead of the actual URL and include any necessary parameters. The trick lies in `n:href`, which tells Latte to process this attribute and generate the real URL. In Nette, you don't need to think about URLs at all, just about presenters and actions. - -You can find more information in the chapter [Creating URL Links|creating-links]. - - -Redirection -=========== - -The `redirect()` and `forward()` methods are used to switch to another presenter. They have a very similar syntax to the [link() |#Creating Links] method. - -The `forward()` method switches to the new presenter immediately without an HTTP redirect: - -```php -$this->forward('Product:show'); -``` - -Example of a temporary redirect with HTTP code 302 (or 303 if the current request method is POST): - -```php -$this->redirect('Product:show', $id); -``` - -To achieve a permanent redirect with HTTP code 301, use this: - -```php -$this->redirectPermanent('Product:show', $id); -``` - -You can redirect to another URL outside the application using the `redirectUrl()` method. The HTTP code can be specified as the second parameter; the default is 302 (or 303 if the current request method is POST): - -```php -$this->redirectUrl('https://nette.org'); -``` - -Redirection immediately terminates the presenter's activity by throwing the so-called silent termination exception, `Nette\Application\AbortException`. - -Before redirection, it's possible to send [#flash messages], i.e., messages that will be displayed in the template after redirection. - - -Flash Messages -============== - -These are messages typically informing about the result of some operation. An important feature of flash messages is that they remain available in the template even after redirection. Once displayed, they stay active for an additional 30 seconds - for instance, if the user refreshes the page due to a transmission error, the message won't disappear immediately. - -Simply call the [flashMessage() |api:Nette\Application\UI\Control::flashMessage()] method, and the presenter handles passing it to the template. The first parameter is the message text, and the optional second parameter is its type (e.g., error, warning, info). The `flashMessage()` method returns an instance of the flash message, allowing additional information to be added. - -```php -$this->flashMessage('The item has been deleted.'); -$this->redirect(/* ... */); // and redirect -``` - -In the template, these messages are available in the `$flashes` variable as `stdClass` objects containing the properties `message` (the message text), `type` (the message type), and potentially the user-added information mentioned earlier. We render them like this: - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -Error 404 etc. -============== - -If the request cannot be fulfilled, for example, because the article we want to display doesn't exist in the database, we throw a 404 error using the `error(string $message = '', int $httpCode = 404)` method. - -```php -public function renderShow(int $id): void -{ - $article = $this->articles->getById($id); - if (!$article) { - $this->error(); - } - // ... -} -``` - -The HTTP error code can be passed as the second parameter; the default is 404. The method works by throwing a `Nette\Application\BadRequestException`, after which the `Application` passes control to the error presenter. This is a presenter whose task is to display a page informing about the error that occurred. The error presenter is configured in the [application configuration|configuration]. - - -Sending JSON -============ - -The `sendJson($data)` method encodes the given data into JSON, sends it as the HTTP response, and terminates the presenter. Example: - -```php -public function actionData(): void -{ - $data = ['hello' => 'nette']; - $this->sendJson($data); -} -``` - - -Request Parameters .{data-version:3.1.14} -========================================= - -The presenter, and also each component, obtains its parameters from the HTTP request. You can retrieve their values using the `getParameter($name)` or `getParameters()` methods. The values are strings or arrays of strings, essentially raw data obtained directly from the URL. - -For greater convenience, we recommend accessing parameters via properties. Simply mark 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 the property, we recommend specifying the data type (e.g., `string`), and Nette will automatically cast the value accordingly. Parameter values can also be [validated |#Validation of Parameters]. - -When creating a link, you can set the parameter's value directly: - -```latte -click -``` - - -Persistent Parameters -===================== - -Persistent parameters are used to maintain state across different requests. Their value remains the same even after clicking a link. Unlike session data, they are transmitted in the URL. And this happens completely automatically, so there's no need to explicitly include them in `link()` or `n:href`. - -An example use case? Imagine you have a multilingual application. The current language is a parameter that must always be part of the URL. But it would be incredibly tedious to include it in every link. So, you make it a persistent parameter `lang`, and it will be carried along automatically. Neat! - -Creating a persistent parameter in Nette is extremely simple. Just create a public property and mark it with the attribute (previously, `/** @persistent */` was used): - -```php -use Nette\Application\Attributes\Persistent; // this line is important - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; // must be public -} -``` - -If `$this->lang` has a value like `'en'`, then links created using `link()` or `n:href` will also contain the parameter `lang=en`. And after clicking the link, `$this->lang` will again be `'en'`. - -For the property, we recommend specifying the data type (e.g., `string`), and you can also provide a default value. Parameter values can be [validated |#Validation of Parameters]. - -Persistent parameters are typically transferred between all actions of a given presenter. To transfer them across multiple presenters as well, they need to be defined either: - -- in a common ancestor from which the presenters inherit -- or in a trait that the presenters use: - -```php -trait LanguageAware -{ - #[Persistent] - public string $lang; -} - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - use LanguageAware; -} -``` - -When creating a link, the value of a persistent parameter can be changed: - -```latte -detail in Czech -``` - -Alternatively, it can be *reset*, i.e., removed from the URL. It will then assume its default value: - -```latte -click -``` - - -Shared Parameter Space -====================== - -The request parameters, [persistent parameters |#Persistent Parameters], and the parameters of the `action`, `render`, and `handle` (signal) methods all share a single space, where each is identified by its name. If the same name appears in more than one of them, they refer to one and the same value. - -This is often used to advantage. For example, a persistent parameter `lang` and the `$lang` argument of an action or signal method are one and the same - you can read the current value of a persistent parameter simply by listing it in the method signature: - -```php -#[Persistent] -public string $lang; - -public function handleSearch(string $query, string $lang): void -{ - // $lang holds the current value of the persistent parameter lang -} -``` - -Because this space is shared, keep parameter names unique unless you deliberately want them to share a value. This applies to signals as well, which additionally read parameters from the request's POST body, see [Signals in Depth |components#Signals in Depth]. - - -Interactive Components -====================== - -Presenters have a built-in component system. Components are separate reusable units that we embed into presenters. They can be [forms |forms:in-presenter], datagrids, menus, essentially anything that makes sense to use repeatedly. - -How are components embedded into presenters and subsequently used? You'll learn this in the [Components |components] chapter. You'll even find out what they have in common with Hollywood. - -And where can you get components? On [Componette |https://componette.org/search/component], you'll find open-source components and many other add-ons for Nette, contributed by volunteers from the framework community. - - -Going Deeper -============ - -.[tip] -What we've covered so far in this chapter will likely be sufficient for most uses. The following sections are intended for those interested in delving deeper into presenters and wanting to know absolutely everything. - - -Validation of Parameters ------------------------- - -The values of [#Request-Parameters] and [#Persistent-Parameters] received from URLs are written to properties by the `loadState()` method. It also checks if the data type specified in the property matches, otherwise it will respond with a 404 error and the page will not be displayed. - -Never blindly trust parameters received from the URL, as they can easily be overwritten by the user. For example, this is how we would verify if the language `$this->lang` is among the supported ones. A suitable way to do this is by overriding the aforementioned `loadState()` method: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; - - public function loadState(array $params): void - { - parent::loadState($params); // $this->lang is set here - // followed by custom value check: - if (!in_array($this->lang, ['en', 'cs'])) { - $this->error(); - } - } -} -``` - - -Save and Restore the Request ----------------------------- - -The request handled by the presenter is a [api:Nette\Application\Request] object, returned by the presenter's `getRequest()` method. - -The current request can be saved to the session or, conversely, restored from it and have the presenter execute it again. This is useful, for example, when a user is filling out a form and their login session expires. To avoid data loss, before redirecting to the login page, we save the current request to the session using `$reqId = $this->storeRequest()`. This returns its identifier as a short string, which we then pass as a parameter to the login presenter. - -After logging in, we call the `$this->restoreRequest($reqId)` method, which retrieves the request from the session. POST requests are forwarded to it, while the others (GET) are redirected to the request's URL. The method verifies that the request was created by the same user who is now logged in. If a different user logs in or the key is invalid, it does nothing, and the program continues as usual. - -See the guide [How to Return to a Previous Page |best-practices:restore-request]. - - -Canonization ------------- - -Presenters have a truly excellent feature that contributes to better SEO (Search Engine Optimization). They automatically prevent the existence of duplicate content at different URLs. If multiple URLs lead to a specific destination, e.g., `/index` and `/index?page=1`, the framework designates one of them as primary (canonical) and redirects the others to it using HTTP code 301. Thanks to this, search engines don't index your pages twice and dilute their page rank. - -This process is called canonization. The canonical URL is the one generated by the [router|routing], typically the first matching route in the collection. - -Canonization is enabled by default and can be disabled via `$this->autoCanonicalize = false`. - -Redirection does not occur during AJAX or POST requests, as this could lead to data loss or would offer no added SEO value. - -You can also trigger canonization manually using the `canonicalize()` method. Similar to the `link()` method, you pass it the presenter, action, and parameters. It generates a link and compares it with the current URL address. If they differ, it redirects to the generated link. - -```php -public function actionShow(int $id, ?string $slug = null): void -{ - $realSlug = $this->facade->getSlugForId($id); - // redirects if $slug is different from $realSlug - $this->canonicalize('Product:show', [$id, $realSlug]); -} -``` - -For a complete pattern that combines route filters with `canonicalize()` to produce SEO-friendly URLs, see [Pretty URLs with Slugs |best-practices:pretty-urls]. - - -Responses ---------- - -The response returned by the presenter is an object implementing the [api:Nette\Application\Response] interface. Several pre-built responses are available: - -- [api:Nette\Application\Responses\CallbackResponse] - sends a callback -- [api:Nette\Application\Responses\FileResponse] - sends the file -- [api:Nette\Application\Responses\ForwardResponse] - forward() -- [api:Nette\Application\Responses\JsonResponse] - sends JSON -- [api:Nette\Application\Responses\RedirectResponse] - redirect -- [api:Nette\Application\Responses\TextResponse] - sends text -- [api:Nette\Application\Responses\VoidResponse] - blank response - -Responses are sent using the `sendResponse()` method: - -```php -use Nette\Application\Responses; - -// Plain text -$this->sendResponse(new Responses\TextResponse('Hello Nette!')); - -// Sends a file -$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf')); - -// Sends a callback -$callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) { - if ($httpResponse->getHeader('Content-Type') === 'text/html') { - echo '

    Hello

    '; - } -}; -$this->sendResponse(new Responses\CallbackResponse($callback)); -``` - -You can also write your own response. Just implement the `Nette\Application\Response` interface, which has a single `send()` method receiving the HTTP request and response. This is useful, for example, when streaming data that you do not want to hold in memory: - -```php -class CsvResponse implements Nette\Application\Response -{ - public function __construct( - private string $fileName, - private iterable $rows, - ) { - } - - public function send(Nette\Http\IRequest $request, Nette\Http\IResponse $response): void - { - $response->setContentType('text/csv', 'utf-8'); - $response->sendAsFile($this->fileName); - - $handle = fopen('php://output', 'w'); - foreach ($this->rows as $row) { - fputcsv($handle, $row); - } - - fclose($handle); - } -} -``` - -You then send it in the presenter as usual: `$this->sendResponse(new CsvResponse('export.csv', $rows));` - - -HTTP Caching ------------- - -The `lastModified()` method makes it easy to take advantage of HTTP caching. You pass it the date and time of the content's last modification (as a timestamp, string, or `DateTimeInterface` object), and optionally an ETag validator (a short string identifying the current version of the content, such as a hash of it) and an expiration time. If the browser already holds a matching version, the presenter sends a `304 Not Modified` response and terminates, so the page isn't rendered or transferred needlessly: - -```php -public function renderArticle(int $id): void -{ - $article = $this->articles->getById($id); - $this->lastModified($article->updatedAt); - // ... -} -``` - - -Completing the Template .{data-version:3.3.0} ---------------------------------------------- - -When the presenter renders a template, the `sendTemplate()` method calls `completeTemplate()` just before rendering. This method fills in the variables marked with the `#[TemplateVariable]` attribute and locates the template file (the default variables are already set by the `TemplateFactory` when the template is created). You can override this protected method to add variables shared across all views or to set a different file: - -```php -protected function completeTemplate(Nette\Application\UI\Template $template): void -{ - parent::completeTemplate($template); - $template->siteName = 'My App'; -} -``` - - -Access Restriction Using `#[Requires]` .{data-version:3.2.3} ------------------------------------------------------------- - -The `#[Requires]` attribute provides advanced options for restricting access to presenters and their methods. It can be used to specify HTTP methods, require an AJAX request, restrict to the same origin, and allow access only via forwarding. The attribute can be applied both to presenter classes and to individual methods like `action()`, `render()`, `handle()`, and `createComponent()`. - -You can specify these restrictions: -- on HTTP methods: `#[Requires(methods: ['GET', 'POST'])]` -- requiring an AJAX request: `#[Requires(ajax: true)]` -- access only from the same origin: `#[Requires(sameOrigin: true)]` -- access only via forwarding: `#[Requires(forward: true)]` -- restrictions on specific actions: `#[Requires(actions: 'default')]` - -.[note] -Since version 3.3, the same-origin match is verified using the browser's `Sec-Fetch-Site` header (previously via a SameSite cookie), which is more reliable and checks the exact match of scheme, domain, and port. - -Details can be found in the guide [How to Use the Requires Attribute |best-practices:attribute-requires]. - - -HTTP Method Check ------------------ - -Presenters in Nette automatically verify the HTTP method of every incoming request, primarily for security reasons. By default, the methods `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH` are allowed. - -If you want to additionally allow, for example, the `OPTIONS` method, use the `#[Requires]` attribute (since Nette Application v3.2.3): - -```php -#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])] -class MyPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Since version 3.1.13, verification is performed in `checkHttpMethod()`, which checks whether the method specified in the request is included in the `$presenter->allowedMethods` array. As of version 3.2.3, this approach is deprecated in favor of `#[Requires]`. You can override the method like this: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - protected function checkHttpMethod(): void - { - $this->allowedMethods[] = 'OPTIONS'; - parent::checkHttpMethod(); - } -} -``` - -It's important to emphasize that if you enable the `OPTIONS` method, you must subsequently handle it appropriately within your presenter. This method is often used as a so-called preflight request, which the browser automatically sends before the actual request when it's necessary to determine if the request is permissible according to the CORS (Cross-Origin Resource Sharing) policy. If you enable the method but don't implement the correct response, it can lead to inconsistencies and potential security problems. - - -Marking Deprecated Actions .{data-version:3.2.3} ------------------------------------------------- - -The `#[Deprecated]` attribute marks actions, signals, or entire presenters as deprecated and scheduled for future removal. When generating links to deprecated parts of the application, Nette throws a warning to alert developers. - -You can apply the attribute to either the entire presenter class or to individual `action()`, `render()`, and `handle()` methods. - - -Further Reading -=============== - -- [Inject methods and attributes |best-practices:inject-method-attribute] -- [Composing presenters from traits |best-practices:presenter-traits] -- [Passing settings to presenters |best-practices:passing-settings-to-presenters] -- [How to Return to a Previous Page |best-practices:restore-request] - -{{maintitle: Nette Presenters: Life Cycle, Actions and Parameters}} -{{description: How a presenter handles a request in Nette, from its life cycle and template rendering to redirects, error pages, JSON responses and persistent parameters.}} diff --git a/application/en/routing.texy b/application/en/routing.texy deleted file mode 100644 index fa2b148882..0000000000 --- a/application/en/routing.texy +++ /dev/null @@ -1,751 +0,0 @@ -Routing -******* - -
    - -The router handles everything related to URL addresses, so you don't have to think about them. We will show you: - -- how to configure the router to make URLs look as you wish -- discuss SEO and redirection -- and demonstrate how to write a custom router - -
    - - -More human-friendly URLs (also known as cool or pretty URLs) are more usable, memorable, and contribute positively to SEO. Nette keeps this in mind and fully caters to developers' needs. You can design the exact URL structure you want for your application. You can even design it when the application is already finished, as it requires no changes to code or templates. It's defined elegantly in [a single place |#Integration], the router, rather than being scattered as annotations throughout all presenters. - -The router in Nette is exceptional because it is **bidirectional.** It can both decode URLs from HTTP requests and create links. Thus, it plays a crucial role in [Nette Application |how-it-works#Nette Application], as it not only decides which presenter and action will execute the current request but is also used for [generating URLs |creating-links] in templates, etc. - -However, the router isn't limited to just this usage; you can use it in applications where presenters aren't used at all, for REST APIs, etc. More details are in the section on [#Standalone Usage]. - - -Route Collection -================ - -The most pleasant way to define the structure of URL addresses in an application is offered by the [api:Nette\Application\Routers\RouteList] class. The definition consists of a list of so-called routes, i.e., masks of URL addresses and their associated presenters and actions using a simple API. We don't need to name the routes in any way. - -```php -$router = new Nette\Application\Routers\RouteList; -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('article/', 'Article:view'); -// ... -``` - -The example shows that if we open `https://domain.com/rss.xml` in the browser, the `Feed` presenter with the `rss` action will be displayed. If `https://domain.com/article/12`, the `Article` presenter with the `view` action will be displayed, etc. If no suitable route is found, Nette Application responds by throwing a [BadRequestException |api:Nette\Application\BadRequestException], which is displayed to the user as a 404 Not Found error page. - - -Order of Routes ---------------- - -The **order** in which the individual routes are listed is absolutely **crucial**, because they are evaluated sequentially from top to bottom. The rule is that we declare routes **from specific to general**: - -```php -// WRONG: 'rss.xml' is captured by the first route and understands this string as -$router->addRoute('', 'Article:view'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// GOOD -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('', 'Article:view'); -``` - -Routes are also evaluated from top to bottom when generating links: - -```php -// WRONG: link to 'Feed:rss' generates as 'admin/feed/rss' -$router->addRoute('admin//', 'Admin:default'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// GOOD -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('admin//', 'Admin:default'); -``` - -We won't hide from you that correctly assembling routes requires some skill. Until you master it, the [routing panel |#Debugging Router] will be a useful tool. - - -Mask and Parameters -------------------- - -The mask describes the relative path from the website's root directory. The simplest mask is a static URL: - -```php -$router->addRoute('products', 'Products:default'); -``` - -Often, masks contain so-called **parameters**. These are enclosed in angle brackets (e.g., ``) and are passed to the target presenter, for example, to the `renderShow(int $year)` method or to the persistent parameter `$year`: - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -The example shows that if we open `https://example.com/chronicle/2020` in the browser, the `History` presenter with the `show` action and the parameter `year: 2020` will be displayed. - -We can specify a default value for parameters directly in the mask, making them optional: - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -The route will now also accept the URL `https://example.com/chronicle/`, which will again display `History:show` with the parameter `year: 2020`. - -Of course, the presenter and action names can also be parameters. For example: - -```php -$router->addRoute('/', 'Home:default'); -``` - -The specified route accepts, for example, URLs in the form `/article/edit` or `/catalog/list` and understands them as presenters and actions `Article:edit` and `Catalog:list`, respectively. - -At the same time, it gives the parameters `presenter` and `action` default values `Home` and `default`, making them optional as well. Thus, the route also accepts a URL like `/article` and understands it as `Article:default`. Or conversely, a link to `Product:default` generates the path `/product`, and a link to the default `Home:default` generates the path `/`. - -The mask can describe not only the relative path from the website's root directory but also an absolute path if it starts with a slash, or even the entire absolute URL if it starts with two slashes: - -```php -// relative to the document root -$router->addRoute('/', /* ... */); - -// absolute path (relative to the domain) -$router->addRoute('//', /* ... */); - -// absolute URL including domain (relative to the scheme) -$router->addRoute('//.example.com//', /* ... */); - -// absolute URL including scheme -$router->addRoute('https://.example.com//', /* ... */); -``` - - -Validation Expressions ----------------------- - -A validation condition can be specified for each parameter using a [regular expression|https://www.php.net/manual/en/reference.pcre.pattern.syntax.php]. For example, for the parameter `id`, we specify that it can only contain digits using the regex `\d+`: - -```php -$router->addRoute('/[/]', /* ... */); -``` - -The default regular expression for all parameters is `[^/]+`, i.e., everything except a slash. If a parameter is supposed to accept slashes as well, we set the expression to `.+`: - -```php -// accepts https://example.com/a/b/c, path will be 'a/b/c' -$router->addRoute('', /* ... */); -``` - - -Optional Sequences ------------------- - -In the mask, optional parts can be marked using square brackets. Any part of the mask can be optional, and it can contain parameters: - -```php -$router->addRoute('[/]', /* ... */); - -// Accepts paths: -// /en/download => lang => en, name => download -// /download => lang => null, name => download -``` - -When a parameter is part of an optional sequence, it naturally becomes optional too. If it doesn't have a specified default value, it will be null. - -Optional parts can also be in the domain: - -```php -$router->addRoute('//[.]example.com//', /* ... */); -``` - -Sequences can be nested and combined arbitrarily: - -```php -$router->addRoute( - '[[-]/][/page-]', - 'Home:default', -); - -// Accepts paths: -// /en/hello -// /en-us/hello -// /hello -// /hello/page-12 -``` - -When generating URLs, the shortest variant is preferred, so everything that can be omitted is omitted. Therefore, for example, the route `index[.html]` generates the path `/index`. This behavior can be reversed by placing an exclamation mark after the left square bracket: - -```php -// accepts /hello and /hello.html, generates /hello -$router->addRoute('[.html]', /* ... */); - -// accepts /hello and /hello.html, generates /hello.html -$router->addRoute('[!.html]', /* ... */); -``` - -Optional parameters (i.e., parameters with a default value) without square brackets essentially behave as if they were enclosed in the following way: - -```php -$router->addRoute('//', /* ... */); - -// corresponds to this: -$router->addRoute('[/[/[]]]', /* ... */); -``` - -If we want to influence the behavior of the trailing slash, so that, for example, `/home` is generated instead of `/home/`, this can be achieved as follows: - -```php -$router->addRoute('[[/[/]]]', /* ... */); -``` - - -Wildcards ---------- - -In the absolute URL mask, we can use the following wildcards to avoid, for example, having to write the domain into the mask, which might differ between development and production environments: - -- `%tld%` = top level domain, e.g., `com` or `org` -- `%sld%` = second level domain, e.g., `example` -- `%domain%` = domain without subdomains, e.g., `example.com` -- `%host%` = entire host, e.g., `www.example.com` -- `%basePath%` = path to the root directory - -```php -$router->addRoute('//www.%domain%/%basePath%//', /* ... */); -$router->addRoute('//www.%sld%.%tld%/%basePath%//', /* ... */); -``` - - -Advanced Notation ------------------ - -The route target, usually written in the format `Presenter:action`, can also be written using an array that defines individual parameters and their default values: - -```php -$router->addRoute('/[/]', [ - 'presenter' => 'Home', - 'action' => 'default', -]); -``` - -For more detailed specification, an even more extended form can be used, where besides default values, we can set other parameter properties, such as a validation regular expression (see the `id` parameter): - -```php -use Nette\Routing\Route; - -$router->addRoute('/[/]', [ - 'presenter' => [ - Route::Value => 'Home', - ], - 'action' => [ - Route::Value => 'default', - ], - 'id' => [ - Route::Pattern => '\d+', - ], -]); -``` - -It is important to note that if parameters defined in the array are not listed in the path mask, their values cannot be changed, not even using query parameters specified after the question mark in the URL. - -This is useful for **fixed parameters** - giving a specific page a short, memorable URL. For example, to make `/tos` always open `Article:view` with `id: 123`: - -```php -$router->addRoute('tos', [ - 'presenter' => 'Article', - 'action' => 'view', - 'id' => 123, -]); -``` - - -Filters and Translations ------------------------- - -We write the application's source code in English, but if the website needs to have Czech URLs, then simple routing like: - -```php -$router->addRoute('/', 'Home:default'); -``` - -will generate English URLs, such as `/product/123` or `/cart`. If we want presenters and actions in the URL to be represented by Czech words (e.g., `/produkt/123` or `/kosik`), we can use a translation dictionary. To write it, we already need the "more verbose" variant of the second parameter: - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterTable => [ - // string in URL => presenter - 'produkt' => 'Product', - 'kosik' => 'Cart', - 'katalog' => 'Catalog', - ], - ], - 'action' => [ - Route::Value => 'default', - Route::FilterTable => [ - 'seznam' => 'list', - ], - ], -]); -``` - -Multiple keys in the translation dictionary can lead to the same presenter. This creates various aliases for it. The last key is considered the canonical variant (i.e., the one that will be in the generated URL). - -The translation table can be used in this way for any parameter. If a translation doesn't exist, the original value is taken. We can change this behavior by adding `Route::FilterStrict => true`, and the route will then reject the URL if the value is not in the dictionary. - -In addition to the translation dictionary in the form of an array, custom translation functions can be deployed. - -```php -use Nette\Routing\Route; - -$router->addRoute('//', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterIn => function (string $s): string { /* ... */ }, - Route::FilterOut => function (string $s): string { /* ... */ }, - ], - 'action' => 'default', - 'id' => null, -]); -``` - -The `Route::FilterIn` function converts between the parameter in the URL and the string that is then passed to the presenter; the `FilterOut` function ensures the conversion in the opposite direction. - -The parameters `presenter`, `action`, and `module` already have predefined filters that convert between PascalCase or camelCase style and the kebab-case used in URLs. The default value of the parameters is written in the form in which it is passed to the application (PascalCase for presenter and module, camelCase for action), so for example, in the case of a presenter, we write ``, not ``. - - -General Filters ---------------- - -Besides filters intended for specific parameters, we can also define general filters that receive an associative array of all parameters, which they can modify in any way and then return. General filters are defined under the empty key. - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => 'Home', - 'action' => 'default', - '' => [ - Route::FilterIn => function (array $params): array { /* ... */ }, - Route::FilterOut => function (array $params): array { /* ... */ }, - ], -]); -``` - -General filters provide the ability to modify the route's behavior in absolutely any way. We can use them, for example, to modify parameters based on other parameters. For instance, translating `` and `` based on the current value of the `` parameter. - -If a parameter has its own filter defined and a general filter also exists, the custom `FilterIn` is executed before the general one, and conversely, the general `FilterOut` is executed before the custom one. Thus, inside the general filter, the values of the parameters `presenter` and `action` are written in PascalCase or camelCase style, respectively. - -See [Pretty URLs with Slugs |best-practices:pretty-urls] for a practical use of these filters - generating SEO-friendly URLs like `/article/123-how-to-bake-bread` without modifying any templates. - - -OneWay Flag ------------ - -One-way routes are used to maintain the functionality of old URLs that the application no longer generates but still accepts. We mark them with the `OneWay` flag: - -```php -// old URL /product-info?id=123 -$router->addRoute('product-info', 'Product:detail', oneWay: true); -// new URL /product/123 -$router->addRoute('product/', 'Product:detail'); -``` - -When accessing the old URL, the presenter automatically redirects to the new URL, so search engines won't index these pages twice (see [#SEO and Canonization]). - - -Dynamic Routing with Callbacks ------------------------------- - -Dynamic routing with callbacks allows you to directly assign functions (callbacks) to routes, which are executed when the given path is visited. This flexible functionality allows you to quickly and efficiently create various endpoints for your application: - -```php -$router->addRoute('test', function () { - echo 'You are at the /test address'; -}); -``` - -You can also define parameters in the mask, which are automatically passed to your callback: - -```php -$router->addRoute('', function (string $lang) { - echo match ($lang) { - 'cs' => 'Welcome to the Czech version of our website!', - 'en' => 'Welcome to the English version of our website!', - }; -}); -``` - -Besides parameters from the mask, the callback can also receive services from the DI container. They are passed based on the parameter type. Additionally, the parameter `$presenter` receives an instance of [MicroPresenter |api:NetteModule\MicroPresenter], which processes the route: - -```php -$router->addRoute('', function (string $lang, Nette\Http\Request $httpRequest, NetteModule\MicroPresenter $presenter) { - // ... -}); -``` - - -Modules -------- - -If we have multiple routes that belong to a common [module |directory-structure#Presenters and Templates], we use `withModule()`. The given module is automatically prepended to the presenter of every route in the group, and it disappears from the URL entirely: - -```php -$router = new RouteList; -$router->withModule('Forum') // the following routes are part of the Forum module - ->addRoute('rss', 'Feed:rss') // presenter will be Forum:Feed - ->addRoute('/') - - ->withModule('Admin') // the following routes are part of the Forum:Admin module - ->addRoute('sign:in', 'Sign:in'); -``` - -An alternative is the `module` parameter, which likewise sets a fixed module and keeps it out of the URL: - -```php -// URL manage/dashboard/default maps to presenter Admin:Dashboard -$router->addRoute('manage//', [ - 'module' => 'Admin', -]); -``` - -Every presenter name is complete only together with its module, e.g. `Front:Admin:ProductList`. Whenever such a full name lands in a URL parameter, the router encodes it by two simple rules: every colon `:` (the module separator) becomes a **dot**, and every word boundary in a PascalCase name becomes a **dash**. So `Front:Admin:ProductList` appears in the URL as `front.admin.product-list` and is decoded back the same way. This is why a modular application, without any of the tools above, produces URLs full of dots. - -Both `withModule()` and the `module` parameter avoid this precisely because they strip a known module prefix off the presenter name before it reaches the URL: since the module is a constant, it doesn't need to be encoded at all. - -Sometimes we want the module itself to vary and to appear in the URL, so we reach for `` directly in the mask. Beware of one crucial detail: **`` captures the whole module path** - everything up to the last colon in the presenter name. For the presenter `Shop:Admin:Product` that means the module `Shop:Admin` and the presenter `Product`, and since colons become dots, we get: - - -Subdomains ----------- - -Route collections can be divided according to subdomains: - -```php -$router = new RouteList; -$router->withDomain('example.com') - ->addRoute('rss', 'Feed:rss') - ->addRoute('/'); -``` - -[#Wildcards] can also be used in the domain name: - -```php -$router = new RouteList; -$router->withDomain('example.%tld%') - // ... -``` - - -Path Prefix ------------ - -Route collections can be divided according to the path in the URL: - -```php -$router = new RouteList; -$router->withPath('eshop') - ->addRoute('rss', 'Feed:rss') // matches URL /eshop/rss - ->addRoute('/'); // matches URL /eshop// -``` - - -Combinations ------------- - -The above groupings can be combined with each other: - -```php -$router = (new RouteList) - ->withDomain('admin.example.com') - ->withModule('Admin') - ->addRoute(/* ... */) - ->addRoute(/* ... */) - ->end() - ->withModule('Images') - ->addRoute(/* ... */) - ->end() - ->end() - ->withDomain('example.com') - ->withPath('export') - ->addRoute(/* ... */) - // ... -``` - - -Query Parameters ----------------- - -Masks can also contain query parameters (parameters after the question mark in the URL). A validation expression cannot be defined for these, but the name under which they are passed to the presenter can be changed: - -```php -// we want to use the query parameter 'cat' under the name 'categoryId' in the application -$router->addRoute('product ? id= & cat=', /* ... */); -``` - - -Foo Parameters --------------- - -Now we're going deeper. Foo parameters are essentially unnamed parameters that allow matching a regular expression. An example is a route accepting `/index`, `/index.html`, `/index.htm`, and `/index.php`: - -```php -$router->addRoute('index', /* ... */); -``` - -It is also possible to explicitly define the string that will be used when generating the URL. The string must be placed directly after the question mark. The following route is similar to the previous one, but generates `/index.html` instead of `/index`, because the string `.html` is set as the generation value: - -```php -$router->addRoute('index', /* ... */); -``` - - -Integration -=========== - -To integrate the created router into the application, we need to tell the DI container about it. The easiest way is to prepare a factory that will create the router object and tell the container in the configuration to use it. Let's say we write the method `App\Core\RouterFactory::createRouter()` for this purpose: - -```php -namespace App\Core; - -use Nette\Application\Routers\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute(/* ... */); - return $router; - } -} -``` - -Then we write in the [configuration |dependency-injection:services]: - -```neon -services: - - App\Core\RouterFactory::createRouter -``` - -Any dependencies, such as on a database, etc., are passed to the factory method as its parameters using [autowiring|dependency-injection:autowiring]: - -```php -public static function createRouter(Nette\Database\Connection $db): RouteList -{ - // ... -} -``` - - -SimpleRouter -============ - -A much simpler router than the route collection is [SimpleRouter |api:Nette\Application\Routers\SimpleRouter]. We use it when we don't have special requirements for the URL format, if `mod_rewrite` (or its alternatives) is not available, or if we don't want to deal with pretty URLs yet. - -It generates addresses roughly in this form: - -``` -http://example.com/?presenter=Product&action=detail&id=123 -``` - -The parameter of the `SimpleRouter` constructor is a default presenter & action, i.e. action to be executed if we open e.g. `http://example.com/` without additional parameters. - -```php -// the default presenter will be 'Home' and action 'default' -$router = new Nette\Application\Routers\SimpleRouter('Home:default'); -``` - -We recommend defining SimpleRouter directly in [configuration |dependency-injection:services]: - -```neon -services: - - Nette\Application\Routers\SimpleRouter('Home:default') -``` - - -SEO and Canonization -==================== - -The framework contributes to SEO (Search Engine Optimization) by preventing duplicate content on different URLs. If multiple addresses lead to a certain destination, e.g., `/index` and `/index.html`, the framework designates the first one as primary (canonical) and redirects the others to it using HTTP code 301. Thanks to this, search engines do not index pages twice and do not dilute their page rank. - -This process is called canonization. The canonical URL is the one generated by the router, i.e. by the first matching route in the collection without the OneWay flag. Therefore, in the collection, we list **primary routes first**. - -Canonization is performed by the presenter, more in the chapter [canonization |presenters#Canonization]. - - -HTTPS -===== - -To use the HTTPS protocol, it is necessary to enable it on the hosting and configure the server correctly. - -Redirecting the entire website to HTTPS must be set at the server level, for example, using the `.htaccess` file in the root directory of our application, with HTTP code 301. The settings may vary depending on the hosting and look something like this: - -``` - - RewriteEngine On - ... - RewriteCond %{HTTPS} off - RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] - ... - -``` - -The router generates URLs with the same protocol as the page was loaded, so nothing more needs to be set. - -However, if we exceptionally need different routes to run under different protocols, we specify it in the route mask: - -```php -// Will generate an HTTP address -$router->addRoute('http://%host%//', /* ... */); - -// Will generate an HTTPS address -$router->addRoute('https://%host%//', /* ... */); -``` - - -Debugging Router -================ - -The routing panel displayed in the [Tracy Bar |tracy:] is a useful helper that shows a list of routes and also the parameters that the router obtained from the URL. - -The green bar with the symbol ✓ represents the route that processed the current URL; blue color and the symbol ≈ indicate routes that would also process the URL if the green one hadn't overtaken them. Further, we see the current presenter & action. - -[* routing-debugger.webp *] - -At the same time, if an unexpected redirect occurs due to [canonization |#SEO and Canonization], it is useful to look at the panel in the *redirect* bar, where you can find out how the router originally understood the URL and why it redirected. - -.[note] -When debugging the router, we recommend opening Developer Tools in the browser (Ctrl+Shift+I or Cmd+Option+I) and disabling the cache in the Network panel so that redirects are not stored in it. - - -Performance -=========== - -The number of routes affects the speed of the router. Their number should definitely not exceed several dozen. If your website has too complicated a URL structure, you can write a custom [#Custom Router]. - -If the router has no dependencies, for example, on a database, and its factory accepts no arguments, we can serialize its compiled form directly into the DI container and thus slightly speed up the application. - -```neon -routing: - cache: true -``` - - -Custom Router -============= - -The following lines are intended for very advanced users. You can create your own router and naturally integrate it into the collection of routes. The router is an implementation of the [api:Nette\Routing\Router] interface with two methods: - -```php -use Nette\Http\IRequest as HttpRequest; -use Nette\Http\UrlScript; - -class MyRouter implements Nette\Routing\Router -{ - public function match(HttpRequest $httpRequest): ?array - { - // ... - } - - public function constructUrl(array $params, UrlScript $refUrl): ?string - { - // ... - } -} -``` - -The `match` method processes the current request [$httpRequest |http:request], from which not only the URL but also headers, etc., can be obtained, into an array containing the presenter name and its parameters. If it cannot process the request, it returns null. When processing the request, we must return at least the presenter; the action is optional and defaults to `default` if not specified. The presenter name is complete and includes any modules: - -```php -[ - 'presenter' => 'Front:Home', - 'action' => 'default', -] -``` - -The `constructUrl` method, on the contrary, constructs the resulting absolute URL from the array of parameters. It can use information from the [`$refUrl`|api:Nette\Http\UrlScript] parameter, which is the current URL. - -Add it to the route collection using `add()`: - -```php -$router = new Nette\Application\Routers\RouteList; -$router->add($myRouter); -$router->addRoute(/* ... */); -// ... -``` - - -Standalone Usage -================ - -By standalone usage, we mean utilizing the router's capabilities in an application that does not use Nette Application and presenters. Almost everything we have shown in this chapter applies to it, with these differences: - -- for route collections, we use the [api:Nette\Routing\RouteList] class -- as a simple router, the [api:Nette\Routing\SimpleRouter] class -- because the `Presenter:action` pair does not exist, we use [#Advanced Notation] - -So again, we create a method that will assemble the router for us, e.g.: - -```php -namespace App\Core; - -use Nette\Routing\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute('rss.xml', [ - 'controller' => 'RssFeedController', - ]); - $router->addRoute('article/', [ - 'controller' => 'ArticleController', - ]); - // ... - return $router; - } -} -``` - -If you use a DI container, which we recommend, add the method to the configuration again, and then obtain the router along with the HTTP request from the container: - -```php -$router = $container->getByType(Nette\Routing\Router::class); -$httpRequest = $container->getByType(Nette\Http\IRequest::class); -``` - -Or create the objects directly: - -```php -$router = App\Core\RouterFactory::createRouter(); -$httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); -``` - -Now all that remains is to let the router do its work: - -```php -$params = $router->match($httpRequest); -if ($params === null) { - // no matching route found, send a 404 error - exit; -} - -// process the obtained parameters -$controller = $params['controller']; -// ... -``` - -And conversely, use the router to construct a link: - -```php -$params = ['controller' => 'ArticleController', 'id' => 123]; -$url = $router->constructUrl($params, $httpRequest->getUrl()); -``` - - -{{composer: nette/routing}} -{{repo: nette/routing}} -{{api: https://api.nette.org/routing/}} -{{maintitle: PHP Routing with Nette Router: Clean URLs and SEO}} -{{description: Define all URLs in one place and change them anytime without touching templates. Route masks, automatic canonicalization, custom routers and standalone use.}} diff --git a/application/en/templates.texy b/application/en/templates.texy deleted file mode 100644 index e51a220d1d..0000000000 --- a/application/en/templates.texy +++ /dev/null @@ -1,422 +0,0 @@ -Templates -********* - -.[perex] -Nette uses the [Latte |latte:] templating system. Latte is used because it is the most secure templating system for PHP, and at the same time, the most intuitive system. You don't need to learn much new; knowledge of PHP and a few tags will suffice. - -It's common for a page to be composed of a layout template + the template for the specific action. This is what a layout template might look like; notice the `{block}` blocks and the `{include}` tag: - -```latte - - - - {block title}My App{/block} - - -
    ...
    - {include content} -
    ...
    - - -``` - -And this would be the action template: - -```latte -{block title}Homepage{/block} - -{block content} -

    Homepage

    -... -{/block} -``` - -It defines the `content` block, which is inserted in place of `{include content}` in the layout, and also re-defines the `title` block, which overwrites `{block title}` in the layout. Try to imagine the result. - - -Template Lookup ---------------- - -In presenters, you don't need to specify which template should be rendered; the framework automatically deduces the path, saving you from writing it. - -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: - -/--pre -app/ -└── Presentation/ - └── Home/ - ├── HomePresenter.php - └── default.latte -\-- - -If you use a structure where presenters are together in one directory and templates are in a `templates` folder, save it either in the file `..latte` or `/.latte`: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── Home/ - │ └── default.latte ← 1st variant - └── Home.default.latte ← 2nd variant -\-- - -The `templates` directory can also be placed one level higher, i.e., at the same level as the directory with presenter classes. - -If the template is not found, the presenter responds with a [404 - page not found error |presenters#Error 404 etc]. - -You can change the view using `$this->setView('otherView')`. It is also possible to directly specify the template file using `$this->template->setFile('/path/to/template.latte')`. - -.[note] -The files where templates are looked up can be changed by overriding the [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()] method, which returns an array of possible file names. - - -Layout Template Lookup ----------------------- - -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 it, or one 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 these locations: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── @layout.latte ← common layout - ├── Home/ - │ └── @layout.latte ← only for Home, 1st variant - └── Home.@layout.latte ← only for Home, 2nd variant -\-- - -If the presenter is located in a module, it will also search further up the directory levels, according to the module nesting. - -The layout name can be changed using `$this->setLayout('layoutAdmin')`, and then it will be expected in the file `@layoutAdmin.latte`. You can also directly specify the layout template file using `$this->setLayout('/path/to/template.latte')`. - -Using `$this->setLayout(false)` or the `{layout none}` tag inside the template disables layout lookup. - -.[note] -The files where layout templates are looked up can be changed by overriding the [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()] method, which returns an array of possible file names. - - -Template Variables ------------------- - -Variables are passed to templates by writing them to `$this->template`. They then become available in the template as local variables: - -```php -$this->template->article = $this->articles->getById($id); -``` - -To automatically pass a property value to the template as a variable, mark it with the `#[TemplateVariable]` attribute and public visibility: .{data-version:3.2.9} - -```php -use Nette\Application\Attributes\TemplateVariable; - -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - #[TemplateVariable] - public string $siteName = 'My blog'; -} -``` - -If you pass a variable with the same name to the template, `#[TemplateVariable]` won't override it. - - -Default Variables ------------------ - -Presenters and components automatically pass several useful variables to templates: - -- `$basePath` is the absolute URL path to the root directory (e.g., `/eshop`) -- `$baseUrl` is the absolute URL to the root directory (e.g., `http://localhost/eshop`) -- `$user` is an object [representing the user |security:authentication] -- `$presenter` is the current presenter -- `$control` is the current component or presenter -- `$flashes` is an array of [messages |presenters#Flash Messages] sent by the `flashMessage()` function - -If you use a custom template class, these variables are passed if you create a property for them. - - -Type-Safe Templates -------------------- - -When developing robust applications, it's useful to explicitly define which variables the template expects and their types. This provides type checking in PHP, smart hints in your IDE, and enables static analysis to catch errors. - -How do you define such a list? Simply as a class with properties representing template variables. Name it similarly to the presenter, just with `Template` at the end: - -```php -/** - * @property-read ArticleTemplate $template - */ -class ArticlePresenter extends Nette\Application\UI\Presenter -{ -} - -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - public Model\Article $article; - public Nette\Security\User $user; - - // and other variables -} -``` - -The `$this->template` object in the presenter will now be an instance of the `ArticleTemplate` class. PHP will thus check the declared types when writing. - -Nette chooses the template class automatically. First it looks for a class named `Template`, e.g. `ArticleEditTemplate` for the `edit` action, and only if it doesn't exist does it fall back to `Template`. - -The `@property-read` annotation is for the IDE and static analysis, enabling code completion, see "PhpStorm and code completion for $this⁠-⁠>⁠template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template. - -[* phpstorm-completion.webp *] - -You can also use code completion directly in templates. Just install the Latte plugin for PhpStorm and specify the template parameter class name at the beginning of the template, more in the chapter [Latte: type system |latte:type-system]: - -```latte -{templateType App\Presentation\Article\ArticleTemplate} -... -``` - -The same applies to components. Just follow the naming convention and create a parameter class `FifteenTemplate` for a component like `FifteenControl`. - -If you need to use a different parameter class, use the `createTemplate()` method: - -```php -public function renderDefault(): void -{ - $template = $this->createTemplate(SpecialTemplate::class); - $template->foo = 123; - // ... - $this->sendTemplate($template); -} -``` - -.{data-version:3.3.0} -If you need to influence how the template is finalized before rendering - for example to add variables shared across all actions - you can override the `completeTemplate()` method in the presenter. It is called just before the template is rendered: - -```php -protected function completeTemplate(Nette\Application\UI\Template $template): void -{ - parent::completeTemplate($template); - $template->siteName = 'My blog'; -} -``` - - -Creating Links --------------- - -In the template, links to other presenters & actions are created this way: - -```latte -product detail -``` - -The `n:href` attribute is very handy for HTML `` tags. If we want to print the link elsewhere, for example in text, we use `{link}`: - -```latte -URL is: {link Home:default} -``` - -More information can be found in the chapter [Creating URL Links|creating-links]. - - -Custom Filters, Tags, etc. --------------------------- - -The Latte templating system can be extended with custom filters, functions, tags, and other elements. There are three approaches available, ranging from quick ad-hoc solutions to architectural patterns for entire applications. - -**Ad-hoc in Presenter Methods** - -The quickest approach is adding filters or functions directly in presenter or component code. In presenters, the `beforeRender()` or `render()` methods work well for this: - -```php -protected function beforeRender(): void -{ - // adding a filter - $this->template->addFilter('money', fn($val) => '$' . number_format($val, 2)); - - // adding a function - $this->template->addFunction('isWeekend', fn($date) => $date->format('N') >= 6); -} -``` - -In the template: - -```latte -

    Price: {$price|money}

    - -{if isWeekend($now)} ... {/if} -``` - -For more complex logic, you can configure the `Latte\Engine` object directly: - -```php -protected function beforeRender(): void -{ - $latte = $this->template->getLatte(); - $latte->setFeature(Latte\Feature::MigrationWarnings); -} -``` - -**Using Attributes** - -A more elegant approach is defining filters and functions as methods directly in the presenter or component's [template parameter class|#Type-safe templates], marked with attributes: - -```php -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - #[Latte\Attributes\TemplateFilter] - public function money(float $val): string - { - return '$' . number_format($val, 2); - } - - #[Latte\Attributes\TemplateFunction] - public function isWeekend(DateTimeInterface $date): bool - { - return $date->format('N') >= 6; - } -} -``` - -Latte automatically discovers and registers methods marked with these attributes. The filter or function name in templates matches the method name. These methods must be public. - -**Globally Using Extensions** - -The previous approaches suit filters and functions needed only in specific presenters or components, not application-wide. For the entire application, creating an [extension |latte:extending-latte#Latte Extension] works best. This class centralizes all Latte extensions for your project. A brief example: - -```php -namespace App\Presentation\Accessory; - -final class LatteExtension extends Latte\Extension -{ - public function __construct( - private App\Model\Facade $facade, - private Nette\Security\User $user, - // ... - ) { - } - - public function getFilters(): array - { - return [ - 'timeAgoInWords' => $this->filterTimeAgoInWords(...), - 'money' => $this->filterMoney(...), - // ... - ]; - } - - public function getFunctions(): array - { - return [ - 'canEditArticle' => - fn($article) => $this->facade->canEditArticle($article, $this->user->getId()), - // ... - ]; - } - - private function filterTimeAgoInWords(DateTimeInterface $time): string - { - // ... - } - - // ... -} -``` - -Register the extension through [configuration |configuration#Latte Templates]: - -```neon -latte: - extensions: - - App\Presentation\Accessory\LatteExtension -``` - -Extensions offer several advantages: dependency injection support, access to your application's model layer, and centralized management of all extensions. They also support custom tags, providers, compiler passes, and more. - - -Setting Up All Templates ------------------------- - -The `TemplateFactory` service, which creates all templates, offers a public `$onCreate` array of callbacks. These are called every time any template is created, so you can set up filters, functions, or variables for all templates in the application from a single place. Each callback receives the newly created template. Have the `TemplateFactory` service [injected |dependency-injection:passing-dependencies] and register the callbacks, e.g. during application startup: - -```php -$templateFactory->onCreate[] = function (Nette\Bridges\ApplicationLatte\Template $template): void { - $template->addFilter('money', fn($val) => '$' . number_format($val, 2)); -}; -``` - - -Translating ------------ - -If you are programming a multilingual application, you will likely need to output some texts in the template in different languages. Nette Framework defines a translation interface [api:Nette\Localization\Translator] for this purpose, which has a single method `translate()`. It accepts the message `$message`, which is usually a string, and any other parameters. The task is to return the translated string. Nette does not have a default implementation; you can choose from several ready-made solutions available on [Componette |https://componette.org/search/localization] according to your needs. Their documentation explains how to configure the translator. - -Templates can be set up with a translator, which we [get passed |dependency-injection:passing-dependencies], using the `setTranslator()` method: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator); -} -``` - -Alternatively, the translator can be set using [configuration |configuration#Latte Templates]: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -Then, the translator can be used, for example, as a filter `|translate`, including additional parameters that are passed to the `translate()` method (see `foo, bar`): - -```latte -
    {='Basket'|translate} -{$item|translate} -{$item|translate, foo, bar} -``` - -Or as an underscore tag: - -```latte -{_'Basket'} -{_$item} -{_$item, foo, bar} -``` - -For translating a section of the template, there is a paired tag `{translate}` (since Latte 2.11, previously the tag `{_}` was used): - -```latte -{translate}Order{/translate} -{translate foo, bar}Order{/translate} -``` - -The translator is normally called at runtime when rendering the template. Latte version 3, however, can translate all static texts already during template compilation. This saves performance, as each string is translated only once, and the resulting translation is written into the compiled form. This creates multiple compiled versions of the template in the cache directory, one for each language. To do this, simply specify the language as the second parameter: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator, $lang); -} -``` - -Static text means, for example, `{_'hello'}` or `{translate}hello{/translate}`. Non-static texts, like `{_$foo}`, will continue to be translated at runtime. - -{{maintitle: Nette Templates: Using Latte in Presenters}} -{{description: How Nette presenters work with Latte templates: template and layout lookup, variables, type-safe templates, links, custom filters and tags, and translations.}} diff --git a/application/en/upgrading.texy b/application/en/upgrading.texy deleted file mode 100644 index ae8b8e8a91..0000000000 --- a/application/en/upgrading.texy +++ /dev/null @@ -1,50 +0,0 @@ -Upgrading -********* - - -Upgrading to Version 3.0 -======================== - -Nette 3.0 adds type hints to method parameters and return values. If you override such a method in a class inheriting from Nette (for example in a presenter or component), you must add the same type hints, otherwise PHP raises a "Declaration must be compatible" error. - -The `Nette\Application\IRouter` interface has changed. The `match()` method now returns, and `constructUrl()` accepts, an array of parameters instead of a `Nette\Application\Request` object. - -Nette now checks that each signal is sent from the same origin (i.e. the same domain and subdomain). This same-origin policy is a critical security mechanism that helps reduce possible attack vectors. If you want to allow other origins, add the `@crossOrigin` annotation to the handler method: - -```php -/** - * @crossOrigin - */ -public function handleXy(): void -{ -} -``` - -This also applies to form submissions. If you want to allow submission from other origins, do it this way: - -```php -$form = new Nette\Application\UI\Form; -$form->allowCrossOrigin(); -``` - -The constructor of `Nette\ComponentModel\Component` has not been used for years and was removed in version 3.0. It is a BC break: if you call the parent constructor in a component or presenter inheriting from `Nette\Application\UI\Presenter`, you must remove the call. - - -Upgrading to Version 2.4 -======================== - -- `Route` and `SimpleRouter` now generate the same HTTP/HTTPS scheme that was used to access the site. A route that requires a specific protocol can be defined with the scheme, e.g. `Route('http://domain.cz/')`. -- For bool-type parameters of render/action methods (i.e. with a default value of true or false) and for persistent parameters, `false` and `null` are now distinguished. If the parameter is not present in the URL, its value is now `null` (previously `false`). -- The class returned by `Presenter::getReflection()` is no longer a descendant of `Nette\Reflection\ClassType`, and `getReflection()->getMethod()` is no longer a descendant of `Nette\Reflection\Method`. -- The `SECURED` flag and `Route::$defaultFlags` are deprecated. - - -Upgrading to Version 2.3 -======================== - -- routes and presenter names are **case-sensitive**. Nette warns you if you use the wrong case in a presenter name; for performance reasons the Route mask is not checked, so verify it manually. -- `Route::addStyle()` and `Route::setStyleProperty()` are deprecated and now trigger `E_USER_DEPRECATED`. -- the template extension `.phtml` and the old link syntax are no longer supported. - -{{maintitle: Upgrading Nette Application: What Changed in Each Version}} -{{description: What changed in Nette Application 3.0, 2.4 and 2.3, and how to update your code: type hints, the router interface, same-origin signals and more.}} diff --git a/application/es/@home.texy b/application/es/@home.texy deleted file mode 100644 index 9d18759735..0000000000 --- a/application/es/@home.texy +++ /dev/null @@ -1,85 +0,0 @@ -Nette Application -***************** - -.[perex] -Nette Application es el núcleo de Nette Framework y ofrece herramientas potentes para crear aplicaciones web modernas. Aporta una serie de funciones excepcionales que simplifican notablemente el desarrollo y mejoran la seguridad y la mantenibilidad del código. - - -Instalación ------------ - -Descargue e instale la biblioteca con [Composer|best-practices:composer]: - -```shell -composer require nette/application -``` - - -¿Por qué elegir Nette Application? ----------------------------------- - -Nette ha sido siempre un pionero en tecnologías web. - -**Router bidireccional:** Nette dispone de un sistema de enrutamiento avanzado y único por su bidireccionalidad: no solo traduce las URL en acciones de la aplicación, sino que también sabe generar URL a la inversa. Esto significa que: -- Puede cambiar en cualquier momento la estructura de URL de toda la aplicación sin tocar las plantillas -- Las URL se canonizan automáticamente, lo que mejora el SEO -- El enrutamiento se define en un solo sitio, no disperso en anotaciones - -**Componentes y señales:** el sistema integrado de componentes, inspirado en Delphi y React.js, es único entre los frameworks de PHP: -- Permite crear elementos de interfaz reutilizables -- Admite la composición jerárquica de componentes -- Ofrece un tratamiento elegante de las peticiones AJAX mediante señales -- Amplia biblioteca de componentes listos para usar en [Componette](https://componette.org) - -**AJAX y snippets:** Nette introdujo en 2009 una forma revolucionaria de trabajar con AJAX, mucho antes que soluciones parecidas como Hotwire para Ruby on Rails o Symfony UX Turbo: -- Los snippets permiten actualizar solo partes de la página sin necesidad de 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 [Latte|latte:] intuitivas:** el sistema de plantillas más seguro para PHP, con funciones avanzadas: -- Protección automática contra XSS con escapado sensible al contexto -- Ampliable con filtros, funciones y etiquetas propios -- Herencia de plantillas y snippets para AJAX -- Excelente soporte de PHP 8.x con sistema de tipos - -**Inyección de dependencias:** Nette aprovecha al máximo la inyección de dependencias: -- Paso automático de dependencias (autowiring) -- Configuración con el claro formato NEON -- Soporte de factories de componentes - - -Principales ventajas --------------------- - -- **Seguridad**: protección automática contra [vulnerabilidades|nette:vulnerability-protection] como XSS, CSRF, etc. -- **Productividad**: menos escritura y más funciones gracias a un diseño inteligente -- **Depuración**: [depurador Tracy|tracy:] con panel de enrutamiento -- **Rendimiento**: caché inteligente, carga diferida de componentes -- **Flexibilidad**: modificación sencilla de las URL incluso con la aplicación terminada -- **Componentes**: sistema único de elementos de interfaz reutilizables -- **Modernidad**: pleno soporte de PHP 8.3+ y del sistema de tipos - - -Primeros pasos --------------- - -1. [¿Cómo funcionan las aplicaciones? |how-it-works] - comprender la arquitectura básica -2. [Presenters |presenters] - trabajar con presenters y acciones -3. [Plantillas |templates] - crear plantillas en Latte -4. [Enrutamiento |routing] - configurar las direcciones URL -5. [Componentes interactivos |components] - usar el sistema de componentes - - -Compatibilidad con PHP ----------------------- - -| versión | compatible con PHP -|-----------------------|------------------- -| Nette Application 3.3 | PHP 8.3 - 8.5 -| Nette Application 3.2 | PHP 8.1 - 8.5 -| 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 - -Vale para las últimas versiones correctivas. diff --git a/application/es/@left-menu.texy b/application/es/@left-menu.texy deleted file mode 100644 index 3813496c5b..0000000000 --- a/application/es/@left-menu.texy +++ /dev/null @@ -1,24 +0,0 @@ -Nette Application -***************** -- [Introducción |@home] -- [¿Cómo funcionan las aplicaciones? |how-it-works] -- [Bootstrapping|bootstrapping] -- [Presenters|presenters] -- [Plantillas|templates] -- [Estructura de directorios |directory-structure] -- [Enrutamiento|routing] -- [Creación de enlaces URL |creating-links] -- [Componentes interactivos |components] -- [AJAX y snippets |ajax] -- [Multiplier |multiplier] -- [Configuración|configuration] -- [Actualización|upgrading] - - -Lecturas adicionales -******************** -- [¿Por qué usar Nette?|www:10-reasons-why-nette] -- [Instalación |nette:installation] -- [¡Cree su primera aplicación! |quickstart:] -- [Buenas prácticas |best-practices:] -- [Solución de problemas |nette:troubleshooting] diff --git a/application/es/@meta.texy b/application/es/@meta.texy deleted file mode 100644 index 3798d9cda4..0000000000 --- a/application/es/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentación de Nette}} diff --git a/application/es/ajax.texy b/application/es/ajax.texy deleted file mode 100644 index 4f526c7836..0000000000 --- a/application/es/ajax.texy +++ /dev/null @@ -1,265 +0,0 @@ -AJAX y snippets -*************** - -
    - -En la era de las aplicaciones web modernas, donde la funcionalidad suele repartirse entre el servidor y el navegador, AJAX es un elemento de unión imprescindible. ¿Qué posibilidades ofrece Nette Framework en este terreno? -- enviar partes de la plantilla, los llamados snippets -- pasar variables entre PHP y JavaScript -- herramientas para depurar las peticiones AJAX - -
    - - -Petición AJAX -============= - -Una petición AJAX no difiere en lo esencial de una petición HTTP clásica. Se llama a un presenter con unos parámetros concretos. Es cosa del presenter decidir cómo responder a la petición: puede devolver datos en formato JSON, enviar una parte del código HTML, un documento XML, etc. - -Del lado del navegador iniciamos una petición AJAX con la función `fetch()`: - -```js -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -.then(response => response.json()) -.then(payload => { - // procesa la respuesta -}); -``` - -Del lado del servidor, una petición AJAX se reconoce con el método `$httpRequest->isAjax()` del servicio que [encapsula la petición HTTP |http:request]. Usa para detectarla la cabecera HTTP `X-Requested-With`, así que es imprescindible enviarla. Dentro del presenter puede usar el método `$this->isAjax()`. - -Si quiere enviar datos en formato JSON, use el método [`sendJson()` |presenters#Envío de una respuesta]. El método termina además la actividad del presenter. - -```php -public function actionExport(): void -{ - $this->sendJson($this->model->getData()); -} -``` - -Si piensa responder con una plantilla especial pensada para AJAX, puede hacerlo así: - -```php -public function handleClick($param): void -{ - if ($this->isAjax()) { - $this->template->setFile('path/to/ajax.latte'); - } - // ... -} -``` - - -Snippets -======== - -La herramienta más potente que ofrece Nette para conectar el servidor con el cliente son los snippets. Con ellos puede convertir una aplicación corriente en una aplicación AJAX con un esfuerzo mínimo y unas pocas líneas de código. El ejemplo Fifteen muestra cómo funciona todo, y su código está en [GitHub |https://github.com/nette-examples/fifteen]. - -Los snippets permiten actualizar solo partes de la página en lugar de recargarla entera. Esto no solo es más rápido y eficiente, sino que ofrece una experiencia de usuario más cómoda. Los snippets pueden recordarle a Hotwire para Ruby on Rails o a Symfony UX Turbo. Curiosamente, Nette introdujo los snippets 14 años antes. - -¿Cómo funcionan los snippets? En la primera carga de la página (una petición no AJAX) se carga la página entera, con todos los snippets incluidos. Cuando el usuario interactúa con la página (pulsa un botón, envía un formulario, etc.), se inicia una petición AJAX en lugar de recargar la página entera. El código del presenter ejecuta la acción y decide qué snippets hay que actualizar. Nette renderiza esos snippets y los envía en una carga útil JSON con un array de snippets. El código de gestión del navegador inserta después los snippets recibidos de vuelta en la página. Así solo se transfiere el código de los snippets modificados, lo que ahorra ancho de banda y acelera la carga frente a transferir el contenido de la página entera. Si no se invalida ningún snippet con `redrawControl()`, Nette devuelve la página entera incluso en una petición AJAX: los snippets se envían solo cuando algo se invalida. - - -Naja ----- - -Para gestionar los snippets del lado del navegador se usa la [biblioteca Naja |https://naja.js.org]. [Instálela |https://naja.js.org/#/guide/01-install-setup-naja] como paquete de Node.js (para usarla con empaquetadores como Webpack, Rollup, Vite, Parcel y otros): - -```shell -npm install naja -``` - -…o insértela directamente en la plantilla de la página: - -```latte - -``` - -Primero hay que [inicializar |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization] la biblioteca: - -```js -naja.initialize(); -``` - -Para convertir un enlace corriente (una señal) o el envío de un formulario en una petición AJAX, basta con marcar el enlace, el formulario o el botón correspondiente con la clase `ajax`: - -```latte -Go - -
    - -
    - -or - -
    - -
    -``` - - -Redibujar los snippets ----------------------- - -Todo objeto de la clase [Control |components] (incluido el propio Presenter) lleva la cuenta de si se han producido cambios que exijan redibujarlo. Para eso sirve el método `redrawControl()`: - -```php -public function handleLogin(string $user): void -{ - // tras el acceso hay que redibujar la parte correspondiente - $this->redrawControl(); - // ... -} -``` - -Nette permite un control aún más fino de lo que hay que redibujar. El método puede recibir como argumento el nombre del snippet. Así es posible invalidar (es decir, forzar el redibujado) a nivel de partes de la plantilla. Si se invalida el componente entero, se redibujarán también todos los snippets que contiene: - -```php -// invalida el snippet 'header' -$this->redrawControl('header'); -``` - -También puede cancelar una invalidación pendiente con el segundo parámetro `$redraw`: llamar a `$this->redrawControl('header', redraw: false)` marca el snippet como no necesitado de redibujado. La firma completa es `redrawControl(?string $snippet = null, bool $redraw = true)`. - - -Snippets en Latte ------------------ - -Usar snippets en Latte es facilísimo. Para definir una parte de la plantilla como snippet, basta con envolverla con las etiquetas `{snippet}` y `{/snippet}`: - -```latte -{snippet header} -

    Hello ...

    -{/snippet} -``` - -El snippet crea en la página HTML un elemento `
    ` con un `id` especial generado. Al redibujar el snippet se actualiza el contenido de ese elemento. Por eso es necesario que, al renderizar la página por primera vez, se rendericen también todos los snippets, aunque al principio puedan estar vacíos. - -También puede crear un snippet con un elemento distinto de `
    ` mediante un n:atributo: - -```latte -
    -

    Hello ...

    -
    -``` - - -Áreas de snippets ------------------ - -Los nombres de los snippets pueden ser también expresiones: - -```latte -{foreach $items as $id => $item} -
  • {$item}
  • -{/foreach} -``` - -Por sí solo, esto es un paso intermedio no funcional: renderizado fuera de un `{snippet}` o `{snippetArea}` estático, un snippet dinámico provoca un `E_USER_WARNING` con el mensaje *Dynamic snippets are allowed only inside static snippet/snippetArea.* Lo arreglamos a continuación. - -Esto crea varios snippets como `item-0`, `item-1`, etc. Si invalidáramos directamente un snippet dinámico (por ejemplo, `item-1`), no se redibujaría nada. El motivo es que los snippets funcionan realmente como extractos y solo ellos mismos se renderizan directamente. En la plantilla, sin embargo, no existe técnicamente ningún snippet llamado `item-1`. Solo nace cuando se ejecuta el código que rodea al snippet, es decir, el bucle foreach. Por eso marcamos con la etiqueta `{snippetArea}` la parte de la plantilla que hay que ejecutar: - -```latte -
      - {foreach $items as $id => $item} -
    • {$item}
    • - {/foreach} -
    -``` - -Y pedimos el redibujado tanto del snippet concreto como de toda el área padre: - -```php -$this->redrawControl('itemsContainer'); -$this->redrawControl('item-1'); -``` - -Al mismo tiempo, conviene asegurarse de que el array `$items` contenga solo los elementos que deben redibujarse. - -Si incluimos en la plantilla principal otra plantilla que contiene snippets mediante la etiqueta `{include}`, hay que envolver de nuevo la inclusión de la plantilla en un `snippetArea` e invalidarlo junto con el snippet: - -```latte -{snippetArea include} - {include 'included.latte'} -{/snippetArea} -``` - -```latte -{* included.latte *} -{snippet item} - ... -{/snippet} -``` - -```php -$this->redrawControl('include'); -$this->redrawControl('item'); -``` - - -Snippets en los componentes ---------------------------- - -Puede crear snippets dentro de los [componentes|components] y Nette los redibujará automáticamente. Hay, sin embargo, una limitación: para redibujar los snippets, Nette llama al método `render()` sin parámetro alguno. Por eso, pasar parámetros en la plantilla no funcionará: - -```latte -OK -{control productGrid} - -will not work: -{control productGrid, $arg, $arg} -{control productGrid:paginator} -``` - - -Enviar datos propios --------------------- - -Junto con los snippets puede enviar al cliente cualquier dato adicional. Basta con escribirlo en el objeto `payload`: - -```php -public function actionDelete(int $id): void -{ - // ... - if ($this->isAjax()) { - $this->payload->message = 'Success'; - } -} -``` - - -Redirección ------------ - -Durante una petición AJAX, los métodos `redirect()` y `redirectUrl()` no envían una redirección HTTP. En su lugar escriben la URL de destino en la carga útil (el objeto de datos enviado en la respuesta AJAX), en concreto en su propiedad `payload.redirect`, y la envían; la redirección propiamente dicha la realiza después la biblioteca del lado del cliente (Naja). - - -Paso de parámetros -================== - -Al enviar parámetros a un componente mediante una petición AJAX, ya sean parámetros de señal o parámetros persistentes, debemos indicar en la petición su nombre global, que incluye el nombre del componente. El método `getParameterId()` devuelve el nombre completo del parámetro. - -```js -let url = new URL({link //foo!}); -url.searchParams.set({$control->getParameterId('bar')}, bar); - -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -``` - -Y el método handle con los parámetros correspondientes en el componente: - -```php -public function handleFoo(int $bar): void -{ -} -``` - - -Lecturas adicionales -==================== - -- [Snippets dinámicos |best-practices:dynamic-snippets] diff --git a/application/es/bootstrapping.texy b/application/es/bootstrapping.texy deleted file mode 100644 index 283c9e693c..0000000000 --- a/application/es/bootstrapping.texy +++ /dev/null @@ -1,312 +0,0 @@ -Bootstrapping -************* - -
    - -El bootstrapping es el proceso de inicializar el entorno de la aplicación, crear el contenedor de inyección de dependencias (DI) y arrancar la aplicación. Hablaremos de: - -- cómo inicializa el entorno la clase Bootstrap -- cómo se configuran las aplicaciones con archivos NEON -- cómo distinguir entre el modo de producción y el de desarrollo -- cómo crear y configurar el contenedor DI - -
    - - -Las aplicaciones, ya sean web o scripts ejecutados desde la línea de comandos, empiezan su ejecución con alguna forma de inicialización del entorno. Antaño se encargaba de ello un archivo llamado quizá `include.inc.php`, incluido por el archivo inicial. En las aplicaciones modernas de Nette lo ha sustituido la clase `Bootstrap` que, como parte de la aplicación, se encuentra en el archivo `app/Bootstrap.php`. Podría tener, por ejemplo, este aspecto: - -```php -namespace App; - -use Nette; -use Nette\Bootstrap\Configurator; - -class Bootstrap -{ - private Configurator $configurator; - private string $rootDir; - - public function __construct() - { - $this->rootDir = dirname(__DIR__); - // El configurador se encarga de preparar el entorno y los servicios de la aplicación. - $this->configurator = new Configurator; - // Establece el directorio de los archivos temporales que genera Nette (p. ej. las 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 - { - // Nette es listo y el modo de desarrollo se activa automáticamente, - // o puede activarlo para una dirección IP concreta descomentando la línea siguiente: - // $this->configurator->setDebugMode('secret@23.75.345.200'); - - // Activa Tracy: la navaja suiza definitiva para depurar. - $this->configurator->enableTracy($this->rootDir . '/log'); - - // RobotLoader: carga automáticamente todas las clases del directorio elegido - $this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); - } - - private function setupContainer(): void - { - // Carga los archivos de configuración - $this->configurator->addConfig($this->rootDir . '/config/common.neon'); - } -} -``` - - -index.php -========= - -En las aplicaciones web, el archivo inicial es `index.php`, situado en el [directorio público |directory-structure#Directorio público www/] `www/`. Le pide a la clase Bootstrap que inicialice el entorno y cree el contenedor DI. Después obtiene del contenedor el servicio `Application`, que ejecuta la aplicación web: - -```php -$bootstrap = new App\Bootstrap; -// Inicializa el entorno + crea un contenedor DI -$container = $bootstrap->bootWebApplication(); -// El contenedor DI crea un objeto Nette\Application\Application -$application = $container->getByType(Nette\Application\Application::class); -// Arranca la aplicación Nette y procesa la petición entrante -$application->run(); -``` - -.[note] -El objeto `$application` emite [eventos |nette:glossary#Eventos] mientras atiende la petición: `onStartup`, `onRequest`, `onPresenter`, `onResponse`, `onShutdown` y `onError` (ante una excepción no capturada). Puede engancharles manejadores, algo práctico para registrar o para supervisar toda la aplicación. - -Como ve, la clase [api:Nette\Bootstrap\Configurator] ayuda a configurar el entorno y a crear el contenedor de inyección de dependencias (DI). La presentaremos ahora en detalle. - - -Modo de desarrollo y modo de producción -======================================= - -Nette se comporta de forma distinta según se ejecute en un servidor de desarrollo o de producción: - -🛠️ Modo de desarrollo: - - Muestra la barra de depuración de Tracy con información útil (consultas SQL, tiempo de ejecución, memoria usada) - - Ante un error, muestra una página de error detallada con las llamadas a funciones y el contenido de las variables - - Refresca automáticamente la caché al cambiar las plantillas de Latte, los archivos de configuración, etc. - - -🚀 Modo de producción: - - No muestra ninguna información de depuración; todos los errores se escriben en el registro - - Ante un error, muestra un ErrorPresenter o una página genérica de "Server Error" - - ¡La caché no se refresca nunca automáticamente! - - Está optimizado para la velocidad y la seguridad - - -El modo se elige por autodetección, así que normalmente no hace falta configurar nada ni cambiar de modo a mano: - -- modo de desarrollo: en localhost (dirección IP `127.0.0.1` o `::1`) si no hay ningún proxy presente (es decir, no se detecta su cabecera HTTP) -- modo de producción: en todos los demás casos - -Si queremos activar el modo de desarrollo en otros casos, por ejemplo para los programadores que se conectan desde una dirección IP concreta, usamos `setDebugMode()`: - -```php -$this->configurator->setDebugMode('23.75.345.200'); // también se puede indicar un array de direcciones IP -``` - -Recomendamos vivamente combinar la dirección IP con un cookie. Guarde un token secreto, por ejemplo `secret1234`, en el cookie `nette-debug` y active así el modo de desarrollo para los programadores que se conecten desde una dirección IP concreta y tengan además ese token en su cookie: - -```php -$this->configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -También podemos desactivar por completo el modo de desarrollo, incluso en localhost: - -```php -$this->configurator->setDebugMode(false); -``` - -Tenga en cuenta que el valor `true` fuerza el modo de desarrollo, lo que **nunca** debería ocurrir en un servidor de producción. - -De la autodetección se encarga internamente el método estático `Configurator::detectDebugMode()`, al que también puede llamar usted mismo, por ejemplo para detectar el modo de desarrollo fuera del configurador. Acepta una lista blanca opcional de direcciones IP o nombres de equipo y devuelve si la petición actual debe ejecutarse en modo de desarrollo: - -```php -$debug = Nette\Bootstrap\Configurator::detectDebugMode('23.75.345.200'); -``` - - -Herramienta de depuración Tracy -=============================== - -Para depurar con comodidad activaremos la excelente herramienta [Tracy |tracy:]. En modo de desarrollo visualiza los errores y, en modo de producción, los registra en el directorio indicado: - -```php -$this->configurator->enableTracy($this->rootDir . '/log'); -``` - - -Archivos temporales -=================== - -Nette usa caché para el contenedor DI, RobotLoader, las plantillas, etc. Por eso hay que indicar la ruta del directorio donde se guardará esa caché: - -```php -$this->configurator->setTempDirectory($this->rootDir . '/temp'); -``` - -En Linux o macOS, dé [permisos de escritura |nette:troubleshooting#Establecer los permisos de los directorios] a los directorios `log/` y `temp/`. - - -RobotLoader -=========== - -Normalmente querremos cargar las clases automáticamente con [RobotLoader |robot-loader:], así que hay que ponerlo en marcha y dejar que cargue las clases del directorio donde está `Bootstrap.php` (es decir, `__DIR__`) y de todos sus subdirectorios: - -```php -$this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); -``` - -Un enfoque alternativo es cargar las clases únicamente mediante [Composer |best-practices:composer], siguiendo PSR-4. - - -Zona horaria -============ - -Puede fijar la zona horaria predeterminada mediante el configurador. - -```php -$this->configurator->setTimeZone('Europe/Prague'); -``` - - -Configuración del contenedor DI -=============================== - -Parte del proceso de arranque es la creación del contenedor DI, o factory de objetos, que es el corazón de toda la aplicación. En realidad es una clase PHP generada por Nette y guardada en el directorio de caché. La factory produce los objetos clave de la aplicación y, mediante los archivos de configuración, le indicamos cómo crearlos y ajustarlos, con lo que influimos en el comportamiento de toda la aplicación. - -Los archivos de configuración se escriben normalmente en [formato NEON |neon:format]. En un capítulo aparte puede leer [qué se puede configurar |nette:configuring]. - -.[tip] -En modo de desarrollo, el contenedor se actualiza automáticamente cada vez que cambian el código o los archivos de configuración. En modo de producción se genera una sola vez y los cambios no se comprueban, para maximizar el rendimiento. - -Mientras que `createContainer()` construye el contenedor y devuelve su instancia, el método `loadContainer()` devuelve solo el nombre de la clase de contenedor generada, que puede instanciar usted mismo. Esto resulta útil en escenarios avanzados. - -Los archivos de configuración se cargan con `addConfig()`: - -```php -$this->configurator->addConfig($this->rootDir . '/config/common.neon'); -``` - -Si queremos añadir más archivos de configuración, podemos llamar varias veces a la función `addConfig()`. - -```php -$configDir = $this->rootDir . '/config'; -$this->configurator->addConfig($configDir . '/common.neon'); -$this->configurator->addConfig($configDir . '/services.neon'); -if (PHP_SAPI === 'cli') { - $this->configurator->addConfig($configDir . '/cli.php'); -} -``` - -El nombre `cli.php` no es una errata; la configuración también se puede escribir en un archivo PHP que la devuelva como array. - -También podemos añadir otros archivos de configuración en [la sección `includes` |dependency-injection:configuration#Inclusión de archivos]. - -Si en los archivos de configuración aparecen elementos con las mismas claves, se sobrescriben o, en el caso de los [arrays, se fusionan |dependency-injection:configuration#Fusión]. Un archivo incluido después tiene más prioridad que el anterior. El archivo en el que figura la sección `includes` tiene más prioridad que los archivos incluidos dentro de ella. - - -Parámetros estáticos --------------------- - -Los parámetros usados en los archivos de configuración se pueden definir [en la sección `parameters` |dependency-injection:configuration#Parámetros] y también pasar (o sobrescribir) con el método `addStaticParameters()` (cuyo alias antiguo, ahora obsoleto, es `addParameters()`). Es importante saber que valores distintos de los parámetros provocan la generación de contenedores DI adicionales, es decir, de clases adicionales. - -```php -$this->configurator->addStaticParameters([ - 'projectId' => 23, -]); -``` - -El parámetro `projectId` se puede referenciar en la configuración con la notación habitual `%projectId%`. - - -Parámetros dinámicos --------------------- - -Al contenedor también podemos añadir parámetros dinámicos, cuyos distintos valores, a diferencia de los estáticos, no provocan la generación de nuevos contenedores DI. - -```php -$this->configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Así podemos añadir con facilidad, por ejemplo, las variables de entorno, que se pueden referenciar después en la configuración con la notación `%env.variable%`. - -```php -$this->configurator->addDynamicParameters([ - 'env' => getenv(), -]); -``` - - -Parámetros predeterminados --------------------------- - -Puede usar estos parámetros en los archivos de configuración: - -- `%appDir%` es la ruta absoluta al directorio que contiene el archivo `Bootstrap.php` -- `%wwwDir%` es la ruta absoluta al directorio que contiene el archivo de entrada `index.php` -- `%tempDir%` es la ruta absoluta al directorio de 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 -- `%baseUrl%` es la URL absoluta al directorio raíz (un parámetro dinámico resuelto en tiempo de ejecución) -- `%debugMode%` indica si la aplicación está en modo de depuración -- `%consoleMode%` indica si la petición llegó por la línea de comandos - - -Servicios importados --------------------- - -Vamos ahora un poco más al fondo. Aunque el cometido del contenedor DI es crear objetos, alguna vez puede surgir la necesidad de insertar en el contenedor un objeto ya existente. Lo hacemos definiendo el servicio con la bandera `imported: true`. - -```neon -services: - myservice: - type: App\Model\MyCustomService - imported: true -``` - -Y en el bootstrap insertamos el objeto en el contenedor: - -```php -$this->configurator->addServices([ - 'myservice' => new App\Model\MyCustomService('foobar'), -]); -``` - - -Entornos distintos -================== - -No dude en modificar la clase `Bootstrap` según sus necesidades. Puede añadir parámetros al método `bootWebApplication()` para distinguir entre proyectos web. O podemos añadir otros métodos, como `bootTestEnvironment()`, que inicializa el entorno para las pruebas unitarias, `bootConsoleApplication()` para los scripts llamados desde la línea de comandos, etc. - -```php -public function bootTestEnvironment(): Nette\DI\Container -{ - Tester\Environment::setup(); // inicialización de Nette Tester - $this->setupContainer(); - return $this->configurator->createContainer(); -} - -public function bootConsoleApplication(): Nette\DI\Container -{ - $this->configurator->setDebugMode(false); - $this->initializeEnvironment(); - $this->setupContainer(); - return $this->configurator->createContainer(); -} -``` diff --git a/application/es/components.texy b/application/es/components.texy deleted file mode 100644 index 426d72ee2c..0000000000 --- a/application/es/components.texy +++ /dev/null @@ -1,504 +0,0 @@ -Componentes interactivos -************************ - -
    - -Los componentes son objetos independientes y reutilizables que insertamos en las páginas. Pueden ser formularios, datagrids, encuestas, en definitiva cualquier cosa que tenga sentido usar repetidamente. Mostraremos: - -- ¿cómo se usan los componentes? -- ¿cómo se escriben? -- ¿qué son las señales? - -
    - -Nette lleva incorporado un sistema de componentes. Algo parecido puede resultar familiar a los veteranos de Delphi o ASP.NET Web Forms; React o Vue.js se apoyan en algo lejanamente similar. En el mundo de los frameworks de PHP, sin embargo, es una característica única. - -Al mismo tiempo, los componentes influyen de manera esencial en la forma de desarrollar la aplicación. Puede componer las páginas a partir de unidades ya preparadas. ¿Necesita un datagrid en la administración? Búsquelo en [Componette |https://componette.org/search/component], un repositorio de complementos de código abierto (no solo componentes) para Nette, e insértelo sin más en el presenter. - -Puede incorporar al presenter cualquier número de componentes. Y dentro de algunos componentes puede insertar otros componentes. Así surge un árbol de componentes cuya raíz es el presenter. - - -Métodos factory -=============== - -¿Cómo se insertan los componentes en el presenter y cómo se usan después? Normalmente mediante métodos factory. - -Una factory de componentes es una forma elegante de crear los componentes solo cuando realmente hacen falta (lazy / on demand). Toda la magia consiste en implementar un método llamado `createComponent()`, donde `` es el nombre del componente que se crea, y que crea y devuelve dicho componente. - -```php .{file:DefaultPresenter.php} -class DefaultPresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentPoll(): PollControl - { - $poll = new PollControl; - $poll->items = $this->items; - return $poll; - } -} -``` - -Como todos los componentes se crean en métodos separados, el código gana en claridad. - -.[note] -Los nombres de los componentes empiezan siempre por minúscula, aunque en el nombre del método se escriban con mayúscula. - -Nunca llamamos a las factories directamente; se invocan solas la primera vez que usamos el componente. Gracias a eso, el componente se crea en el momento adecuado y solo si realmente hace falta. Si no usamos el componente (por ejemplo, en una petición AJAX en la que solo se transfiere una parte de la página, o al guardar la plantilla en caché), no se creará en absoluto, lo que ahorra rendimiento del servidor. - -```php .{file:DefaultPresenter.php} -// accedemos al componente y, si es la primera vez, -// se llama a createComponentPoll(), que lo crea -$poll = $this->getComponent('poll'); -// sintaxis alternativa: $poll = $this['poll']; -``` - -En la plantilla se puede renderizar un componente con la etiqueta [{control} |#Renderizado]. Por eso no hace falta pasar los componentes a la plantilla manualmente. - -```latte -

    Please Vote

    - -{control poll} -``` - -.[tip] -Para crear dinámicamente un número variable de componentes, use [Multiplier |multiplier]. - -Los métodos factory `createComponent()` no funcionan solo en los presenters. Del mismo modo puede anidar un componente dentro de otro componente y componerlos en un árbol, lo que resulta práctico, por ejemplo, para un formulario renderizado por separado dentro de un componente. - - -Estilo Hollywood -================ - -Los componentes usan habitualmente una técnica fresca que nos gusta llamar el estilo Hollywood. Seguro que conoce el tópico que oyen a menudo los participantes en los cástines de cine: "No nos llame, ya le llamaremos nosotros." Y de eso se trata exactamente. - -En Nette, en lugar de tener que preguntar constantemente ("¿se ha enviado el formulario?", "¿era válido?" o "¿ha pulsado el usuario este botón?"), le dice al framework "cuando ocurra esto, llama a este método" y le deja a él el trabajo restante. Si programa en JavaScript, conoce a fondo este estilo de programación. Escribe funciones que se invocan cuando ocurre un determinado evento. Y el lenguaje les pasa los parámetros adecuados. - -Esto cambia por completo la perspectiva desde la que se escriben las aplicaciones. Cuantas más tareas pueda dejar al framework, menos trabajo tendrá. Y menos cosas se le podrán pasar por alto. - - -Escribir un componente -====================== - -Con el término componente nos referimos normalmente a un descendiente de la clase [api:Nette\Application\UI\Control]. (Sería más exacto usar el término "controls", pero en algunos idiomas tiene otro significado y "componentes" se ha impuesto más.) El propio presenter [api:Nette\Application\UI\Presenter] es también descendiente de la clase `Control`. - -```php .{file:PollControl.php} -use Nette\Application\UI\Control; - -class PollControl extends Control -{ -} -``` - - -Renderizado -=========== - -Ya sabemos que para renderizar un componente se usa la etiqueta `{control nombreComponente}`. En realidad llama al método `render()` del componente, en el que nos ocupamos del renderizado. Tenemos a disposición, igual que en el presenter, una [plantilla Latte|templates] en la variable `$this->template`, a la que pasamos los parámetros. A diferencia del presenter, debemos indicar el archivo de plantilla y mandar renderizarlo: - -```php .{file:PollControl.php} -public function render(): void -{ - // pasamos algunos parámetros a la plantilla - $this->template->param = $value; - // y la renderizamos - $this->template->render(__DIR__ . '/poll.latte'); -} -``` - -La etiqueta `{control}` permite pasar parámetros al método `render()`: - -```latte -{control poll, $id, $message} -``` - -```php .{file:PollControl.php} -public function render(int $id, string $message): void -{ - // ... -} -``` - -A veces un componente puede constar de varias partes que queremos renderizar por separado. Para cada una de ellas creamos su propio método de renderizado, aquí en el ejemplo `renderPaginator()`: - -```php .{file:PollControl.php} -public function renderPaginator(): void -{ - // ... -} -``` - -Y en la plantilla lo invocamos después con: - -```latte -{control poll:paginator} -``` - -Para entenderlo mejor, conviene saber cómo se traduce esta etiqueta a código PHP. - -```latte -{control poll} -{control poll:paginator, 123, 'hello'} -``` - -se traduce a: - -```php -$control->getComponent('poll')->render(); -$control->getComponent('poll')->renderPaginator(123, 'hello'); -``` - -El método `getComponent()` devuelve el componente `poll` y sobre él se llama al método `render()`, o bien `renderPaginator()` si en la etiqueta se indica tras los dos puntos otro método de renderizado. - -.[caution] -Atención: si en los parámetros aparece **`=>`** fuera de corchetes, todos los parámetros se envolverán en un array y se pasarán como primer argumento: - -```latte -{control poll, id: 123, message: 'hello'} -``` - -se traduce a: - -```php -$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']); -``` - -Renderizado de un subcomponente: - -```latte -{control cartControl-someForm} -``` - -se traduce a: - -```php -$control->getComponent("cartControl-someForm")->render(); -``` - -Los componentes, igual que los presenters, pasan automáticamente a las plantillas varias variables útiles: - -- `$basePath` es la ruta URL absoluta al directorio raíz (p. ej. `/eshop`) -- `$baseUrl` es la URL absoluta al directorio raíz (p. ej. `http://localhost/eshop`) -- `$user` es un objeto que [representa al usuario |security:authentication] -- `$presenter` es el presenter actual -- `$control` es el componente actual -- `$flashes` es un array de [mensajes |#Mensajes flash] enviados con la función `flashMessage()` - - -Señal -===== - -Ya sabemos que la navegación en una aplicación Nette consiste en enlazar o redirigir a pares `Presenter:action`. Pero ¿y si solo queremos realizar una acción en la **página actual**? Por ejemplo, cambiar la ordenación de las columnas de una tabla; borrar un elemento; cambiar entre modo claro y oscuro; enviar un formulario; votar en una encuesta; etc. - -Este tipo de petición se llama señal. Y del mismo modo que las acciones invocan los métodos `action()` o `render()`, las señales llaman a los métodos `handle()`. Mientras que el concepto de acción (o vista) se refiere puramente a los presenters, las señales conciernen a todos los componentes. Y por tanto también a los presenters, porque `UI\Presenter` es descendiente de `UI\Control`. - -```php -public function handleClick(int $x, int $y): void -{ - // ... procesamiento de la señal ... -} -``` - -Un enlace que llama a una señal se crea de la forma habitual, es decir, en la plantilla con el atributo `n:href` o la etiqueta `{link}`, y en el código con el método `link()`. Más en el capítulo [Creación de enlaces URL |creating-links#Enlaces a una señal]. - -```latte -click here -``` - -Una señal se llama siempre sobre el presenter y la acción actuales; no es posible invocarla sobre otro presenter u otra acción. - -La señal provoca, por tanto, la recarga de la página exactamente igual que la petición original, pero además llama al método de gestión de la señal con los parámetros adecuados. Si el método no existe, se lanza la excepción [api:Nette\Application\UI\BadSignalException], que se muestra al usuario como una página de error 403 Forbidden. - - -Snippets y AJAX -=============== - -Las señales pueden recordarle un poco a AJAX: gestores que se invocan en la página actual. Y tiene razón, las señales se llaman de hecho a menudo mediante AJAX y a continuación solo se transfieren al navegador las partes modificadas de la página. Se llaman snippets. Encontrará más información en la [página dedicada a AJAX |ajax]. - - -Mensajes flash -============== - -Un componente tiene su propio almacén de mensajes flash, independiente del presenter. Son mensajes que informan, por ejemplo, del resultado de una operación. Una propiedad importante de los mensajes flash es que están disponibles en la plantilla incluso después de una redirección. Incluso tras mostrarse siguen activos otros 30 segundos: por ejemplo, por si el usuario recarga la página debido a un error de transmisión, el mensaje no desaparecerá de inmediato. - -Del envío se ocupa el método [flashMessage |api:Nette\Application\UI\Control::flashMessage()]. El primer parámetro es el texto del mensaje (`string`, `Stringable`) o un objeto `stdClass` que representa el mensaje. El segundo parámetro, opcional, es su tipo (error, warning, info, etc.). El método `flashMessage()` devuelve una instancia del mensaje flash como objeto `stdClass`, al que se le puede añadir más información. - -```php -$this->flashMessage('Item was deleted.'); -$this->redirect(/* ... */); // y redirige -``` - -Estos mensajes están disponibles en la plantilla en la variable `$flashes` como objetos `stdClass`, que contienen las propiedades `message` (texto del mensaje) y `type` (tipo del mensaje), y pueden contener la información de usuario ya mencionada. Los renderizamos, por ejemplo, así: - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -Redirección tras procesar una señal -=================================== - -Tras procesar la señal de un componente suele venir una redirección. Es algo parecido a los formularios: después de enviarlos también redirigimos, para evitar que se vuelvan a enviar los datos al recargar la página en el navegador. - -```php -$this->redirect('this'); // redirige al presenter y la acción actuales -``` - -Como un componente es un elemento reutilizable y normalmente no debería tener un vínculo directo con presenters concretos, los métodos `redirect()` y `link()` interpretan automáticamente el parámetro como una señal del componente: - -```php -$this->redirect('click'); // redirige a la señal 'click' del mismo componente -``` - -Si necesita redirigir a otro presenter o acción, puede hacerlo a través del presenter: - -```php -$this->getPresenter()->redirect('Product:show'); // redirige a otro presenter/acción -``` - - -Parámetros persistentes -======================= - -Los parámetros persistentes sirven para mantener el estado de los componentes entre distintas peticiones. Su valor sigue siendo el mismo incluso después de pulsar un enlace. A diferencia de los datos de la sesión, se transfieren en la URL. Y esto ocurre de forma completamente automática, incluidos los enlaces creados en otros componentes de la misma página. - -Por ejemplo, tiene un componente para paginar contenido. Puede haber varios componentes así en una página. Y queremos que todos los componentes sigan en su página actual después de pulsar un enlace. Por eso convertimos el número de página (`page`) en un parámetro persistente. - -Crear un parámetro persistente en Nette es facilísimo. Basta con crear una propiedad pública y marcarla con el atributo: (antes se usaba `/** @persistent */`) - -```php -use Nette\Application\Attributes\Persistent; // esta línea es importante - -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; // debe ser public -} -``` - -Recomendamos indicar el tipo de dato de la propiedad (p. ej. `int`), y también puede indicar un valor por defecto. Los valores de los parámetros se pueden [validar |#Validación de los parámetros persistentes]. - -Al crear un enlace se puede cambiar el valor de un parámetro persistente: - -```latte -next -``` - -O se puede *resetear*, es decir, eliminar de la URL. Entonces adoptará su valor por defecto: - -```latte -reset -``` - - -Componentes persistentes -======================== - -No solo los parámetros, también los componentes pueden ser persistentes. Sus parámetros persistentes se transfieren entonces incluso entre distintas acciones del presenter o entre varios presenters. Los componentes persistentes los marcamos con un atributo en la clase del presenter. Por ejemplo, marcamos así los componentes `calendar` y `poll`: - -```php -use Nette\Application\Attributes\Persistent; - -#[Persistent('calendar', 'poll')] -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -No hace falta marcar los subcomponentes dentro de estos componentes; también pasan a ser persistentes. - -La anotación antigua `@persistent` sigue funcionando, pero está obsoleta y provoca una advertencia: - -```php -/** - * @persistent(calendar, poll) - */ -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Componentes con dependencias -============================ - -¿Cómo crear componentes con dependencias sin "ensuciar" los presenters que los van a usar? Gracias a las funciones inteligentes del contenedor DI de Nette, igual que ocurre con los servicios clásicos, se puede dejar la mayor parte del trabajo al framework. - -Tomemos como ejemplo un componente que depende del servicio `PollFacade`: - -```php -class PollControl extends Control -{ - public function __construct( - private int $id, // ID de la encuesta para la que creamos el componente - private PollFacade $facade, - ) { - } - - public function handleVote(int $voteId): void - { - $this->facade->vote($this->id, $voteId); - // ... - } -} -``` - -Si estuviéramos escribiendo un servicio clásico, no habría nada que discutir. El contenedor DI se encargaría de forma invisible de pasar todas las dependencias. Pero con los componentes solemos actuar creando una nueva instancia directamente en el presenter, dentro de los [métodos factory |#Métodos factory] `createComponent…()`. Pero pasar al presenter todas las dependencias de todos los componentes solo para pasárselas después a los componentes es engorroso. Y la cantidad de código que hay que escribir... - -La pregunta lógica es por qué no registramos simplemente el componente como un servicio clásico, se lo pasamos al presenter y luego lo devolvemos en el método `createComponent…()`. Este enfoque, sin embargo, no es adecuado, porque queremos poder crear el componente varias veces si hace falta. - -La solución correcta es escribir una factory para el componente, es decir, una clase que nos cree el componente: - -```php -class PollControlFactory -{ - public function __construct( - private PollFacade $facade, - ) { - } - - public function create(int $id): PollControl - { - return new PollControl($id, $this->facade); - } -} -``` - -Registramos esta factory en nuestro contenedor en la configuración: - -```neon -services: - - PollControlFactory -``` - -y finalmente la usamos en nuestro presenter: - -```php -class PollPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private PollControlFactory $pollControlFactory, - ) { - } - - protected function createComponentPollControl(): PollControl - { - $pollId = 1; // podemos pasar nuestro parámetro - return $this->pollControlFactory->create($pollId); - } -} -``` - -Lo genial es que Nette DI sabe [generar |dependency-injection:factory] estas factories tan sencillas, de modo que en lugar de escribir todo su código basta con escribir su interfaz: - -```php -interface PollControlFactory -{ - public function create(int $id): PollControl; -} -``` - -Y eso es todo. Nette implementa internamente esta interfaz y la inyecta en el presenter, donde podemos usarla. Añade mágicamente a nuestro componente el parámetro `$id` y una instancia de la clase `PollFacade`. - - -Los componentes en profundidad -============================== - -Los componentes en Nette Application representan partes reutilizables de una aplicación web que insertamos en las páginas, y a las que está dedicado todo este capítulo. ¿Qué es exactamente capaz de hacer un componente así? - -1) se puede renderizar en una plantilla -2) sabe [qué parte de sí mismo |ajax#Snippets] debe renderizar durante una petición AJAX (snippets) -3) tiene la capacidad de guardar su estado en la URL (parámetros persistentes) -4) tiene la capacidad de reaccionar a las acciones del usuario (señales) -5) crea una estructura jerárquica (cuya raíz es el presenter) - -De cada una de estas funciones se encarga una de las clases de la línea de herencia. Del renderizado (1 + 2) se ocupa [api:Nette\Application\UI\Control], de la integración en el [ciclo de vida |presenters#Ciclo de vida del presenter] (3, 4) la clase [api:Nette\Application\UI\Component], y de la creación de la estructura jerárquica (5) las clases [Container y Component |component-model:]. - -``` -Nette\ComponentModel\Component { IComponent } -| -+- Nette\ComponentModel\Container { IContainer } - | - +- Nette\Application\UI\Component { SignalReceiver, StatePersistent } - | - +- Nette\Application\UI\Control { Renderable } - | - +- Nette\Application\UI\Presenter { IPresenter } -``` - - -Ciclo de vida del componente ----------------------------- - -[* lifecycle-component.svg *] *** *Ciclo de vida del componente* .<> - - -Validación de los parámetros persistentes ------------------------------------------ - -Los valores de los [parámetros persistentes |#Parámetros persistentes] recibidos de las URL los escribe en las propiedades el método `loadState()`. Este comprueba también si coinciden con el tipo de dato indicado en la propiedad; en caso contrario responde con un error 404 y la página no se muestra. - -Nunca confíe ciegamente en los parámetros persistentes, porque el usuario puede sobrescribirlos fácilmente en la URL. Así comprobamos, por ejemplo, si el número de página `$this->page` es mayor que 0. Una forma adecuada es sobrescribir el mencionado método `loadState()`: - -```php -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; - - public function loadState(array $params): void - { - parent::loadState($params); // aquí se establece $this->page - // sigue la comprobación propia del valor: - if ($this->page < 1) { - $this->error(); - } - } -} -``` - -El proceso inverso, es decir, la recogida de los valores de las propiedades persistentes, lo realiza el método `saveState()`. - - -Conexión con el presenter -------------------------- - -En el momento en que un componente pasa a formar parte de la jerarquía del presenter, se invocan sus callbacks guardados en el array `$onAnchor`. A partir de ese momento el componente tiene el presenter a su disposición, puede crear enlaces con seguridad, leer parámetros persistentes, etc. - -```php -$control->onAnchor[] = function ($control): void { - // el componente ya tiene el presenter a su disposición -}; -``` - - -Las señales en profundidad --------------------------- - -La señal provoca la recarga de la página exactamente igual que la petición original (salvo cuando se llama por AJAX) e invoca el método `signalReceived($signal)`, cuya implementación por defecto en la clase `Nette\Application\UI\Component` intenta llamar a un método compuesto por las palabras `handle`. El procesamiento posterior depende del objeto en cuestión. Los objetos que heredan de `Component` (es decir, `Control` y `Presenter`) reaccionan intentando llamar al método `handle` con los parámetros adecuados. - -Dicho de otro modo: se toma la definición de la función `handle` junto con todos los parámetros que llegaron con la petición, a los argumentos se les asignan por nombre los parámetros de la URL y se intenta llamar al método. Por ejemplo, el valor del parámetro `id` de la URL se pasa como argumento `$id`, `something` de la URL se pasa como `$something`, etc. Y si el método no existe, el método `signalReceived` lanza una [excepción |api:Nette\Application\UI\BadSignalException]. - -Además de los parámetros de la URL, la señal lee también los parámetros enviados en el **cuerpo POST de la petición**. Esto viene bien porque las señales se invocan a menudo mediante JavaScript, donde es natural enviar los datos por el método POST. Sin embargo, si llega un parámetro con el mismo nombre tanto de la URL como del cuerpo POST, tiene preferencia el valor **de la URL**. Por eso, evite dar a un campo POST el mismo nombre que a un parámetro de la URL o de la ruta; de lo contrario, el valor de la URL lo sobrescribiría silenciosamente. Los parámetros de las señales comparten un espacio común con los parámetros de acción y los persistentes, véase [Espacio común de parámetros |presenters#Espacio común de parámetros]. - -Una señal puede recibirla cualquier componente, presenter u objeto que implemente la interfaz `SignalReceiver` y esté conectado al árbol de componentes. - -Los principales destinatarios de las señales serán los `Presenters` y los componentes visuales que heredan de `Control`. La señal está pensada como una indicación al objeto de que debe hacer algo: la encuesta debe contar el voto del usuario, el bloque de noticias debe expandirse y mostrar el doble de noticias, el formulario se ha enviado y debe procesar los datos, etc. - -La URL de una señal se crea con el método [Component::link() |api:Nette\Application\UI\Component::link()]. Como parámetro `$destination` pasamos la cadena `{signal}!` y como `$args` un array de argumentos que queremos pasar a la señal. La señal se llama siempre sobre el presenter y la acción actuales con los parámetros actuales; los parámetros de la señal solo se añaden. Además se añade el **parámetro `?do`, que indica la señal**. - -Su formato es `{signal}` o `{signalReceiver}-{signal}`. `{signalReceiver}` es el nombre del componente en el presenter. Por eso no puede usarse un guion en el nombre del componente: sirve para separar el nombre del componente y la señal, aunque de esta forma sea posible anidar varios componentes. - -El método [isSignalReceiver()|api:Nette\Application\UI\Presenter::isSignalReceiver()] comprueba si el componente (primer argumento) es el destinatario de la señal (segundo argumento). El segundo argumento se puede omitir; entonces comprueba si el componente es el destinatario de alguna señal. Si el segundo parámetro se pone a `true`, verifica si el componente indicado o alguno de sus descendientes es el destinatario. - -En cualquier fase previa a `handle` podemos ejecutar la señal manualmente llamando al método [processSignal()|api:Nette\Application\UI\Presenter::processSignal()], que se ocupa de gestionar la señal: toma el componente identificado como destinatario de la señal (si no se indica ningún destinatario, es el propio presenter) y le envía la señal. - -Ejemplo: - -```php -if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) { - $this->processSignal(); -} -``` - -Así se ejecuta la señal prematuramente y ya no se volverá a llamar. diff --git a/application/es/configuration.texy b/application/es/configuration.texy deleted file mode 100644 index 9ff649801c..0000000000 --- a/application/es/configuration.texy +++ /dev/null @@ -1,186 +0,0 @@ -Configuración de la aplicación -****************************** - -.[perex] -Resumen de las opciones de configuración de Nette Application. - - -Application -=========== - -```neon -application: - # ¿mostrar el panel "Nette Application" en la BlueScreen de Tracy? - debugger: ... # (bool) activado si Tracy está disponible - - # en producción, de las excepciones se ocupa siempre el error-presenter; - # esta opción solo activa ese comportamiento también en modo de desarrollo - catchExceptions: ... # (bool) el valor predeterminado es false, es decir, off en dev y siempre on en producción - - # nombre del error-presenter - errorPresenter: Error # (string|array) el valor predeterminado es 'Nette:Error' - - # define alias para los presenters y las acciones - aliases: ... - - # define las reglas para traducir el nombre del presenter a una clase - mapping: ... - - # ¿suprimir las advertencias de los enlaces no válidos? - # solo tiene efecto en modo de desarrollo - silentLinks: ... # (bool) el valor predeterminado es false -``` - -Desde la versión 3.2 de `nette/application` es posible definir un par de error-presenters: - -```neon -application: - errorPresenter: - 4xx: Error4xx # para Nette\Application\BadRequestException - 5xx: Error5xx # para las demás excepciones -``` - -Separarlos resulta útil porque las dos situaciones son radicalmente distintas. Una `BadRequestException` (códigos 4xx) significa que la aplicación está bien y que el visitante simplemente ha pedido algo que no existe. Puede usar entonces un presenter con todas sus funciones, que muestre un mensaje amable dentro del layout de su web. Un error 5xx, en cambio, significa que algo se ha roto en la aplicación y no sabe qué. Mantenga el presenter 5xx lo más mínimo posible, para que nada más pueda fallar mientras se renderiza; lo ideal es que no toque la base de datos, ni el layout, ni el usuario conectado. - -La opción `silentLinks` determina cómo se comporta Nette en modo de desarrollo cuando falla la generación de un enlace (por ejemplo, porque el presenter no existe, etc.). El valor predeterminado `false` significa que Nette lanza un error `E_USER_WARNING`. Ponerla a `true` silencia ese mensaje de error. En un entorno de producción se lanza siempre `E_USER_WARNING`. Este comportamiento también se puede influir con la variable del presenter [$invalidLinkMode |creating-links#Enlaces no válidos]. - -Los [alias simplifican la referencia |creating-links#Alias] a los presenters de uso frecuente. - -El [mapping define las reglas |directory-structure#Mapeo de presenters] por las que se deriva el nombre de la clase a partir del nombre del presenter. - - -Registro automático de presenters ---------------------------------- - -Nette añade automáticamente los presenters como servicios al contenedor DI, lo que acelera notablemente su creación. La forma en que Nette localiza los presenters se puede configurar: - -```neon -application: - # ¿buscar los presenters en el class map de Composer? - scanComposer: ... # (bool) el valor predeterminado es true - - # máscara con la que deben encajar el nombre de la clase y el del archivo - scanFilter: ... # (string) el valor predeterminado es '*Presenter' - - # ¿en qué directorios buscar los presenters? - scanDirs: # (string[]|false) el valor predeterminado es '%appDir%' - - %vendorDir%/mymodule -``` - -Los directorios indicados en `scanDirs` no sustituyen al valor predeterminado `%appDir%`, sino que lo complementan, así que `scanDirs` contendrá ambas rutas: `%appDir%` y `%vendorDir%/mymodule`. Si queremos omitir el directorio predeterminado, usamos un [signo de exclamación |dependency-injection:configuration#Fusión]: - -```neon -application: - scanDirs!: - - %vendorDir%/mymodule -``` - -El escaneo de directorios se puede desactivar poniendo el valor a `false`. Los presenters dejan entonces de registrarse como servicios, por lo que no se pueden ajustar mediante la sección [decorator |dependency-injection:configuration#Decorator] y su creación es más lenta. Por eso no recomendamos suprimir del todo el registro automático: reduciría el rendimiento de la aplicación. - - -Plantillas Latte -================ - -Este ajuste afecta globalmente al comportamiento de Latte en los componentes y los presenters. - -```neon -latte: - # ¿mostrar el panel de Latte en la Tracy Bar para la plantilla principal (true) o para todos los componentes (all)? - debugger: ... # (true|false|'all') activado si Tracy está disponible (solo en modo de depuración) - - # genera las plantillas con la cabecera declare(strict_types=1) - strictTypes: ... # (bool) el valor predeterminado es false - - # activa el [modo estricto del parser |latte:develop#strict mode] - strictParsing: ... # (bool) el valor predeterminado es false - - # limita el alcance de las variables al cuerpo del bucle - scopedLoopVariables: ... # (bool) el valor predeterminado es false - - # elimina la indentación causada por el anidamiento en etiquetas pareadas - dedent: ... # (bool) el valor predeterminado es false - - # activa la [comprobación del código generado |latte:develop#Checking Generated Code] - phpLinter: ... # (string) el valor predeterminado es null - - # establece el locale - locale: cs_CZ # (string) el valor predeterminado es null - - # clase del objeto $this->template - templateClass: App\MyTemplateClass # el valor predeterminado es Nette\Bridges\ApplicationLatte\DefaultTemplate -``` - -Puede añadir nuevas [extensiones |latte:extending-latte#Latte Extension] así: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - - -Enrutamiento -============ - -Ajustes básicos: - -```neon -routing: - # ¿mostrar el panel de enrutamiento en la Tracy Bar? - debugger: ... # (bool) activado si Tracy está disponible (solo en modo de depuración) - - # serializa el router en el contenedor DI - cache: ... # (bool) el valor predeterminado es false -``` - -El enrutamiento se define normalmente en la clase [RouterFactory |routing#Colección de rutas]. Como alternativa, las rutas también se pueden definir en la configuración mediante parejas `máscara: acción`, pero este método no ofrece mucha flexibilidad: - -```neon -routing: - routes: - 'detail/': Admin:Home:default - '/': Front:Home:default -``` - - -Constantes -========== - -Creación de constantes de PHP. - -```neon -constants: - Foobar: 'baz' -``` - -La constante `Foobar` se creará al arrancar la aplicación. - -.[note] -Las constantes no deberían servir de variables globalmente accesibles. Para pasar valores a los objetos, use la [inyección de dependencias |dependency-injection:passing-dependencies]. - - -PHP -=== - -Ajuste de las directivas de PHP. Encontrará un resumen de todas las directivas en [php.net |https://www.php.net/manual/en/ini.list.php]. - -```neon -php: - date.timezone: Europe/Prague -``` - - -Servicios DI -============ - -Estos servicios se añaden al contenedor DI: - -| Nombre | Tipo | Descripción -|----------------------------|---------------------------------------------------|----------------------------------------- -| `application.application` | [api:Nette\Application\Application] | el [lanzador de la aplicación |how-it-works#Nette Application] -| `application.linkGenerator` | [api:Nette\Application\LinkGenerator] | [LinkGenerator |creating-links#LinkGenerator] -| `application.presenterFactory` | [api:Nette\Application\IPresenterFactory] | factory de presenters -| `application.###` | [api:Nette\Application\UI\Presenter] | los distintos presenters -| `routing.router` | [api:Nette\Routing\Router] | router -| `latte.latteFactory` | [api:Nette\Bridges\ApplicationLatte\LatteFactory] | factory del objeto `Latte\Engine` -| `latte.templateFactory` | [api:Nette\Application\UI\TemplateFactory] | factory de [`$this->template` |templates] diff --git a/application/es/creating-links.texy b/application/es/creating-links.texy deleted file mode 100644 index 17bb7234b1..0000000000 --- a/application/es/creating-links.texy +++ /dev/null @@ -1,319 +0,0 @@ -Creación de enlaces URL -*********************** - -
    - -Crear enlaces en Nette es tan fácil como señalar con el dedo. Basta con apuntar y el framework hará todo el trabajo por usted. Veremos: - -- cómo crear enlaces en las plantillas y fuera de ellas -- cómo distinguir un enlace a la página actual -- qué hacer con los enlaces no válidos - -
    - - -Gracias al [enrutamiento bidireccional |routing] nunca tendrá que escribir a fuego en las plantillas o en el código las URL de su aplicación, que quizá cambien más adelante o resulten complicadas de componer. En el enlace basta con indicar el presenter y la acción, pasar los parámetros que hagan falta, y el framework generará la URL por sí solo. En realidad es muy parecido a llamar a una función. Le va a gustar. - - -En la plantilla del presenter -============================= - -Lo más habitual es crear los enlaces en las plantillas, y el atributo `n:href` es una gran ayuda: - -```latte -detail -``` - -Fíjese en que, en lugar del atributo HTML `href`, hemos usado el [n:atributo |latte:syntax#n:atributos] `n:href`. Su valor no es una URL, como ocurriría con el atributo `href`, sino el nombre del presenter y la acción. - -Pulsar un enlace es, dicho de forma sencilla, algo así como llamar al método `ProductPresenter::renderShow()`. Y si tiene parámetros en su firma, podemos llamarlo con argumentos: - -```latte -product detail -``` - -También es posible pasar parámetros nombrados. El siguiente enlace pasa el parámetro `lang` con el valor `en`: - -```latte -product detail -``` - -Si el método `ProductPresenter::renderShow()` no tiene `$lang` en su firma, puede obtener el valor del parámetro con `$lang = $this->getParameter('lang')` o desde una [propiedad |presenters#Parámetros de la petición]. - -Si los parámetros están guardados en un array, se pueden expandir con el operador `...`: - -```latte -{var $args = [$product->id, lang => en]} -product detail -``` - -Los llamados [parámetros persistentes |presenters#Parámetros persistentes] también se pasan automáticamente en los enlaces. - -El atributo `n:href` resulta muy práctico en las etiquetas HTML ``. Si queremos imprimir el enlace en otro sitio, por ejemplo dentro de un texto, usamos `{link}`: - -```latte -URL is: {link Home:default} -``` - - -En el código -============ - -Para crear un enlace en el presenter se usa el método `link()`: - -```php -$url = $this->link('Product:show', $product->id); -``` - -Los parámetros también se pueden pasar como array, donde además se pueden indicar parámetros nombrados: - -```php -$url = $this->link('Product:show', [$product->id, 'lang' => 'en']); -``` - -Los enlaces también se pueden crear sin presenter, con el [#LinkGenerator] y su método `link()`. - -A veces necesita crear un enlace ahora pero generar la URL real más tarde. Para eso está el método `lazyLink()`, que devuelve un objeto `Nette\Application\UI\Link`. La ventaja es que puede pasar ese objeto de un sitio a otro, por ejemplo a una plantilla, y antes de que se renderice todavía puede ajustar sus parámetros con el método `setParameter()`. La URL propiamente dicha se compone solo cuando el objeto se convierte en cadena: - -```php -$link = $this->lazyLink('Product:show', $id); -// ... -echo $link; // la URL se genera solo aquí -``` - - -Enlaces a un presenter -====================== - -Si el destino del enlace es un presenter y una acción, la sintaxis es esta: - -``` -[//] [[[[:]module:]presenter:]action | this] [#fragment] -``` - -Este formato lo admiten todas las etiquetas de Latte y todos los métodos del presenter que trabajan con enlaces, es decir, `n:href`, `{link}`, `{plink}`, `link()`, `lazyLink()`, `isLinkCurrent()`, `redirect()`, `redirectPermanent()`, `forward()`, `canonicalize()` y también el [#LinkGenerator]. Así que, aunque en los ejemplos se use `n:href`, ahí podría estar cualquiera de esas funciones. - -La forma básica es, por tanto, `Presenter:acción`: - -```latte -home page -``` - -Si enlazamos a una acción del presenter actual, podemos omitir su nombre: - -```latte -home page -``` - -Si la acción de destino es `default`, la podemos omitir, pero los dos puntos deben quedarse: - -```latte -home page -``` - -Los enlaces también pueden apuntar a otros [módulos |directory-structure#Presenters y plantillas]. Aquí se distingue entre enlaces relativos a un submódulo anidado y enlaces absolutos. El principio es análogo al de las rutas de disco, solo que en lugar de barras se usan dos puntos. Suponiendo que el presenter actual forme parte del módulo `Front`, escribiríamos: - -```latte -link to Front:Shop:Product:show -link to Admin:Product:show -``` - -Un caso especial es el enlace [a sí mismo |#Enlace a la página actual], donde indicamos `this` como destino. - -```latte -refresh -``` - -Podemos enlazar a una parte concreta de la página mediante un fragmento tras el signo de almohadilla `#`: - -```latte -link to Home:default and fragment #main -``` - -.{data-version:3.3.0} -El fragmento también se puede fijar dinámicamente como argumento con la clave `#`. Su valor se codifica automáticamente y tiene prioridad sobre el fragmento indicado en el destino: - -```php -$this->link('Home:default', ['#' => $fragment]); -``` - - -Rutas absolutas -=============== - -Los enlaces generados con `link()` o `n:href` son siempre rutas absolutas (es decir, empiezan por `/`), pero no URL absolutas con protocolo y dominio, como `https://domain`. - -Para generar una URL absoluta, añada dos barras al principio (por ejemplo, `n:href="//Home:"`). Como alternativa, puede hacer que el presenter genere solo enlaces absolutos poniendo `$this->absoluteUrls = true`. - -En la plantilla también se puede usar el filtro `|absoluteUrl` para convertir una ruta relativa en absoluta. - - -Enlace a la página actual -========================= - -El destino `this` crea un enlace a la página actual: - -```latte -refresh -``` - -Al mismo tiempo se transfieren todos los parámetros indicados en la firma del método `action()` o `render()` (si `action()` no está definido). Así, si estamos en la página `Product:show` con `id: 123`, el enlace a `this` pasará también ese parámetro. - -Por supuesto, es posible indicar los parámetros directamente: - -```latte -refresh -``` - -La función `isLinkCurrent()` comprueba si el destino del enlace es idéntico a la página actual. Esto se puede usar, por ejemplo, en una plantilla para distinguir los enlaces, etc. - -Los parámetros son los mismos que los del método `link()`, pero además es posible usar el comodín `*` en lugar de una acción concreta, lo que significa cualquier acción del presenter dado. - -```latte -{if !isLinkCurrent('Admin:login')} - Login -{/if} - -
  • - ... -
  • -``` - -Combinado con `n:href` en un mismo elemento, se puede usar una forma abreviada: - -```latte -... -``` - -El comodín `*` solo se puede usar en lugar de la acción, no del presenter. - -Para saber si estamos en un módulo concreto o en uno de sus submódulos, use el método `isModuleCurrent(moduleName)`. - -```latte -
  • - ... -
  • -``` - - -Cambio de la base de los enlaces .{data-version:3.2.7} -====================================================== - -De forma predeterminada, los enlaces relativos se derivan del presenter actual. Esto se puede cambiar con `{linkBase}`: - -```latte -{linkBase Admin:Dashboard} -product detail -``` - -El enlace llevará a `Admin:Dashboard:Product:show`. Solo se ven afectados los enlaces relativos: los absolutos, que empiezan por dos puntos, y los enlaces al presenter actual (`this`, `show`) quedan sin cambios. - -`{linkBase}` vale para toda la plantilla y resulta especialmente útil en las plantillas de layout, donde garantiza enlaces coherentes con independencia del presenter que las use. -La etiqueta debe colocarse al principio de la plantilla, o lanzará una `CompileException`. - - -Enlaces a una señal -=================== - -El destino de un enlace no tiene por qué ser solo un presenter y una acción, sino también una [señal |components#Señal] (que llama al método `handle()`). La sintaxis es entonces esta: - -``` -[//] [sub-component:]signal! [#fragment] -``` - -La señal se distingue, pues, por el signo de exclamación: - -```latte -signal -``` - -También puede crear un enlace a la señal de un subcomponente (o de un subsubcomponente): - -```latte -signal -``` - - -Enlaces en un componente -======================== - -Como los [componentes|components] son unidades independientes y reutilizables que no deberían tener ningún vínculo con los presenters que los rodean, aquí los enlaces funcionan de forma algo distinta. El atributo de Latte `n:href` y la etiqueta `{link}`, así como los métodos del componente como `link()` y demás, **consideran siempre que el destino del enlace es el nombre de una señal**. Por eso ni siquiera hace falta poner el signo de exclamación: - -```latte -signal, not an action -``` - -Si en la plantilla del componente quisiéramos enlazar a presenters, usaríamos la etiqueta `{plink}`: - -```latte -home -``` - -o en el código - -```php -$this->getPresenter()->link('Home:default') -``` - - -Alias .{data-version:3.2.3} -=========================== - -A veces puede resultar útil asignar a una pareja Presenter:acción un alias fácil de recordar. Por ejemplo, llamar a la página de inicio `Front:Home:default` simplemente `home`, o a `Admin:Dashboard:default` llamarla `admin`. - -Los alias se definen en la [configuración|configuration], bajo la clave `application › aliases`: - -```neon -application: - aliases: - home: Front:Home:default - admin: Admin:Dashboard:default - sign: Front:Sign:in -``` - -En los enlaces se escriben después con una arroba, por ejemplo: - -```latte -administration -``` - -También están admitidos en todos los métodos que trabajan con enlaces, como `redirect()` y similares. - - -Enlaces no válidos -================== - -Puede ocurrir que creemos un enlace no válido, ya sea porque lleva a un presenter inexistente, porque pasa más parámetros de los que acepta en su firma el método de destino, o porque no se puede generar ninguna URL para la acción de destino. Cómo tratar los enlaces no válidos se fija en el presenter con `$this->invalidLinkMode`. Puede tomar una combinación de estos valores (constantes): - -- `Presenter::InvalidLinkSilent` - modo silencioso, devuelve el carácter # como URL -- `Presenter::InvalidLinkWarning` - se lanza una advertencia E_USER_WARNING, que se registrará en modo de producción, pero no interrumpirá la ejecución del script -- `Presenter::InvalidLinkTextual` - advertencia visual, imprime el error directamente en el enlace -- `Presenter::InvalidLinkException` - lanza InvalidLinkException - -El ajuste predeterminado es `InvalidLinkWarning` en modo de producción e `InvalidLinkWarning | InvalidLinkTextual` en modo de desarrollo. `InvalidLinkWarning` en el entorno de producción no interrumpe el script, pero la advertencia queda registrada. En el entorno de desarrollo, [Tracy |tracy:] la captura y muestra una pantalla azul. `InvalidLinkTextual` funciona devolviendo como URL un mensaje de error que empieza por los caracteres `#error:`. Para que esos enlaces salten a la vista, añada esto a su CSS: - -```css -a[href^="#error:"] { - background: red; - color: white; -} -``` - -Si no queremos que se produzcan advertencias en el entorno de desarrollo, podemos silenciarlas directamente en la [configuración|configuration]. - -```neon -application: - silentLinks: true -``` - - -LinkGenerator -============= - -¿Cómo crear enlaces con una comodidad parecida a la del método `link()`, pero sin la presencia de un presenter? Para eso está [api:Nette\Application\LinkGenerator]. - -LinkGenerator es un servicio que puede hacerse pasar por el constructor y con cuyo método `link()` puede crear después los enlaces. - -Hay una diferencia respecto a los presenters. LinkGenerator crea todos los enlaces directamente como URL absolutas. Además, no existe un "presenter actual", así que no es posible indicar como destino solo el nombre de la acción, `link('default')`, ni usar rutas relativas a los módulos. - -Los enlaces no válidos lanzan siempre `Nette\Application\UI\InvalidLinkException`. diff --git a/application/es/directory-structure.texy b/application/es/directory-structure.texy deleted file mode 100644 index 057717ee41..0000000000 --- a/application/es/directory-structure.texy +++ /dev/null @@ -1,526 +0,0 @@ -Estructura de directorios de la aplicación -****************************************** - -
    - -¿Cómo diseñar una estructura de directorios clara y escalable para los proyectos en Nette Framework? Le mostraremos prácticas probadas que le ayudarán a organizar el código. Aprenderá: - -- cómo **estructurar lógicamente** la aplicación en directorios -- cómo diseñar la estructura para que **escale bien** a medida que el proyecto crece -- cuáles son las **alternativas posibles** y sus ventajas o inconvenientes - -
    - - -Es importante mencionar que Nette Framework en sí no impone ninguna estructura concreta. Está diseñado para adaptarse fácilmente a cualquier necesidad y preferencia. - - -Estructura básica del proyecto -============================== - -Aunque Nette Framework no dicta ninguna estructura de directorios fija, existe una disposición predeterminada probada en forma del [Web Project|https://github.com/nette/web-project]: - -/--pre -web-project/ -├── app/ ← directorio de la aplicación -├── assets/ ← archivos SCSS, JS, imágenes..., alternativamente resources/ -├── bin/ ← scripts para la línea de comandos -├── config/ ← configuración -├── log/ ← errores registrados -├── temp/ ← archivos temporales, caché -├── tests/ ← tests -├── vendor/ ← bibliotecas instaladas por Composer -└── www/ ← directorio público (document-root) -\-- - -Puede modificar esta estructura libremente según sus necesidades: renombrar o mover carpetas. Después solo hace falta ajustar las rutas relativas a los directorios en `Bootstrap.php` y, eventualmente, en `composer.json`. Nada más, ninguna reconfiguración complicada, ningún cambio de constantes. Nette dispone de una autodetección inteligente y reconoce automáticamente la ubicación de la aplicación, incluida su URL base. - - -Principios de organización del código -===================================== - -Cuando explora un proyecto nuevo por primera vez, debería poder orientarse rápidamente. Imagine que hace clic en el directorio `app/Model/` y ve esta estructura: - -/--pre -app/Model/ -├── Services/ -├── Repositories/ -└── Entities/ -\-- - -De aquí solo se entera de que el proyecto usa unos servicios, unos repositorios y unas entidades. No se entera de nada sobre el propósito real de la aplicación. - -Veamos otro enfoque distinto: la **organización por dominios**: - -/--pre -app/Model/ -├── Cart/ -├── Payment/ -├── Order/ -└── Product/ -\-- - -Aquí es diferente: a primera vista está claro que se trata de una tienda online. Los propios nombres de los directorios revelan lo que sabe hacer la aplicación: trabaja con pagos, pedidos y productos. - -El primer enfoque (organización por tipo de clase) trae en la práctica varios problemas: el código que está lógicamente relacionado queda fragmentado entre distintas carpetas y hay que ir saltando de una a otra. Por eso organizaremos por dominios. - - -Espacios de nombres -------------------- - -Es habitual que la estructura de directorios se corresponda con los espacios de nombres de la aplicación. Eso significa que la ubicación física de los archivos coincide con su espacio de nombres. Por ejemplo, una clase situada en `app/Model/Product/ProductRepository.php` debería tener el espacio de nombres `App\Model\Product`. Este principio ayuda a orientarse en el código y simplifica la carga automática. - - -Singular y plural en los nombres --------------------------------- - -Fíjese en que usamos el singular para los directorios principales de la aplicación: `app`, `config`, `log`, `temp`, `www`. Lo mismo dentro de la aplicación: `Model`, `Core`, `Presentation`. Es porque cada uno representa un único concepto coherente. - -De forma parecida, `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 `nokia.php`, `samsung.php`). Es un espacio de nombres que contiene clases para trabajar con productos: `ProductRepository.php`, `ProductService.php`. - -La carpeta `app/Tasks` está en plural porque contiene un conjunto de scripts ejecutables independientes: `CleanupTask.php`, `ImportTask.php`. Cada uno de ellos es una unidad autónoma. - -Por coherencia, recomendamos usar: -- singular para los espacios de nombres que representan una unidad funcional (aunque trabajen con varias entidades) -- plural para las colecciones de unidades independientes -- en caso de duda, o si no quiere darle vueltas, elija el singular - - -Directorio público `www/` -========================= - -Este directorio es el único accesible desde la web (el document-root). A menudo puede encontrarse con el nombre `public/` en lugar de `www/`: es solo cuestión de convención y no afecta al funcionamiento de la aplicación. El directorio contiene: -- el [punto de entrada |bootstrapping#index.php] de la aplicación `index.php` -- el archivo `.htaccess` con las reglas para mod_rewrite (para Apache) -- archivos estáticos (CSS, JavaScript, imágenes) -- archivos subidos - -Para la correcta seguridad de la aplicación es crucial tener el [document-root configurado correctamente |nette:troubleshooting#¿Cómo cambiar o quitar el directorio www de la URL?]. - -.[note] -Nunca coloque en este directorio la carpeta `node_modules/`: contiene miles de archivos que pueden ser ejecutables y que no deberían ser accesibles públicamente. - - -Directorio de la aplicación `app/` -================================== - -Este es el directorio principal, el que contiene el código de la aplicación. Estructura básica: - -/--pre -app/ -├── Core/ ← asuntos de infraestructura -├── Model/ ← lógica de negocio -├── Presentation/ ← presenters y plantillas -├── Tasks/ ← scripts de comandos -└── Bootstrap.php ← clase de arranque de la aplicación -\-- - -`Bootstrap.php` es la [clase de arranque de la aplicación|bootstrapping] que inicializa el entorno, carga la configuración y crea el contenedor DI. - -Veamos ahora con más detalle cada uno de los subdirectorios. - - -Presenters y plantillas -======================= - -La parte de presentación de la aplicación está en el directorio `app/Presentation`. Una alternativa es el más corto `app/UI`. Es el lugar de todos los presenters, sus plantillas y las posibles clases auxiliares asociadas. - -Esta capa la organizamos por dominios. En un proyecto complejo que combine tienda online, blog y API, la estructura sería así: - -/--pre -app/Presentation/ -├── Shop/ ← frontend de la tienda -│ ├── Product/ -│ ├── Cart/ -│ └── Order/ -├── Blog/ ← blog -│ ├── Home/ -│ └── Post/ -├── Admin/ ← administración -│ ├── Dashboard/ -│ └── Products/ -└── Api/ ← endpoints de la API - └── V1/ -\-- - -Por el contrario, para un blog sencillo usaríamos la siguiente 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 presenters y plantillas. Carpetas como `Front/`, `Admin/` o `Api/` se llaman **módulos**. Técnicamente son directorios corrientes que sirven para dividir lógicamente la aplicación. - -Cada carpeta que contiene un presenter incluye el propio archivo del presenter y sus plantillas. Por ejemplo, la carpeta `Dashboard/` contiene: - -/--pre -Dashboard/ -├── DashboardPresenter.php ← presenter -└── default.latte ← plantilla -\-- - -Esta estructura de directorios se refleja en los espacios de nombres de las clases. Por ejemplo, `DashboardPresenter` está en el espacio de nombres `App\Presentation\Admin\Dashboard` (véase [#Mapeo de presenters]): - -```php -namespace App\Presentation\Admin\Dashboard; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -Al presenter `Dashboard` dentro del módulo `Admin` nos referimos en la aplicación con la notación de dos puntos como `Admin:Dashboard`. A su acción `default` nos referimos entonces como `Admin:Dashboard:default`. Con módulos anidados usamos varios grupos de dos puntos, por ejemplo `Shop:Order:Detail:default`. - - -Desarrollo flexible de la estructura ------------------------------------- - -Una de las grandes ventajas de esta estructura es lo elegantemente que se adapta a las necesidades crecientes del proyecto. Tomemos como ejemplo la parte que genera los feeds XML. Al principio tenemos una forma sencilla: - -/--pre -Export/ -├── ExportPresenter.php ← un presenter para todas las exportaciones -├── sitemap.latte ← plantilla para el sitemap -└── feed.latte ← plantilla para el feed RSS -\-- - -Con el tiempo se añaden más tipos de feed y necesitamos más lógica para ellos... ¡Ningún 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 las nuevas subcarpetas, repartir el código en ellas y actualizar los enlaces (p. ej. de `Export:feed` a `Export:Feed:amazon`). Gracias a eso podemos ampliar la estructura gradualmente según haga falta; el nivel de anidamiento no está limitado de ninguna manera. - -Por ejemplo, si en la administración tiene muchos presenters relacionados con la gestión de pedidos, como `OrderDetail`, `OrderEdit`, `OrderDispatch`, etc., puede crear para una mejor organización un módulo (carpeta) llamado `Order`, que contendrá (las carpetas de) los presenters `Detail`, `Edit`, `Dispatch` y otros. - - -Ubicación de las plantillas ---------------------------- - -En los ejemplos anteriores hemos visto que las plantillas están directamente en la carpeta del presenter: - -/--pre -Dashboard/ -├── DashboardPresenter.php ← presenter -├── DashboardTemplate.php ← clase de plantilla opcional -└── default.latte ← plantilla -\-- - -Esta ubicación resulta ser la más cómoda en la práctica: tiene todos los archivos relacionados a mano. - -Alternativamente puede colocar las plantillas en una subcarpeta `templates/`. Nette admite ambas variantes. Incluso puede colocar las plantillas completamente fuera de la carpeta `Presentation/`. Todo sobre las opciones de ubicación de las plantillas lo encontrará en el capítulo [Búsqueda de plantillas |templates#Búsqueda de plantillas]. - - -Clases auxiliares y componentes -------------------------------- - -A los presenters y las plantillas los acompañan a menudo otros archivos auxiliares. Los colocamos de forma lógica según su alcance: - -1. **Directamente junto al presenter** en el caso de componentes específicos de ese presenter: - -/--pre -Product/ -├── ProductPresenter.php -├── ProductGrid.php ← componente para el listado de productos -└── FilterForm.php ← formulario para filtrar -\-- - -2. **Para el módulo**: recomendamos usar la carpeta `Accessory`, que queda convenientemente al principio por orden alfabético: - -/--pre -Front/ -├── Accessory/ -│ ├── NavbarControl.php ← componentes para el frontend -│ └── TemplateFilters.php -├── Product/ -└── Cart/ -\-- - -3. **Para toda la aplicación**: en `Presentation/Accessory/`: -/--pre -app/Presentation/ -├── Accessory/ -│ ├── LatteExtension.php -│ └── TemplateFilters.php -├── Front/ -└── Admin/ -\-- - -Alternativamente puede colocar las clases auxiliares 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. - - -Model: el corazón de la aplicación -================================== - -El modelo contiene toda la lógica de negocio de la aplicación. La regla para organizarlo es de nuevo: estructurar por dominios: - -/--pre -app/Model/ -├── Payment/ ← todo sobre los pagos -│ ├── PaymentFacade.php ← punto de entrada principal -│ ├── PaymentRepository.php -│ ├── Payment.php ← entidad -├── Order/ ← todo sobre los pedidos -│ ├── OrderFacade.php -│ ├── OrderRepository.php -│ ├── Order.php -└── Shipping/ ← todo sobre los envíos -\-- - -En el modelo se encuentra normalmente con estos tipos de clases: - -**Facades**: representan el punto de entrada principal a un dominio concreto dentro de la aplicación. Actúan como orquestador que coordina la colaboración entre distintos servicios para implementar casos de uso completos (como "crear un pedido" o "procesar un pago"). Bajo su capa de orquestación, la fachada oculta al resto de la aplicación los detalles de implementación y ofrece así una interfaz limpia para trabajar con ese dominio. - -```php -class OrderFacade -{ - public function createOrder(Cart $cart): Order - { - // validación - // creación del pedido - // envío del correo - // escritura en las estadísticas - } -} -``` - -**Servicios**: se centran en operaciones de negocio concretas dentro de un dominio. A diferencia de las fachadas, que orquestan casos de uso completos, un servicio implementa una lógica de negocio concreta (como el cálculo de precios o el procesamiento de pagos). Los servicios normalmente no tienen estado y pueden usarlos las fachadas como bloques de construcción para operaciones más complejas, o bien directamente otras partes de la aplicación para tareas más simples. - -```php -class PricingService -{ - public function calculateTotal(Order $order): Money - { - // cálculo del precio - } -} -``` - -**Repositorios**: se ocupan de toda la comunicación con el almacén de datos, típicamente una base de datos. Su tarea es cargar y guardar entidades e implementar métodos para buscarlas. El repositorio aísla al resto de la aplicación de los detalles de implementación de la base de datos y ofrece 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 propia identidad y cambian con el tiempo. Normalmente son clases mapeadas a tablas de la base de datos mediante un 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 mapeada a la tabla 'orders' de la base de datos -class Order extends Nette\Database\Table\ActiveRow -{ - public function addItem(Product $product, int $quantity): void - { - $this->related('order_items')->insert([ - 'product_id' => $product->id, - 'quantity' => $quantity, - 'unit_price' => $product->price, - ]); - } -} -``` - -**Value Objects**: objetos inmutables que representan valores sin identidad propia, por ejemplo un importe monetario o una dirección de correo. Dos instancias de un value object con los mismos valores se consideran idénticas. - - -Código de infraestructura -========================= - -La carpeta `Core/` (o alternativamente `Infrastructure/`) alberga la base técnica de la aplicación. El código de infraestructura incluye normalmente: - -/--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 monitorizació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 basta naturalmente con una estructura plana: - -/--pre -Core/ -├── RouterFactory.php -├── Authenticator.php -└── QueueMailer.php -\-- - -Es el código que: - -- se ocupa de la infraestructura técnica (enrutamiento, registro, 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 de un dominio concreto: la caché o el logger funcionan igual para una tienda online que para un blog. - -¿Se pregunta si una determinada clase pertenece aquí o al modelo? La diferencia clave es que el código de `Core/`: - -- no sabe nada del dominio (productos, pedidos, artículos) -- normalmente se puede trasladar a otro proyecto -- resuelve el "cómo funciona" (cómo enviar un correo), no el "qué hace" (qué correo enviar) - -Un ejemplo para entenderlo mejor: - -- `App\Core\MailerFactory` crea instancias de la clase para enviar correos y se ocupa de la configuración SMTP -- `App\Model\OrderMailer` usa `MailerFactory` para enviar correos sobre pedidos, conoce sus plantillas y sabe cuándo deben enviarse - - -Scripts de comandos -=================== - -Las aplicaciones necesitan a menudo realizar actividades fuera de las peticiones HTTP habituales, ya sea el procesamiento de datos en segundo plano, el mantenimiento o tareas periódicas. Para ejecutarlas sirven scripts sencillos en el directorio `bin/`, mientras que la lógica de implementación propiamente dicha se coloca en `app/Tasks/` (o `app/Commands/`). - -Ejemplo: - -/--pre -app/Tasks/ -├── Maintenance/ ← scripts de mantenimiento -│ ├── CleanupCommand.php ← borrado 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 regulares - ├── NewsletterCommand.php ← envío de newsletters - └── ReminderCommand.php ← avisos a los clientes -\-- - -¿Qué pertenece al modelo y qué a los scripts de comandos? Por ejemplo, la lógica del envío de un único correo forma parte del modelo, mientras que el envío masivo de miles de correos pertenece a `Tasks/`. - -Las tareas se ejecutan normalmente desde la línea de comandos o mediante cron: el script de `bin/` crea el contenedor DI con el método [bootConsoleApplication() |bootstrapping#Entornos distintos] y saca de él el servicio necesario. También se pueden ejecutar mediante una petición HTTP, pero hay que pensar en la seguridad. El presenter que ejecuta la tarea necesita estar protegido, por ejemplo solo para usuarios conectados o con un token fuerte y acceso desde direcciones IP permitidas. Para las tareas de larga duración es necesario aumentar el límite de tiempo del script y usar `session_write_close()` para no bloquear la sesión. - - -Otros directorios posibles -========================== - -Además de los directorios básicos mencionados puede añadir otras carpetas especializadas según las necesidades del proyecto. Veamos las más habituales y su uso: - -/--pre -app/ -├── Api/ ← lógica de la API independiente de la capa de presentación -├── Database/ ← scripts de migración y seeders para datos de prueba -├── Components/ ← componentes visuales compartidos por toda la aplicación -├── Event/ ← útil si se usa una arquitectura orientada a eventos -├── Mail/ ← plantillas de correo y la lógica relacionada -└── Utils/ ← clases auxiliares -\-- - -Para los componentes visuales compartidos que se usan en los presenters de toda la aplicación puede usar 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 quiere compartir componentes entre varios proyectos, conviene extraerlos a un paquete de Composer separado. - -En el directorio `app/Mail` puede colocar la gestión de la comunicación por correo: - -/--pre -app/Mail/ -├── templates/ ← plantillas de correo -│ ├── order-confirmation.latte -│ └── welcome.latte -└── OrderMailer.php -\-- - - -Mapeo de presenters -=================== - -El mapeo define las reglas para derivar el nombre de la clase a partir del nombre del presenter. Las indicamos en la [configuración|configuration] bajo la clave `application › mapping`. - -En esta página hemos mostrado que colocamos los presenters en la carpeta `app/Presentation` (o `app/UI`). Desde Nette Application 3.3 esta es la convención predeterminada, que no hace falta configurar. Si usa una estructura distinta o quiere indicar el mapeo explícitamente, la configuración predeterminada corresponde a esta línea: - -```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 de los presenters caigan bajo el espacio de nombres `App\Presentation`, de modo que el presenter `Home` se mapee 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 presenter `Home`, lo que da como resultado el nombre final de la clase `App\Presentation\HomePresenter`. ¡Sencillo! - -Sin embargo, como ve en los ejemplos de este y otros capítulos, colocamos las clases de los presenters en subdirectorios del mismo nombre, por ejemplo el presenter `Home` se mapea a la clase `App\Presentation\Home\HomePresenter`. Esto lo conseguimos usando un asterisco doble `**` (requiere Nette Application 3.2.3): - -```neon -application: - mapping: App\Presentation\**Presenter -``` - -Ahora pasamos al mapeo de presenters en módulos. Podemos definir un mapeo específico 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 presenter `Front:Home` se mapea a la clase `App\Presentation\Front\Home\HomePresenter`, mientras que el presenter `Api:OAuth` se mapea a la clase `App\Api\OAuthPresenter`. - -Como los módulos `Front` y `Admin` tienen un patrón de mapeo parecido, y es probable que haya más módulos así, es posible crear una regla general que los sustituya. A la máscara de la clase se le añade un nuevo asterisco para el módulo: - -```neon -application: - mapping: - *: App\Presentation\*\**Presenter - Api: App\Api\*Presenter -``` - -Funciona también para estructuras de directorios anidadas más profundamente, como el presenter `Admin:User:Edit`, donde el segmento con el asterisco se repite para cada nivel de módulo, dando como resultado la clase `App\Presentation\Admin\User\Edit\EditPresenter`. - -Una notación alternativa consiste en usar, en lugar de una cadena, un array formado por tres segmentos. Para los ejemplos mostrados arriba, 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 deleted file mode 100644 index 09a378001d..0000000000 --- a/application/es/how-it-works.texy +++ /dev/null @@ -1,200 +0,0 @@ -¿Cómo funcionan las aplicaciones? -********************************* - -
    - -Está leyendo el capítulo fundacional de la documentación de Nette. Aprenderá de la A a la Z todo el principio de funcionamiento de las aplicaciones web, desde el instante en que nace una petición hasta que el script PHP termina de ejecutarse. Tras leerlo entenderá: - -- cómo funciona todo -- qué son Bootstrap, Presenter y el contenedor DI -- qué aspecto tiene la estructura de directorios - -
    - - -Estructura de directorios -========================= - -Abra el esqueleto de ejemplo de una aplicación web llamado [WebProject|https://github.com/nette/web-project]. Mientras lee, puede consultar los archivos de los que hablamos. - -La estructura de directorios tiene más o menos este aspecto: - -/--pre -web-project/ -├── app/ ← directorio de la aplicación -│ ├── Core/ ← clases básicas necesarias para funcionar -│ │ └── RouterFactory.php ← configuración de las direcciones URL -│ ├── Presentation/ ← presenters, plantillas y compañía -│ │ ├── @layout.latte ← plantilla del layout -│ │ └── Home/ ← directorio del presenter Home -│ │ ├── HomePresenter.php ← clase del presenter Home -│ │ └── default.latte ← plantilla de la acción default -│ └── Bootstrap.php ← clase de arranque Bootstrap -├── assets/ ← recursos (SCSS, TypeScript, imágenes fuente) -├── bin/ ← scripts ejecutados desde la línea de comandos -├── config/ ← archivos de configuración -│ ├── common.neon -│ └── services.neon -├── log/ ← errores registrados -├── temp/ ← archivos temporales, caché, … -├── vendor/ ← bibliotecas instaladas por Composer -│ ├── ... -│ └── autoload.php ← autocarga de todos los paquetes instalados -├── www/ ← directorio público, document root del proyecto -│ ├── assets/ ← archivos estáticos compilados (CSS, JS, imágenes, ...) -│ ├── .htaccess ← reglas de mod_rewrite -│ └── index.php ← archivo inicial que lanza la aplicación -└── .htaccess ← prohíbe el acceso a todos los directorios salvo www -\-- - -Puede cambiar la estructura de directorios como quiera, renombrar o mover carpetas; es del todo flexible. Nette dispone además de una autodetección inteligente y reconoce automáticamente la ubicación de la aplicación, incluida su base de URL. - -En aplicaciones algo mayores podemos organizar las carpetas de presenters y plantillas en [subdirectorios |directory-structure#Presenters y plantillas] y agrupar las clases en espacios de nombres, lo que llamamos módulos. - -El directorio `www/` representa el directorio público o document-root del proyecto. Puede renombrarlo sin necesidad de configurar nada más del lado de la aplicación. Solo hace falta [configurar el alojamiento |nette:troubleshooting#¿Cómo cambiar o quitar el directorio www de la URL?] para que el document-root apunte a ese directorio. - -También puede descargar WebProject directamente, con Nette incluido, mediante [Composer |best-practices:composer]: - -```shell -composer create-project nette/web-project -``` - -En Linux o macOS, dé [permisos de escritura |nette:troubleshooting#Establecer los permisos de los directorios] a los directorios `log/` y `temp/`. - -La aplicación WebProject está lista para funcionar; no hace falta configurar absolutamente nada y puede verla directamente en el navegador accediendo a la carpeta `www/`. - - -Petición HTTP -============= - -Todo empieza cuando un usuario abre una página en su navegador. El navegador envía una petición HTTP al servidor. Esa petición apunta a un único archivo PHP situado en el directorio público `www/`, que es `index.php`. Supongamos que la petición es a la dirección `https://example.com/product/123`. Gracias a una [configuración del servidor |nette:troubleshooting#¿Cómo configurar el servidor para URL bonitas?] adecuada, también esa URL se asigna al archivo `index.php`, que se ejecuta entonces. - -Su tarea es: - -1) inicializar el entorno -2) obtener la factory -3) ejecutar la aplicación de Nette, que atiende la petición - -¿Qué factory? ¡No fabricamos tractores, construimos webs! Espere, se explicará enseguida. - -Por "inicializar el entorno" entendemos, por ejemplo, activar [Tracy|tracy:], una herramienta magnífica para registrar o visualizar errores. En un servidor de producción registra los errores; en un entorno de desarrollo los muestra directamente. La inicialización incluye, por tanto, determinar si el sitio funciona en modo de producción o de desarrollo. Nette usa para ello una [autodetección inteligente |bootstrapping#Modo de desarrollo y modo de producción]: si ejecuta el sitio en localhost, funciona en modo de desarrollo. No necesita configurar nada y la aplicación está lista al instante tanto para el desarrollo como para el despliegue en vivo. Estos pasos se realizan y se describen en detalle en el capítulo sobre la [clase Bootstrap|bootstrapping]. - -El tercer punto (sí, nos hemos saltado el segundo, pero volveremos a él) es lanzar la aplicación. De atender las peticiones HTTP en Nette se encarga la clase `Nette\Application\Application` (en adelante, `Application`). Así que, cuando decimos ejecutar la aplicación, nos referimos en concreto a llamar al método, muy bien llamado, `run()` de un objeto de esta clase. - -Nette actúa como un mentor y le guía para que escriba aplicaciones limpias según metodologías probadas. Una de las más asentadas es la **inyección de dependencias**, abreviada DI. No queremos cargarle ahora con la explicación de la DI; para eso hay un [capítulo aparte|dependency-injection:introduction]. La consecuencia esencial es que los objetos clave los suele crear una factory de objetos conocida como **contenedor DI** (o DIC). Sí, esa es la factory mencionada antes. Ella produce también el objeto `Application` para nosotros, y por eso necesitamos primero el contenedor. Lo obtenemos con la clase `Configurator`, dejamos que cree el objeto `Application`, llamamos a su método `run()` y con ello arranca la aplicación de Nette. Eso es justo lo que ocurre en el archivo [index.php |bootstrapping#index.php]. - - -Nette Application -================= - -La clase `Application` tiene una única tarea: responder a la petición HTTP. - -Las aplicaciones escritas en Nette se dividen en muchos presenters (en otros frameworks puede encontrarse el término "controlador", que es en esencia lo mismo). Son clases que representan cada una una página concreta del sitio web: la página de inicio, un producto de una tienda en línea, un formulario de acceso, un feed de sitemap, etc. Una aplicación puede tener desde un presenter hasta miles. - -`Application` empieza preguntando al router qué presenter debe atender la petición actual. El router determina la responsabilidad. Examina la URL de entrada `https://example.com/product/123` y, según su configuración, decide que esa tarea corresponde, por ejemplo, al **presenter** `Product`, que debe ejecutar la **acción** `show` para el producto de `id: 123`. Es buena práctica escribir la pareja presenter + acción separada por dos puntos, así: `Product:show`. - -El router ha transformado, por tanto, la URL en la pareja `Presenter:acción` + parámetros, en nuestro caso `Product:show` + `id: 123`. Puede ver qué aspecto tiene ese router en el archivo `app/Core/RouterFactory.php`, y lo describimos en detalle en el capítulo [Enrutamiento |Routing]. - -Continuemos. `Application` conoce ya el nombre del presenter y puede seguir adelante. Lo hace creando una instancia de la clase `ProductPresenter`, que contiene el código del presenter `Product`. Más exactamente, pide al contenedor DI que cree el presenter, porque crear objetos es cosa suya. - -El presenter podría tener este aspecto: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ProductRepository $repository, - ) { - } - - public function renderShow(int $id): void - { - // obtiene los datos del modelo y se los pasa a la plantilla - $this->template->product = $this->repository->getProduct($id); - } -} -``` - -El presenter toma el relevo en la atención de la petición. La tarea está clara: ejecutar la acción `show` con `id: 123`. En la terminología de los presenters, eso significa que se llama al método `renderShow()`, que recibe `123` en el parámetro `$id`. - -Un presenter puede atender varias acciones, es decir, puede tener varios métodos `render()`. Recomendamos, sin embargo, diseñar los presenters con una acción, o con las menos posibles. - -Así pues, se ha llamado al método `renderShow(123)`. Su código es un ejemplo ficticio, pero muestra cómo se pasan los datos a la plantilla: escribiendo en `$this->template`. - -A continuación, el presenter devuelve una respuesta. Puede ser una página HTML, una imagen, un documento XML, el envío de un archivo del disco, JSON o quizá una redirección a otra página. Lo importante es que, si no indicamos explícitamente cómo responder (que es el caso de `ProductPresenter`), la respuesta será renderizar una plantilla en una página HTML. ¿Por qué? Porque en el 99 % de los casos queremos renderizar una plantilla. Por eso el presenter adopta ese comportamiento como predeterminado, para simplificarnos el trabajo. Esa es la esencia de Nette. - -Ni siquiera necesitamos indicar qué plantilla renderizar; el framework deduce la ruta automáticamente. En el caso de la acción `show`, simplemente intenta cargar la plantilla `show.latte` situada en el mismo directorio que la clase `ProductPresenter`. También trata de encontrar el layout en el archivo `@layout.latte` (más detalles en [búsqueda de plantillas |templates#Búsqueda de plantillas]). - -Después se renderizan las plantillas. Con eso termina la tarea del presenter y de toda la aplicación. Si la plantilla no existe, se devuelve una página de error 404. Puede aprender más sobre los presenters en la página [Presenters|presenters]. - -[* request-flow.svg *] - -Por si acaso, recapitulemos todo el proceso con una URL ligeramente distinta: - -1) La URL es `https://example.com` -2) La aplicación arranca, se crea el contenedor DI y se ejecuta `Application::run()`. -3) El router decodifica la URL en la pareja `Home:default`. -4) Se crea una instancia de la clase `HomePresenter`. -5) Se llama al método `renderDefault()` (si existe). -6) Se renderiza la plantilla, por ejemplo `default.latte`, junto con el layout, por ejemplo `@layout.latte`. - - -Puede que se haya encontrado ahora con muchos conceptos nuevos, pero creemos que tienen sentido. Desarrollar aplicaciones en Nette es notablemente sencillo. - - -Plantillas -========== - -Puesto que hablamos de plantillas, Nette usa el sistema de plantillas [Latte |latte:]. Por eso los archivos de plantilla tienen la extensión `.latte`. Latte se emplea ante todo porque es el sistema de plantillas más seguro para PHP, y también el más intuitivo. No necesita aprender mucho nuevo; basta con saber PHP y unas cuantas etiquetas. Encontrará todo lo necesario [en la documentación |templates]. - -En la plantilla, los [enlaces |creating-links] a otros presenters y acciones se crean así: - -```latte -product detail -``` - -Basta con escribir la conocida pareja `Presenter:acción` en lugar de la URL real e incluir los parámetros necesarios. El truco está en `n:href`, que le dice a Nette que procese ese atributo. Generará entonces: - -```latte -product detail -``` - -De generar las URL se encarga el router mencionado antes. Los routers de Nette son excepcionales porque saben hacer no solo la transformación de una URL en la pareja `Presenter:acción`, sino también la inversa: generar una URL a partir del nombre del presenter, la acción y los parámetros. Gracias a ello puede cambiar por completo el formato de las URL de toda su aplicación terminada sin tocar un solo carácter de las plantillas ni de los presenters: basta con modificar el router. Esto permite además la llamada canonización, otra función única de Nette que mejora el SEO (optimización para buscadores) al impedir automáticamente que exista contenido duplicado en URL distintas. A muchos programadores esta capacidad les parece asombrosa. - - -Componentes interactivos -======================== - -Tenemos que contarle una cosa más sobre los presenters: llevan incorporado un sistema de componentes. Quien tenga más experiencia recordará algo parecido de Delphi o de ASP.NET Web Forms; React o Vue.js se apoyan en conceptos algo emparentados. En el mundo de los frameworks de PHP, esta es una función completamente única. - -Los componentes son unidades independientes y reutilizables que insertamos en las páginas (es decir, en los presenters). Pueden ser [formularios |forms:in-presenter], [datagrids |https://componette.org/contributte/datagrid/], menús, encuestas: en esencia, cualquier cosa que tenga sentido reutilizar. Podemos crear nuestros propios componentes o aprovechar alguno de la [amplísima oferta |https://componette.org] de componentes de código abierto. - -Los componentes influyen de raíz en la forma de desarrollar aplicaciones. Abren nuevas posibilidades para componer páginas a partir de unidades ya preparadas. Y además tienen algo en común con [Hollywood |components#Estilo Hollywood]. - - -Contenedor DI y configuración -============================= - -El contenedor DI, o factory de objetos, es el corazón de toda la aplicación. - -No se preocupe, no es ninguna caja negra mágica, por mucho que las líneas anteriores lo puedan sugerir. En realidad es una clase PHP bastante prosaica, generada por Nette y guardada en el directorio de caché. Contiene muchos métodos con nombres del estilo `createServiceAbcd()`, cada uno capaz de crear y devolver un objeto concreto. Sí, también hay un método `createServiceApplication__application()` que produce la instancia de `Nette\Application\Application` que necesitábamos en `index.php` para ejecutar la aplicación. Y hay métodos para crear los distintos presenters, y así sucesivamente. - -A los objetos creados por el contenedor DI se los llama, por algún motivo, servicios. - -Lo verdaderamente especial de esta clase es que usted no la programa: lo hace el framework. Genera realmente el código PHP y lo guarda en disco. Usted se limita a dar instrucciones sobre qué objetos debe saber crear el contenedor y cómo exactamente. Esas instrucciones se escriben en [archivos de configuración |bootstrapping#Configuración del contenedor DI], que usan el formato [NEON|neon:format] y, por tanto, tienen la extensión `.neon`. - -Los archivos de configuración sirven puramente para instruir al contenedor DI. Así, por ejemplo, si indica la opción `expiration: 14 days` en la sección [session |http:configuration#Sesión], el contenedor DI, al crear el objeto `Nette\Http\Session` que representa la sesión, llamará a su método `setExpiration('14 days')` y convertirá así la configuración en realidad. - -Tiene preparado todo un capítulo que describe qué se puede [configurar |nette:configuring] y cómo [definir sus propios servicios |dependency-injection:services]. - -En cuanto profundice un poco en la creación de servicios, se encontrará con el término [autowiring |dependency-injection:autowiring]. Es una función que le simplificará la vida de forma increíble. Sabe pasar automáticamente los objetos allí donde los necesita (por ejemplo, en los constructores de sus clases) sin que usted tenga que hacer nada. Descubrirá que el contenedor DI de Nette es un pequeño milagro. - - -¿Y ahora qué? -============= - -Hemos repasado los principios fundamentales de las aplicaciones de Nette. Ha sido, de momento, una visión superficial, pero pronto profundizará y, con el tiempo, creará aplicaciones web estupendas. ¿Adónde ir ahora? ¿Ha probado ya el tutorial [Cree su primera aplicación|quickstart:]? - -Además de lo descrito arriba, Nette ofrece todo un arsenal de [clases útiles|utils:], una [capa de base de datos|database:], etc. Pruebe a navegar por la documentación. O visite el [blog|https://blog.nette.org]. Descubrirá muchas cosas interesantes. - -Que el framework le traiga mucha alegría 💙 diff --git a/application/es/multiplier.texy b/application/es/multiplier.texy deleted file mode 100644 index c121342b6a..0000000000 --- a/application/es/multiplier.texy +++ /dev/null @@ -1,63 +0,0 @@ -Multiplier: componentes dinámicos -********************************* - -.[perex] -Una herramienta para crear dinámicamente componentes interactivos. - -Empecemos por un ejemplo típico: imagine una lista de productos en una tienda en línea donde quiere un formulario "Añadir al carrito" para cada artículo. Un enfoque posible es envolver todo el listado en un único formulario. Un método mucho más cómodo, sin embargo, lo ofrece [api:Nette\Application\UI\Multiplier]. - -Multiplier le permite definir cómodamente una factory para varios componentes. Funciona según el principio de los componentes anidados: cualquier componente que herede de [api:Nette\ComponentModel\Container] puede contener otros componentes. - -.[tip] -Vea el capítulo sobre el [modelo de componentes |components#Los componentes en profundidad] de la documentación. - -La esencia de Multiplier es que actúa como un padre capaz de crear dinámicamente a sus hijos mediante un callback pasado al constructor. Vea el ejemplo: - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function () { - $form = new Nette\Application\UI\Form; - $form->addInteger('amount', 'Amount:') - ->setRequired(); - $form->addSubmit('send', 'Add to cart'); - return $form; - }); -} -``` - -Ahora, en la plantilla, podemos renderizar sin más el formulario de cada producto, y cada uno será realmente un componente único. - -```latte -{foreach $items as $item} -

    {$item->title}

    - {$item->description} - - {control "shopForm-$item->id"} -{/foreach} -``` - -El argumento pasado en la etiqueta `{control}` sigue un formato que significa: - -1. Obtener el componente `shopForm`. -2. De él, obtener el hijo llamado `$item->id`. - -Durante la primera llamada del punto **1**, el componente `shopForm` aún no existe, así que se llama a su factory `createComponentShopForm`. Después, sobre el componente obtenido (una instancia de Multiplier) se llama a la factory del formulario concreto, que es la función anónima que pasamos al constructor de Multiplier. - -En la siguiente iteración del bucle foreach ya no se llamará al método `createComponentShopForm` (porque el componente ya existe). Pero como buscamos un hijo distinto (ya que `$item->id` será distinto en cada iteración), la función anónima se llamará de nuevo y devolverá un formulario nuevo. - -Solo queda asegurarse de que el formulario añada al carrito el producto correcto: por ahora, el formulario es idéntico para todos los productos. Aquí nos ayuda una característica de Multiplier (y, en general, de cualquier factory de componentes en Nette Framework): toda factory recibe como primer argumento el nombre del componente que se está creando. Además, una factory de Multiplier recibe como segundo argumento la propia instancia de Multiplier. En nuestro caso, el primer argumento será `$item->id`, que es justo la información que necesitamos. Así que basta con modificar ligeramente la creación del formulario: - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function (string $itemId) { - $form = new Nette\Application\UI\Form; - $form->addInteger('amount', 'Amount:') - ->setRequired(); - $form->addHidden('itemId', $itemId); - $form->addSubmit('send', 'Add to cart'); - return $form; - }); -} -``` diff --git a/application/es/presenters.texy b/application/es/presenters.texy deleted file mode 100644 index da45ae7a80..0000000000 --- a/application/es/presenters.texy +++ /dev/null @@ -1,595 +0,0 @@ -Presenters -********** - -
    - -Veremos cómo se escriben en Nette los presenters y las plantillas. Después de leerlo entenderá: - -- cómo funciona un presenter -- qué son los parámetros persistentes -- cómo se renderizan las plantillas - -
    - -[Ya sabemos |how-it-works#Nette Application] que un presenter es una clase que representa una página concreta de la aplicación web, por ejemplo la portada, un producto de una tienda online, un formulario de acceso, un feed sitemap, etc. Una aplicación puede tener desde un presenter hasta miles. En otros frameworks se les llama también controladores. - -Normalmente con el término presenter nos referimos a un descendiente de la clase [api:Nette\Application\UI\Presenter], que es adecuado para generar interfaces web y al que se dedicará el resto de este capítulo. En sentido general, un presenter es cualquier objeto que implemente la interfaz [api:Nette\Application\IPresenter]. - - -Ciclo de vida del presenter -=========================== - -La tarea del presenter es procesar una petición y devolver una respuesta (que puede ser una página HTML, una imagen, una redirección, etc.). - -Así pues, al principio se le pasa una petición. No es directamente la petición HTTP, sino un objeto [api:Nette\Application\Request], en el que se transformó la petición HTTP con ayuda del router. Normalmente no trabajamos directamente con este objeto, porque el presenter delega ingeniosamente el procesamiento de la petición en otros métodos, que veremos ahora. - -[* lifecycle.svg *] *** Ciclo de vida del presenter .<> - -El diagrama muestra la lista de métodos que se llaman sucesivamente de arriba abajo, si es que existen. Ninguno es obligatorio; puede tener un presenter completamente vacío, sin un solo método, y construir sobre él un sitio web estático sencillo. - - -`__construct()` ---------------- - -El constructor no pertenece del todo al ciclo de vida del presenter, porque se llama en el momento de crear el objeto. Pero lo mencionamos por su importancia. El constructor (junto con el [método inject|best-practices:inject-method-attribute]) sirve para pasar las dependencias. - -El presenter no debería ocuparse de la lógica de negocio de la aplicación, escribir o leer en la base de datos, hacer cálculos, etc. De eso se encargan las clases de la capa que llamamos modelo. Por ejemplo, la clase `ArticleRepository` puede encargarse de cargar y guardar los artículos. Para que el presenter pueda trabajar con ella, hay que [pasársela mediante inyección de dependencias |dependency-injection:passing-dependencies]: - - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articles, - ) { - } -} -``` - - -`startup()` ------------ - -Inmediatamente después de recibir la petición se invoca el método `startup()`. Puede usarlo para inicializar propiedades, comprobar los permisos del usuario, etc. Es obligatorio que este método llame siempre a su antecesor: `parent::startup()`. - - -`action(args...)` .{toc: action()} --------------------------------------------------- - -Parecido al método `render()`. Mientras que `render()` está pensado para preparar los datos de una plantilla concreta que después se renderizará, `action()` procesa la petición sin que necesariamente se renderice después una plantilla. Por ejemplo, puede procesar datos, conectar o desconectar al usuario, etc., y luego [redirigir a otro sitio |#Redirección]. - -Es importante que `action()` se llame *antes* que `render()`. Eso nos permite cambiar el curso de la petición dentro del método de acción, por ejemplo cambiando la plantilla que se renderizará o incluso el método `render()` que se llamará, mediante `setView('otherView')`. - -.{data-version:3.2.3} -Incluso puede cambiar a una acción completamente distinta con el método `switch('otherAction')`. Aborta el método actual y ejecuta en su lugar los métodos `action()` y `render()` de la nueva acción (y desactiva la [canonización|#Canonización] automática). La propia petición continúa; solo se interrumpe el método que se estaba ejecutando. - -A este método se le pasan los parámetros de la petición. Es posible y recomendable indicar los tipos de estos parámetros, p. ej. `actionShow(int $id, ?string $slug = null)`. Si falta el parámetro `id` o no es un número entero, el presenter devuelve un [error 404 |#Error 404 y otros] y termina. - - -`handle(args...)` .{toc: handle()} --------------------------------------------------- - -Este método procesa las llamadas señales, que conoceremos en el capítulo dedicado a los [componentes |components#Señal]. Está pensado sobre todo para los componentes y el procesamiento de las peticiones AJAX. - -A este método se le pasan los parámetros de la petición, igual que en `action()`, incluida la comprobación de tipos. - - -`beforeRender()` ----------------- - -El método `beforeRender`, como su nombre indica, se llama antes de cada método `render()`. Sirve para la configuración común de la plantilla, para pasar variables al layout y cosas parecidas. - - -`render(args...)` .{toc: render()} ----------------------------------------------- - -Aquí es donde preparamos la plantilla para su posterior renderizado, le pasamos los datos, etc. - -A este método se le pasan los parámetros de la petición, igual que en `action()`, incluida la comprobación de tipos. - -```php -public function renderShow(int $id): void -{ - // obtiene los datos del modelo y se los pasa a la plantilla - $this->template->article = $this->articles->getById($id); -} -``` - - -`afterRender()` ---------------- - -El método `afterRender`, de nuevo como su nombre indica, se llama después de cada método `render()`. Se usa más bien poco. - - -`shutdown()` ------------- - -Se llama al final del ciclo de vida del presenter. - - -Eventos -------- - -Además de los métodos `startup()`, `beforeRender()` y `shutdown()`, que se llaman como parte del ciclo de vida del presenter, se pueden definir otras funciones que se llamen automáticamente. El presenter define los llamados [eventos |nette:glossary#Eventos], y sus manejadores se añaden a los arrays `$onStartup`, `$onRender` y `$onShutdown`. - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct() - { - $this->onStartup[] = function () { - // ... - }; - } -} -``` - -Los manejadores del array `$onStartup` se llaman justo antes del método `startup()`, los de `$onRender` entre `beforeRender()` y `render()`, y por último los de `$onShutdown` justo antes de `shutdown()`. - - -**Un consejo antes de continuar:** como ve, un presenter puede gestionar varias acciones/vistas, es decir, tener varios métodos `render()`. Pero recomendamos diseñar los presenters con una sola acción o con el menor número posible de ellas. - - -Envío de una respuesta -====================== - -La respuesta del presenter suele ser el [renderizado de una plantilla en una página HTML|templates], pero también puede ser el envío de un archivo, de JSON o incluso una redirección a otra página. - -En cualquier momento del ciclo de vida podemos usar alguno de los siguientes métodos para enviar una respuesta y terminar al mismo tiempo el presenter: - -- `redirect()`, `redirectPermanent()`, `redirectUrl()` y `forward()` realizan una [redirección |#Redirección] -- `error()` termina el presenter [por un error |#Error 404 y otros] -- `sendJson($data)` termina el presenter y [envía los datos |#Envío de JSON] en formato JSON -- `sendTemplate()` termina el presenter y [renderiza inmediatamente la plantilla |templates] -- `sendResponse($response)` termina el presenter y envía una [respuesta propia |#Respuestas] -- `terminate()` termina el presenter sin respuesta - -Cada uno de estos métodos termina inmediatamente el presenter lanzando la excepción de terminación silenciosa `Nette\Application\AbortException`. - -Si no llama a ninguno de estos métodos, el presenter pasa automáticamente a renderizar la plantilla. ¿Por qué? Porque en el 99 % de los casos queremos renderizar una plantilla, así que el presenter adopta este comportamiento como predeterminado para facilitarnos el trabajo. - - -Creación de enlaces -=================== - -El presenter tiene el método `link()`, con el que se crean enlaces URL a otros presenters. El primer parámetro es el presenter y la acción de destino, seguido de los argumentos, que se pueden pasar como array: - -```php -$url = $this->link('Product:show', $id); - -$url = $this->link('Product:show', [$id, 'lang' => 'en']); -``` - -En la plantilla, los enlaces a otros presenters y acciones se crean así: - -```latte -product detail -``` - -Simplemente escriba en lugar de la URL real el conocido par `Presenter:action` e indique los parámetros que hagan falta. El truco está en `n:href`, que le dice a Latte que procese este atributo y genere la URL real. En Nette no tiene que pensar en absoluto en las URL, solo en los presenters y las acciones. - -Encontrará más información en el capítulo [Creación de enlaces URL|creating-links]. - - -Redirección -=========== - -Para pasar a otro presenter sirven los métodos `redirect()` y `forward()`, que tienen una sintaxis muy parecida a la del método [link() |#Creación de enlaces]. - -El método `forward()` pasa al nuevo presenter inmediatamente, sin redirección HTTP: - -```php -$this->forward('Product:show'); -``` - -Ejemplo de redirección temporal con el código HTTP 302 (o 303, si el método de la petición actual es POST): - -```php -$this->redirect('Product:show', $id); -``` - -Para conseguir una redirección permanente con el código HTTP 301, use esto: - -```php -$this->redirectPermanent('Product:show', $id); -``` - -A otra URL fuera de la aplicación puede redirigir con el método `redirectUrl()`. El código HTTP se puede indicar como segundo parámetro; el predeterminado es 302 (o 303, si el método de la petición actual es POST): - -```php -$this->redirectUrl('https://nette.org'); -``` - -La redirección termina inmediatamente la actividad del presenter lanzando la llamada excepción de terminación silenciosa `Nette\Application\AbortException`. - -Antes de la redirección se pueden enviar [#Mensajes flash], es decir, mensajes que se mostrarán en la plantilla después de redirigir. - - -Mensajes flash -============== - -Son mensajes que suelen informar del resultado de alguna operación. Una propiedad importante de los mensajes flash es que siguen disponibles en la plantilla incluso después de una redirección. Una vez mostrados siguen activos otros 30 segundos: por ejemplo, si el usuario recarga la página debido a un error de transmisión, el mensaje no desaparecerá de inmediato. - -Basta con llamar al método [flashMessage() |api:Nette\Application\UI\Control::flashMessage()] y el presenter se encarga de pasarlo a la plantilla. El primer parámetro es el texto del mensaje y el segundo, opcional, es su tipo (p. ej. error, warning, info). El método `flashMessage()` devuelve una instancia del mensaje flash, a la que se le puede añadir más información. - -```php -$this->flashMessage('The item has been deleted.'); -$this->redirect(/* ... */); // y redirige -``` - -En la plantilla, estos mensajes están disponibles en la variable `$flashes` como objetos `stdClass` que contienen las propiedades `message` (el texto del mensaje), `type` (el tipo del mensaje) y, eventualmente, la información añadida por el usuario ya mencionada. Los renderizamos así: - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -Error 404 y otros -================= - -Si no podemos atender la petición, por ejemplo porque el artículo que queremos mostrar no existe en la base de datos, lanzamos un error 404 con el método `error(string $message = '', int $httpCode = 404)`. - -```php -public function renderShow(int $id): void -{ - $article = $this->articles->getById($id); - if (!$article) { - $this->error(); - } - // ... -} -``` - -El código HTTP del error se puede pasar como segundo parámetro; el predeterminado es 404. El método funciona lanzando la excepción `Nette\Application\BadRequestException`, tras la cual `Application` pasa el control al presenter de error. Es un presenter cuya tarea es mostrar una página que informe del error ocurrido. El presenter de error se establece en la [configuración de la aplicación|configuration]. - - -Envío de JSON -============= - -El método `sendJson($data)` codifica los datos indicados en JSON, los envía como respuesta HTTP y termina el presenter. Ejemplo: - -```php -public function actionData(): void -{ - $data = ['hello' => 'nette']; - $this->sendJson($data); -} -``` - - -Parámetros de la petición .{data-version:3.1.14} -================================================ - -El presenter, y también cada componente, obtiene sus parámetros de la petición HTTP. Puede consultar sus valores con los métodos `getParameter($name)` o `getParameters()`. Los valores son cadenas o arrays de cadenas, en esencia datos en bruto obtenidos directamente de la URL. - -Para mayor comodidad recomendamos acceder a los parámetros mediante propiedades. Basta con marcarlas con el atributo `#[Parameter]`: - -```php -use Nette\Application\Attributes\Parameter; // esta línea es importante - -class HomePresenter extends Nette\Application\UI\Presenter -{ - #[Parameter] - public string $theme; // debe ser public -} -``` - -Para la propiedad recomendamos indicar el tipo de dato (p. ej. `string`) y Nette convertirá el valor automáticamente. Los valores de los parámetros también se pueden [validar |#Validación de los parámetros]. - -Al crear un enlace puede establecer directamente el valor del parámetro: - -```latte -click -``` - - -Parámetros persistentes -======================= - -Los parámetros persistentes sirven para mantener el estado entre distintas peticiones. Su valor sigue siendo el mismo incluso después de pulsar un enlace. A diferencia de los datos de la sesión, se transfieren en la URL. Y esto ocurre de forma completamente automática, así que no hace falta indicarlos explícitamente en `link()` ni en `n:href`. - -¿Un ejemplo de uso? Imagine que tiene una aplicación multilingüe. El idioma actual es un parámetro que debe formar parte siempre de la URL. Pero sería increíblemente tedioso indicarlo en cada enlace. Así que lo convierte en el parámetro persistente `lang` y se irá arrastrando solo. ¡Genial! - -Crear un parámetro persistente en Nette es facilísimo. Basta con crear una propiedad pública y marcarla con el atributo: (antes se usaba `/** @persistent */`) - -```php -use Nette\Application\Attributes\Persistent; // esta línea es importante - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; // debe ser public -} -``` - -Si `$this->lang` tiene, por ejemplo, el valor `'en'`, los enlaces creados con `link()` o `n:href` contendrán también el parámetro `lang=en`. Y después de pulsar el enlace, `$this->lang` volverá a ser `'en'`. - -Para la propiedad recomendamos indicar el tipo de dato (p. ej. `string`) y también puede indicar un valor por defecto. Los valores de los parámetros se pueden [validar |#Validación de los parámetros]. - -Los parámetros persistentes se transfieren normalmente entre todas las acciones de un presenter dado. Para transferirlos también entre varios presenters hay que definirlos: - -- en un antecesor común del que hereden los presenters -- o en un trait que usen los presenters: - -```php -trait LanguageAware -{ - #[Persistent] - public string $lang; -} - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - use LanguageAware; -} -``` - -Al crear un enlace se puede cambiar el valor de un parámetro persistente: - -```latte -detail in Czech -``` - -O se puede *resetear*, es decir, eliminar de la URL. Entonces adoptará su valor por defecto: - -```latte -click -``` - - -Espacio común de parámetros -=========================== - -Los parámetros de la petición, los [parámetros persistentes |#Parámetros persistentes] y los parámetros de los métodos `action`, `render` y `handle` (señal) comparten un único espacio, en el que cada uno se identifica por su nombre. Si el mismo nombre aparece en varios de ellos, se refieren a un único y mismo valor. - -Esto se aprovecha a menudo. Por ejemplo, el parámetro persistente `lang` y el argumento `$lang` de un método de acción o de señal son una y la misma cosa: puede leer el valor actual de un parámetro persistente con solo indicarlo en la firma del método: - -```php -#[Persistent] -public string $lang; - -public function handleSearch(string $query, string $lang): void -{ - // $lang contiene el valor actual del parámetro persistente lang -} -``` - -Como este espacio es compartido, mantenga los nombres de los parámetros únicos, salvo que quiera deliberadamente que compartan un valor. Esto vale también para las señales, que además leen parámetros del cuerpo POST de la petición, véase [Las señales en profundidad |components#Las señales en profundidad]. - - -Componentes interactivos -======================== - -Los presenters llevan incorporado un sistema de componentes. Los componentes son unidades independientes y reutilizables que insertamos en los presenters. Pueden ser [formularios |forms:in-presenter], datagrids, menús, en definitiva cualquier cosa que tenga sentido usar repetidamente. - -¿Cómo se insertan los componentes en los presenters y cómo se usan después? Lo aprenderá en el capítulo [Componentes |components]. Descubrirá incluso qué tienen en común con Hollywood. - -¿Y dónde puedo conseguir componentes? En [Componette |https://componette.org/search/component] encontrará componentes de código abierto y muchos otros complementos para Nette, aportados por voluntarios de la comunidad del framework. - - -Profundizando -============= - -.[tip] -Lo que hemos visto hasta ahora en este capítulo bastará probablemente para la mayoría de los usos. Las secciones siguientes están pensadas para quien quiera profundizar en los presenters y saberlo absolutamente todo. - - -Validación de los parámetros ----------------------------- - -Los valores de los [#Parámetros de la petición] y de los [#Parámetros persistentes] recibidos de las URL los escribe en las propiedades el método `loadState()`. Este comprueba también si coinciden con el tipo de dato indicado en la propiedad; en caso contrario responderá con un error 404 y la página no se mostrará. - -Nunca confíe ciegamente en los parámetros recibidos de la URL, porque el usuario puede sobrescribirlos fácilmente. Así, por ejemplo, comprobaríamos si el idioma `$this->lang` está entre los admitidos. Una forma adecuada de hacerlo es sobrescribir el mencionado método `loadState()`: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; - - public function loadState(array $params): void - { - parent::loadState($params); // aquí se establece $this->lang - // sigue la comprobación propia del valor: - if (!in_array($this->lang, ['en', 'cs'])) { - $this->error(); - } - } -} -``` - - -Guardar y restaurar la petición -------------------------------- - -La petición que procesa el presenter es un objeto [api:Nette\Application\Request], que devuelve el método `getRequest()` del presenter. - -La petición actual se puede guardar en la sesión o, al revés, restaurarla desde ella y hacer que el presenter la ejecute otra vez. Esto resulta útil, por ejemplo, cuando el usuario está rellenando un formulario y su sesión de acceso caduca. Para no perder los datos, antes de redirigir a la página de acceso guardamos la petición actual en la sesión con `$reqId = $this->storeRequest()`. Esto devuelve su identificador en forma de cadena corta, que luego pasamos como parámetro al presenter de acceso. - -Tras el acceso llamamos al método `$this->restoreRequest($reqId)`, que recupera la petición de la sesión. Las peticiones POST se le reenvían, mientras que las demás (GET) se redirigen a la URL de la petición. El método comprueba que la petición la creó el mismo usuario que ahora está conectado. Si se conecta otro usuario o la clave no es válida, no hace nada y el programa continúa con normalidad. - -Véase la guía [Cómo volver a una página anterior |best-practices:restore-request]. - - -Canonización ------------- - -Los presenters tienen una característica realmente excelente que contribuye a un mejor SEO (Search Engine Optimization). Impiden automáticamente que exista contenido duplicado en URL distintas. Si a un destino concreto llevan varias URL, p. ej. `/index` y `/index?page=1`, el framework designa una de ellas como principal (canónica) y redirige las demás a ella con el código HTTP 301. Gracias a eso, los buscadores no indexan sus páginas dos veces ni diluyen su page rank. - -Este proceso se llama canonización. La URL canónica es la que genera el [router|routing], normalmente la primera ruta coincidente de la colección. - -La canonización está activada de forma predeterminada y se puede desactivar con `$this->autoCanonicalize = false`. - -La redirección no se produce en las peticiones AJAX ni POST, porque podría provocar la pérdida de datos o no aportaría ningún valor SEO añadido. - -También puede provocar la canonización manualmente con el método `canonicalize()`. Igual que al método `link()`, se le pasan el presenter, la acción y los parámetros. Genera un enlace y lo compara con la dirección URL actual. Si difieren, redirige al enlace generado. - -```php -public function actionShow(int $id, ?string $slug = null): void -{ - $realSlug = $this->facade->getSlugForId($id); - // redirige si $slug es distinto de $realSlug - $this->canonicalize('Product:show', [$id, $realSlug]); -} -``` - -Para ver un patrón completo que combina los filtros de ruta con `canonicalize()` para obtener URL amigables para el SEO, consulte [URLs amigables con slugs |best-practices:pretty-urls]. - - -Respuestas ----------- - -La respuesta que devuelve el presenter es un objeto que implementa la interfaz [api:Nette\Application\Response]. Hay disponibles varias respuestas ya preparadas: - -- [api:Nette\Application\Responses\CallbackResponse] - envía un callback -- [api:Nette\Application\Responses\FileResponse] - envía un archivo -- [api:Nette\Application\Responses\ForwardResponse] - forward() -- [api:Nette\Application\Responses\JsonResponse] - envía JSON -- [api:Nette\Application\Responses\RedirectResponse] - redirección -- [api:Nette\Application\Responses\TextResponse] - envía texto -- [api:Nette\Application\Responses\VoidResponse] - respuesta vacía - -Las respuestas se envían con el método `sendResponse()`: - -```php -use Nette\Application\Responses; - -// Texto plano -$this->sendResponse(new Responses\TextResponse('Hello Nette!')); - -// Envía un archivo -$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf')); - -// Envía un callback -$callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) { - if ($httpResponse->getHeader('Content-Type') === 'text/html') { - echo '

    Hello

    '; - } -}; -$this->sendResponse(new Responses\CallbackResponse($callback)); -``` - -También puede escribir su propia respuesta. Basta con implementar la interfaz `Nette\Application\Response`, que tiene un único método `send()` que recibe la petición y la respuesta HTTP. Esto es útil, por ejemplo, al transmitir datos que no quiere mantener en memoria: - -```php -class CsvResponse implements Nette\Application\Response -{ - public function __construct( - private string $fileName, - private iterable $rows, - ) { - } - - public function send(Nette\Http\IRequest $request, Nette\Http\IResponse $response): void - { - $response->setContentType('text/csv', 'utf-8'); - $response->sendAsFile($this->fileName); - - $handle = fopen('php://output', 'w'); - foreach ($this->rows as $row) { - fputcsv($handle, $row); - } - - fclose($handle); - } -} -``` - -Después la envía en el presenter como de costumbre: `$this->sendResponse(new CsvResponse('export.csv', $rows));` - - -Caché HTTP ----------- - -El método `lastModified()` permite aprovechar fácilmente la caché HTTP. Se le pasa la fecha y hora de la última modificación del contenido (como timestamp, cadena u objeto `DateTimeInterface`) y, opcionalmente, un validador ETag (una cadena corta que identifica la versión actual del contenido, por ejemplo su hash) y un tiempo de expiración. Si el navegador ya tiene una versión coincidente, el presenter envía una respuesta `304 Not Modified` y termina, con lo que la página no se renderiza ni se transfiere innecesariamente: - -```php -public function renderArticle(int $id): void -{ - $article = $this->articles->getById($id); - $this->lastModified($article->updatedAt); - // ... -} -``` - - -Completar la plantilla .{data-version:3.3.0} --------------------------------------------- - -Cuando el presenter renderiza la plantilla, el método `sendTemplate()` llama a `completeTemplate()` justo antes del renderizado. Este método rellena las variables marcadas con el atributo `#[TemplateVariable]` y localiza el archivo de la plantilla (las variables predeterminadas ya las establece `TemplateFactory` al crear la plantilla). Puede sobrescribir este método protegido para añadir variables compartidas por todas las vistas o para establecer otro archivo: - -```php -protected function completeTemplate(Nette\Application\UI\Template $template): void -{ - parent::completeTemplate($template); - $template->siteName = 'My App'; -} -``` - - -Restricción de acceso con `#[Requires]` .{data-version:3.2.3} -------------------------------------------------------------- - -El atributo `#[Requires]` ofrece opciones avanzadas para restringir el acceso a los presenters y a sus métodos. Sirve para indicar métodos HTTP, exigir una petición AJAX, limitarlo al mismo origen y permitir el acceso solo mediante forward. El atributo se puede aplicar tanto a las clases de los presenters como a métodos concretos como `action()`, `render()`, `handle()` y `createComponent()`. - -Puede indicar estas restricciones: -- sobre los métodos HTTP: `#[Requires(methods: ['GET', 'POST'])]` -- exigiendo una petición AJAX: `#[Requires(ajax: true)]` -- acceso solo desde el mismo origen: `#[Requires(sameOrigin: true)]` -- acceso solo mediante forward: `#[Requires(forward: true)]` -- restricciones para acciones concretas: `#[Requires(actions: 'default')]` - -.[note] -Desde la versión 3.3, la coincidencia de origen se comprueba mediante la cabecera `Sec-Fetch-Site` del navegador (antes mediante una cookie SameSite), lo que es más fiable y comprueba la coincidencia exacta de esquema, dominio y puerto. - -Los detalles los encontrará en la guía [Cómo usar el atributo Requires |best-practices:attribute-requires]. - - -Comprobación del método HTTP ----------------------------- - -Los presenters en Nette comprueban automáticamente el método HTTP de cada petición entrante, sobre todo por motivos de seguridad. De forma predeterminada se permiten los métodos `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH`. - -Si quiere permitir además, por ejemplo, el método `OPTIONS`, use el atributo `#[Requires]` (desde Nette Application v3.2.3): - -```php -#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])] -class MyPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Desde la versión 3.1.13, la comprobación se realiza en `checkHttpMethod()`, que verifica si el método indicado en la petición está incluido en el array `$presenter->allowedMethods`. Desde la versión 3.2.3, este enfoque está obsoleto en favor de `#[Requires]`. El método se puede sobrescribir así: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - protected function checkHttpMethod(): void - { - $this->allowedMethods[] = 'OPTIONS'; - parent::checkHttpMethod(); - } -} -``` - -Es importante subrayar que, si habilita el método `OPTIONS`, debe después gestionarlo adecuadamente dentro de su presenter. Este método se usa a menudo como la llamada petición preflight, que el navegador envía automáticamente antes de la petición propiamente dicha cuando hay que averiguar si la petición es admisible según la política CORS (Cross-Origin Resource Sharing). Si habilita el método pero no implementa la respuesta correcta, puede provocar inconsistencias y posibles problemas de seguridad. - - -Marcar acciones obsoletas .{data-version:3.2.3} ------------------------------------------------ - -El atributo `#[Deprecated]` marca acciones, señales o presenters enteros como obsoletos y destinados a desaparecer en el futuro. Al generar enlaces a partes obsoletas de la aplicación, Nette lanza una advertencia para avisar a los desarrolladores. - -El atributo se puede aplicar tanto a toda la clase del presenter como a métodos concretos `action()`, `render()` y `handle()`. - - -Lecturas adicionales -==================== - -- [Métodos y atributos inject |best-practices:inject-method-attribute] -- [Componer presenters a partir de traits |best-practices:presenter-traits] -- [Pasar ajustes a los presenters |best-practices:passing-settings-to-presenters] -- [Cómo volver a una página anterior |best-practices:restore-request] diff --git a/application/es/routing.texy b/application/es/routing.texy deleted file mode 100644 index 7aa9ce9fb1..0000000000 --- a/application/es/routing.texy +++ /dev/null @@ -1,749 +0,0 @@ -Enrutamiento -************ - -
    - -El router se ocupa de todo lo relacionado con las direcciones URL, de modo que usted no tenga que pensar en ellas. Le mostraremos: - -- cómo configurar el router para que las URL tengan el aspecto que desea -- hablaremos del SEO y de las redirecciones -- y le enseñaremos cómo escribir un router propio - -
    - - -Las URL más amables para las personas (también llamadas cool o pretty URLs) son más utilizables, se recuerdan mejor y contribuyen positivamente al SEO. Nette lo tiene en cuenta y satisface plenamente las necesidades de los desarrolladores. Puede diseñar para su aplicación exactamente la estructura de URL que quiera. Incluso puede diseñarla cuando la aplicación ya está terminada, porque no requiere ningún cambio en el código ni en las plantillas. Se define de forma elegante en [un único lugar |#Integración], el router, en vez de andar dispersa como anotaciones por todos los presenters. - -El router en Nette es excepcional porque es **bidireccional.** Sabe tanto descodificar las URL de las peticiones HTTP como crear enlaces. Por eso desempeña un papel clave en [Nette Application |how-it-works#Nette Application], ya que no solo decide qué presenter y qué acción ejecutarán la petición actual, sino que se usa también para [generar las URL |creating-links] en las plantillas, etc. - -El router no se limita, sin embargo, a este uso; puede utilizarlo en aplicaciones donde no se usan presenters en absoluto, para API REST, etc. Más detalles en la sección [#Uso independiente]. - - -Colección de rutas -================== - -La forma más agradable de definir la estructura de las direcciones URL de una aplicación la ofrece la clase [api:Nette\Application\Routers\RouteList]. La definición consiste en una lista de las llamadas rutas, es decir, máscaras de direcciones URL y los presenters y acciones asociados, mediante una API sencilla. No hace falta nombrar las rutas de ninguna manera. - -```php -$router = new Nette\Application\Routers\RouteList; -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('article/', 'Article:view'); -// ... -``` - -El ejemplo muestra que, si abrimos en el navegador `https://domain.com/rss.xml`, se mostrará el presenter `Feed` con la acción `rss`. Si abrimos `https://domain.com/article/12`, se mostrará el presenter `Article` con la acción `view`, etc. Si no se encuentra ninguna ruta adecuada, Nette Application reacciona lanzando la excepción [BadRequestException |api:Nette\Application\BadRequestException], que se muestra al usuario como una página de error 404 Not Found. - - -Orden de las rutas ------------------- - -El **orden** en el que se indican las distintas rutas es absolutamente **crucial**, porque se evalúan sucesivamente de arriba abajo. La regla es que declaramos las rutas **de las específicas a las generales**: - -```php -// MAL: 'rss.xml' lo captura la primera ruta y entiende esa cadena como -$router->addRoute('', 'Article:view'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// BIEN -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('', 'Article:view'); -``` - -Las rutas se evalúan de arriba abajo también al generar los enlaces: - -```php -// MAL: el enlace a 'Feed:rss' se genera como 'admin/feed/rss' -$router->addRoute('admin//', 'Admin:default'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// BIEN -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('admin//', 'Admin:default'); -``` - -No le vamos a ocultar que montar las rutas correctamente requiere cierta destreza. Hasta que le coja el truco, el [panel de enrutamiento |#Depuración del router] le será de gran ayuda. - - -Máscara y parámetros --------------------- - -La máscara describe la ruta relativa desde el directorio raíz del sitio web. La máscara más sencilla es una URL estática: - -```php -$router->addRoute('products', 'Products:default'); -``` - -A menudo las máscaras contienen los llamados **parámetros**. Se escriben entre corchetes angulares (p. ej. ``) y se pasan al presenter de destino, por ejemplo al método `renderShow(int $year)` o al parámetro persistente `$year`: - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -El ejemplo muestra que, si abrimos en el navegador `https://example.com/chronicle/2020`, se mostrará el presenter `History` con la acción `show` y el parámetro `year: 2020`. - -A los parámetros les podemos indicar un valor por defecto directamente en la máscara, con lo que se vuelven opcionales: - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -La ruta aceptará ahora también la URL `https://example.com/chronicle/`, que mostrará de nuevo `History:show` con el parámetro `year: 2020`. - -Naturalmente, el nombre del presenter y de la acción también pueden ser parámetros. Por ejemplo: - -```php -$router->addRoute('/', 'Home:default'); -``` - -La ruta indicada acepta, por ejemplo, URL con la forma `/article/edit` o `/catalog/list` y las entiende como los presenters y acciones `Article:edit` y `Catalog:list`, respectivamente. - -Al mismo tiempo da a los parámetros `presenter` y `action` los valores por defecto `Home` y `default`, con lo que también son opcionales. Así, la ruta acepta también una URL como `/article` y la entiende como `Article:default`. O al revés: un enlace a `Product:default` genera la ruta `/product` y un enlace al predeterminado `Home:default` genera la ruta `/`. - -La máscara puede describir no solo la ruta relativa desde el directorio raíz del sitio web, sino también una ruta absoluta si empieza por una barra, o incluso la URL absoluta entera si empieza por dos barras: - -```php -// relativa al document root -$router->addRoute('/', /* ... */); - -// ruta absoluta (relativa al dominio) -$router->addRoute('//', /* ... */); - -// URL absoluta incluido el dominio (relativa al esquema) -$router->addRoute('//.example.com//', /* ... */); - -// URL absoluta incluido el esquema -$router->addRoute('https://.example.com//', /* ... */); -``` - - -Expresiones de validación -------------------------- - -A cada parámetro se le puede indicar una condición de validación mediante una [expresión regular|https://www.php.net/manual/en/reference.pcre.pattern.syntax.php]. Por ejemplo, al parámetro `id` le indicamos con la expresión regular `\d+` que solo puede contener cifras: - -```php -$router->addRoute('/[/]', /* ... */); -``` - -La expresión regular predeterminada para todos los parámetros es `[^/]+`, es decir, todo menos la barra. Si un parámetro debe aceptar también barras, indicamos la expresión `.+`: - -```php -// acepta https://example.com/a/b/c, path será 'a/b/c' -$router->addRoute('', /* ... */); -``` - - -Secuencias opcionales ---------------------- - -En la máscara se pueden marcar partes opcionales con corchetes. Cualquier parte de la máscara puede ser opcional y puede contener parámetros: - -```php -$router->addRoute('[/]', /* ... */); - -// Acepta las rutas: -// /en/download => lang => en, name => download -// /download => lang => null, name => download -``` - -Cuando un parámetro forma parte de una secuencia opcional, se vuelve naturalmente opcional también. Si no tiene indicado un valor por defecto, será null. - -Las partes opcionales pueden estar también en el dominio: - -```php -$router->addRoute('//[.]example.com//', /* ... */); -``` - -Las secuencias se pueden anidar y combinar libremente: - -```php -$router->addRoute( - '[[-]/][/page-]', - 'Home:default', -); - -// Acepta las rutas: -// /en/hello -// /en-us/hello -// /hello -// /hello/page-12 -``` - -Al generar las URL se busca la variante más corta, así que todo lo que se pueda omitir se omite. Por eso, por ejemplo, la ruta `index[.html]` genera la ruta `/index`. Este comportamiento se puede invertir escribiendo un signo de exclamación después del corchete izquierdo: - -```php -// acepta /hello y /hello.html, genera /hello -$router->addRoute('[.html]', /* ... */); - -// acepta /hello y /hello.html, genera /hello.html -$router->addRoute('[!.html]', /* ... */); -``` - -Los parámetros opcionales (es decir, los que tienen un valor por defecto) sin corchetes se comportan en esencia como si estuvieran encerrados de la siguiente manera: - -```php -$router->addRoute('//', /* ... */); - -// corresponde a esto: -$router->addRoute('[/[/[]]]', /* ... */); -``` - -Si queremos influir en el comportamiento de la barra final, para que se genere por ejemplo `/home` en lugar de `/home/`, se puede conseguir así: - -```php -$router->addRoute('[[/[/]]]', /* ... */); -``` - - -Comodines ---------- - -En la máscara de una URL absoluta podemos usar los siguientes comodines para no tener que escribir en la máscara, por ejemplo, el dominio, que puede diferir entre el entorno de desarrollo y el de producción: - -- `%tld%` = dominio de primer nivel, p. ej. `com` u `org` -- `%sld%` = dominio de segundo nivel, p. ej. `example` -- `%domain%` = dominio sin subdominios, p. ej. `example.com` -- `%host%` = host entero, p. ej. `www.example.com` -- `%basePath%` = ruta al directorio raíz - -```php -$router->addRoute('//www.%domain%/%basePath%//', /* ... */); -$router->addRoute('//www.%sld%.%tld%/%basePath%//', /* ... */); -``` - - -Notación avanzada ------------------ - -El destino de la ruta, que se escribe normalmente con el formato `Presenter:action`, se puede escribir también mediante un array que define los distintos parámetros y sus valores por defecto: - -```php -$router->addRoute('/[/]', [ - 'presenter' => 'Home', - 'action' => 'default', -]); -``` - -Para una especificación más detallada se puede usar una forma aún más extendida, en la que, además de los valores por defecto, podemos 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; - -$router->addRoute('/[/]', [ - 'presenter' => [ - Route::Value => 'Home', - ], - 'action' => [ - Route::Value => 'default', - ], - 'id' => [ - Route::Pattern => '\d+', - ], -]); -``` - -Es importante señalar que, si los parámetros definidos en el array no aparecen en la máscara de la ruta, sus valores no se pueden cambiar, ni siquiera con los parámetros de consulta indicados tras el signo de interrogación en la URL. - -Esto es útil para los **parámetros fijos**: dar a una página concreta una URL corta y fácil de recordar. Por ejemplo, para que `/tos` abra siempre `Article:view` con `id: 123`: - -```php -$router->addRoute('tos', [ - 'presenter' => 'Article', - 'action' => 'view', - 'id' => 123, -]); -``` - - -Filtros y traducciones ----------------------- - -El código fuente de la aplicación lo escribimos en inglés, pero si el sitio web debe tener las URL en checo, un enrutamiento sencillo como: - -```php -$router->addRoute('/', 'Home:default'); -``` - -generará URL en inglés, como `/product/123` o `/cart`. Si queremos que los presenters y las acciones en la URL estén representados por palabras checas (p. ej. `/produkt/123` o `/kosik`), podemos usar un diccionario de traducción. Para escribirlo necesitamos ya la variante "más locuaz" del segundo parámetro: - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterTable => [ - // cadena en la URL => presenter - 'produkt' => 'Product', - 'kosik' => 'Cart', - 'katalog' => 'Catalog', - ], - ], - 'action' => [ - Route::Value => 'default', - Route::FilterTable => [ - 'seznam' => 'list', - ], - ], -]); -``` - -Varias claves del diccionario de traducción pueden llevar al mismo presenter. Así se le crean distintos alias. La última clave se considera la variante canónica (es decir, la que aparecerá en la URL generada). - -La tabla de traducción se puede usar de esta manera para cualquier parámetro. Si la traducción no existe, se toma el valor original. Podemos cambiar este comportamiento añadiendo `Route::FilterStrict => true` y la ruta rechazará entonces la URL si el valor no está en el diccionario. - -Además del diccionario de traducción en forma de array, se pueden emplear funciones de traducción propias. - -```php -use Nette\Routing\Route; - -$router->addRoute('//', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterIn => function (string $s): string { /* ... */ }, - Route::FilterOut => function (string $s): string { /* ... */ }, - ], - 'action' => 'default', - 'id' => null, -]); -``` - -La función `Route::FilterIn` convierte entre el parámetro de la URL y la cadena que se pasa después al presenter; la función `FilterOut` se encarga de la conversión en sentido contrario. - -Los parámetros `presenter`, `action` y `module` ya tienen filtros predefinidos que convierten entre el estilo PascalCase o camelCase y el kebab-case usado en la URL. El valor por defecto de los parámetros se escribe ya en la forma en la que se pasa a la aplicación (PascalCase para presenter y module, camelCase para action), así que, por ejemplo, en el caso del presenter escribimos ``, no ``. - - -Filtros generales ------------------ - -Además de los filtros destinados a parámetros concretos, podemos definir también filtros generales, que reciben un array asociativo de todos los parámetros, que pueden modificar de cualquier manera y devolver después. Los filtros generales se definen bajo la clave vacía. - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => 'Home', - 'action' => 'default', - '' => [ - Route::FilterIn => function (array $params): array { /* ... */ }, - Route::FilterOut => function (array $params): array { /* ... */ }, - ], -]); -``` - -Los filtros generales ofrecen la posibilidad de modificar el comportamiento de la ruta de absolutamente cualquier manera. Podemos usarlos, por ejemplo, para modificar unos parámetros a partir de otros. Por ejemplo, para traducir `` y `` según el valor actual del parámetro ``. - -Si un parámetro tiene definido su propio filtro y existe además un filtro general, el `FilterIn` propio se ejecuta antes que el general y, al revés, el `FilterOut` general se ejecuta antes que el propio. Así, dentro del filtro general los valores de los parámetros `presenter` y `action` están escritos en estilo PascalCase o camelCase, respectivamente. - -Véase [URLs amigables con slugs |best-practices:pretty-urls] para un uso práctico de estos filtros: generar URL amigables para el SEO como `/article/123-how-to-bake-bread` sin modificar ninguna plantilla. - - -Bandera OneWay --------------- - -Las rutas de un solo sentido se usan para mantener la funcionalidad de URL antiguas que la aplicación ya no genera, pero que sigue aceptando. Las marcamos con la bandera `OneWay`: - -```php -// URL antigua /product-info?id=123 -$router->addRoute('product-info', 'Product:detail', oneWay: true); -// URL nueva /product/123 -$router->addRoute('product/', 'Product:detail'); -``` - -Al acceder a la URL antigua, el presenter redirige automáticamente a la nueva, de modo que los buscadores no indexen estas páginas dos veces (véase [#SEO y canonización]). - - -Enrutamiento dinámico con callbacks ------------------------------------ - -El enrutamiento dinámico con callbacks le permite asignar directamente a las rutas funciones (callbacks) que se ejecutan al visitar la ruta dada. Esta funcionalidad flexible le permite crear rápida y eficazmente distintos endpoints para su aplicación: - -```php -$router->addRoute('test', function () { - echo 'You are at the /test address'; -}); -``` - -En la máscara puede definir también parámetros, que se pasan automáticamente a su callback: - -```php -$router->addRoute('', function (string $lang) { - echo match ($lang) { - 'cs' => 'Welcome to the Czech version of our website!', - 'en' => 'Welcome to the English version of our website!', - }; -}); -``` - -Además de los parámetros de la máscara, el callback puede recibir también servicios del contenedor DI. Se pasan según el tipo del parámetro. Además, el parámetro `$presenter` recibe una instancia de [MicroPresenter |api:NetteModule\MicroPresenter], que procesa la ruta: - -```php -$router->addRoute('', function (string $lang, Nette\Http\Request $httpRequest, NetteModule\MicroPresenter $presenter) { - // ... -}); -``` - - -Módulos -------- - -Si tenemos varias rutas que pertenecen a un [módulo |directory-structure#Presenters y plantillas] común, usamos `withModule()`. El módulo indicado se antepone automáticamente al presenter de cada ruta del grupo y desaparece por completo de la URL: - -```php -$router = new RouteList; -$router->withModule('Forum') // las rutas siguientes forman parte del módulo Forum - ->addRoute('rss', 'Feed:rss') // el presenter será Forum:Feed - ->addRoute('/') - - ->withModule('Admin') // las rutas siguientes forman parte del módulo Forum:Admin - ->addRoute('sign:in', 'Sign:in'); -``` - -Una alternativa es el parámetro `module`, que igualmente fija un módulo y lo mantiene fuera de la URL: - -```php -// la URL manage/dashboard/default se mapea al presenter Admin:Dashboard -$router->addRoute('manage//', [ - 'module' => 'Admin', -]); -``` - -El nombre de un presenter está completo solo junto con su módulo, p. ej. `Front:Admin:ProductList`. Siempre que ese nombre completo acaba en un parámetro de la URL, el router lo codifica con dos reglas sencillas: cada dos puntos `:` (el separador de módulos) se convierte en un **punto** y cada límite entre palabras de un nombre en PascalCase se convierte en un **guion**. Así, `Front:Admin:ProductList` se escribe en la URL como `front.admin.product-list` y de la misma manera se descodifica de vuelta. Precisamente por eso una aplicación modular sin ninguna de las herramientas anteriores genera URL llenas de puntos. - -Tanto `withModule()` como el parámetro `module` evitan esto justamente porque recortan el prefijo de módulo conocido del nombre del presenter antes de que llegue a la URL: como el módulo es una constante, no hace falta codificarlo de ninguna manera. - -A veces queremos que el módulo mismo varíe y aparezca en la URL, así que recurrimos a `` directamente en la máscara. Cuidado, sin embargo, con un detalle esencial: **`` absorbe toda la ruta de módulos**, todo hasta los últimos dos puntos del nombre del presenter. En el presenter `Shop:Admin:Product` eso es el módulo `Shop:Admin` y el presenter `Product`, y como los dos puntos se convierten en puntos, obtenemos: - - -Subdominios ------------ - -Las colecciones de rutas se pueden dividir según los subdominios: - -```php -$router = new RouteList; -$router->withDomain('example.com') - ->addRoute('rss', 'Feed:rss') - ->addRoute('/'); -``` - -En el nombre del dominio se pueden usar también [#Comodines]: - -```php -$router = new RouteList; -$router->withDomain('example.%tld%') - // ... -``` - - -Prefijo de ruta ---------------- - -Las colecciones de rutas se pueden dividir según la ruta de la URL: - -```php -$router = new RouteList; -$router->withPath('eshop') - ->addRoute('rss', 'Feed:rss') // coincide con la URL /eshop/rss - ->addRoute('/'); // coincide con la URL /eshop// -``` - - -Combinaciones -------------- - -Las agrupaciones anteriores se pueden combinar entre sí: - -```php -$router = (new RouteList) - ->withDomain('admin.example.com') - ->withModule('Admin') - ->addRoute(/* ... */) - ->addRoute(/* ... */) - ->end() - ->withModule('Images') - ->addRoute(/* ... */) - ->end() - ->end() - ->withDomain('example.com') - ->withPath('export') - ->addRoute(/* ... */) - // ... -``` - - -Parámetros de consulta ----------------------- - -Las máscaras pueden contener también parámetros de consulta (los parámetros que van tras el signo de interrogación en la URL). Para ellos no se puede definir una expresión de validación, pero sí se puede cambiar el nombre con el que se pasan al presenter: - -```php -// queremos usar el parámetro de consulta 'cat' con el nombre 'categoryId' en la aplicación -$router->addRoute('product ? id= & cat=', /* ... */); -``` - - -Parámetros foo --------------- - -Ahora vamos a profundizar. Los parámetros foo son, en esencia, parámetros sin nombre que permiten hacer coincidir una expresión regular. Un ejemplo es una ruta que acepta `/index`, `/index.html`, `/index.htm` e `/index.php`: - -```php -$router->addRoute('index', /* ... */); -``` - -También es posible definir explícitamente la cadena que se usará al generar la URL. La cadena debe colocarse justo después del signo de interrogación. La siguiente ruta es parecida a la anterior, pero genera `/index.html` en lugar de `/index`, porque la cadena `.html` está establecida como valor de generación: - -```php -$router->addRoute('index', /* ... */); -``` - - -Integración -=========== - -Para integrar el router creado en la aplicación tenemos que informar de él al contenedor DI. La forma más sencilla es preparar una factory que cree el objeto del router e indicar al contenedor en la configuración que la use. Digamos que escribimos para ello el método `App\Core\RouterFactory::createRouter()`: - -```php -namespace App\Core; - -use Nette\Application\Routers\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute(/* ... */); - return $router; - } -} -``` - -Después escribimos en la [configuración |dependency-injection:services]: - -```neon -services: - - App\Core\RouterFactory::createRouter -``` - -Las eventuales dependencias, por ejemplo de la base de datos, se pasan al método factory como sus parámetros mediante el [autowiring|dependency-injection:autowiring]: - -```php -public static function createRouter(Nette\Database\Connection $db): RouteList -{ - // ... -} -``` - - -SimpleRouter -============ - -Un router mucho más sencillo que la colección de rutas es [SimpleRouter |api:Nette\Application\Routers\SimpleRouter]. Lo usamos cuando no tenemos requisitos especiales para el formato de las URL, cuando `mod_rewrite` (o sus alternativas) no está disponible o cuando todavía no queremos ocuparnos de las URL bonitas. - -Genera direcciones más o menos con esta forma: - -``` -http://example.com/?presenter=Product&action=detail&id=123 -``` - -El parámetro del constructor de `SimpleRouter` es el presenter y la acción predeterminados, es decir, la acción que se ejecutará si abrimos, por ejemplo, `http://example.com/` sin parámetros adicionales. - -```php -// el presenter predeterminado será 'Home' y la acción 'default' -$router = new Nette\Application\Routers\SimpleRouter('Home:default'); -``` - -Recomendamos definir SimpleRouter directamente en la [configuración |dependency-injection:services]: - -```neon -services: - - Nette\Application\Routers\SimpleRouter('Home:default') -``` - - -SEO y canonización -================== - -El framework contribuye al SEO (Search Engine Optimization) impidiendo la duplicidad de contenido en URL distintas. Si a un destino concreto llevan varias direcciones, p. ej. `/index` e `/index.html`, el framework designa la primera como principal (canónica) y redirige las demás a ella con el código HTTP 301. Gracias a eso, los buscadores no indexan las páginas dos veces ni diluyen su page rank. - -Este proceso se llama canonización. La URL canónica es la que genera el router, es decir, la primera ruta coincidente de la colección sin la bandera OneWay. Por eso indicamos en la colección **primero las rutas principales**. - -De la canonización se encarga el presenter, más en el capítulo [canonización |presenters#Canonización]. - - -HTTPS -===== - -Para usar el protocolo HTTPS es necesario activarlo en el hosting y configurar el servidor correctamente. - -La redirección de todo el sitio web a HTTPS hay que establecerla a nivel del servidor, por ejemplo mediante el archivo `.htaccess` en el directorio raíz de nuestra aplicación, con el código HTTP 301. La configuración puede variar según el hosting y tener más o menos este aspecto: - -``` - - RewriteEngine On - ... - RewriteCond %{HTTPS} off - RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] - ... - -``` - -El router genera las URL con el mismo protocolo con el que se cargó la página, así que no hace falta configurar nada más. - -Sin embargo, si excepcionalmente necesitamos que distintas rutas funcionen bajo protocolos distintos, lo indicamos en la máscara de la ruta: - -```php -// Generará una dirección HTTP -$router->addRoute('http://%host%//', /* ... */); - -// Generará una dirección HTTPS -$router->addRoute('https://%host%//', /* ... */); -``` - - -Depuración del router -===================== - -El panel de enrutamiento que se muestra en la [Tracy Bar |tracy:] es un ayudante útil que muestra la lista de rutas y también los parámetros que el router obtuvo de la URL. - -La barra verde con el símbolo ✓ representa la ruta que procesó la URL actual; el color azul y el símbolo ≈ señalan las rutas que también habrían procesado la URL si la verde no se les hubiera adelantado. Más abajo vemos el presenter y la acción actuales. - -[* routing-debugger.webp *] - -Al mismo tiempo, si se produce una redirección inesperada a causa de la [canonización |#SEO y canonización], conviene mirar en el panel la barra *redirect*, donde averiguará cómo entendió el router la URL originalmente y por qué redirigió. - -.[note] -Al depurar el router recomendamos abrir las herramientas para desarrolladores del navegador (Ctrl+Shift+I o Cmd+Option+I) y desactivar la caché en el panel Network, para que las redirecciones no se guarden en ella. - - -Rendimiento -=========== - -El número de rutas influye en la velocidad del router. Su número no debería superar en ningún caso unas pocas decenas. Si su sitio web tiene una estructura de URL demasiado complicada, puede escribir un [#Router propio] propio. - -Si el router no tiene dependencias, por ejemplo de la base de datos, y su factory no acepta argumentos, podemos serializar su forma compilada directamente en el contenedor DI y acelerar así ligeramente la aplicación. - -```neon -routing: - cache: true -``` - - -Router propio -============= - -Las siguientes líneas están destinadas a usuarios muy avanzados. Puede crear su propio router e integrarlo con toda naturalidad en la colección de rutas. El router es una implementación de la interfaz [api:Nette\Routing\Router] con dos métodos: - -```php -use Nette\Http\IRequest as HttpRequest; -use Nette\Http\UrlScript; - -class MyRouter implements Nette\Routing\Router -{ - public function match(HttpRequest $httpRequest): ?array - { - // ... - } - - public function constructUrl(array $params, UrlScript $refUrl): ?string - { - // ... - } -} -``` - -El método `match` procesa la petición actual [$httpRequest |http:request], de la que se puede obtener no solo la URL sino también las cabeceras, etc., y la convierte en un array que contiene el nombre del presenter y sus parámetros. Si no puede procesar la petición, devuelve null. Al procesar la petición debemos devolver al menos el presenter; la acción es opcional y, si no se indica, es `default`. El nombre del presenter es completo e incluye los eventuales módulos: - -```php -[ - 'presenter' => 'Front:Home', - 'action' => 'default', -] -``` - -El método `constructUrl`, por el contrario, construye la URL absoluta resultante a partir del array de parámetros. Puede usar la información del parámetro [`$refUrl`|api:Nette\Http\UrlScript], que es la URL actual. - -Lo añadimos a la colección de rutas con `add()`: - -```php -$router = new Nette\Application\Routers\RouteList; -$router->add($myRouter); -$router->addRoute(/* ... */); -// ... -``` - - -Uso independiente -================= - -Por uso independiente entendemos el aprovechamiento de las capacidades del router en una aplicación que no usa Nette Application ni presenters. Vale para él casi todo lo que hemos mostrado en este capítulo, con estas diferencias: - -- para las colecciones de rutas usamos la clase [api:Nette\Routing\RouteList] -- como router sencillo, la clase [api:Nette\Routing\SimpleRouter] -- como no existe el par `Presenter:action`, usamos la [#Notación avanzada] - -Así que creamos de nuevo un método que nos monte el router, p. ej.: - -```php -namespace App\Core; - -use Nette\Routing\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute('rss.xml', [ - 'controller' => 'RssFeedController', - ]); - $router->addRoute('article/', [ - 'controller' => 'ArticleController', - ]); - // ... - return $router; - } -} -``` - -Si usa un contenedor DI, cosa que recomendamos, añada de nuevo el método a la configuración y obtenga después del contenedor el router junto con la petición HTTP: - -```php -$router = $container->getByType(Nette\Routing\Router::class); -$httpRequest = $container->getByType(Nette\Http\IRequest::class); -``` - -O cree los objetos directamente: - -```php -$router = App\Core\RouterFactory::createRouter(); -$httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); -``` - -Ahora ya solo queda dejar que el router haga su trabajo: - -```php -$params = $router->match($httpRequest); -if ($params === null) { - // no se encontró ninguna ruta coincidente, se envía un error 404 - exit; -} - -// procesa los parámetros obtenidos -$controller = $params['controller']; -// ... -``` - -Y al revés, use el router para construir un enlace: - -```php -$params = ['controller' => 'ArticleController', 'id' => 123]; -$url = $router->constructUrl($params, $httpRequest->getUrl()); -``` - - -{{composer: nette/routing}} -{{repo: nette/routing}} -{{api: https://api.nette.org/routing/}} diff --git a/application/es/templates.texy b/application/es/templates.texy deleted file mode 100644 index 1aa2c3ba8d..0000000000 --- a/application/es/templates.texy +++ /dev/null @@ -1,419 +0,0 @@ -Plantillas -********** - -.[perex] -Nette usa el sistema de plantillas [Latte |latte:]. Se usa Latte porque es el sistema de plantillas más seguro para PHP y, a la vez, el más intuitivo. No necesita aprender mucho nuevo; bastará con saber PHP y unas cuantas etiquetas. - -Es habitual que una página se componga de una plantilla de layout más la plantilla de la acción concreta. Este podría ser el aspecto de una plantilla de layout; fíjese en los bloques `{block}` y en la etiqueta `{include}`: - -```latte - - - - {block title}My App{/block} - - -
    ...
    - {include content} -
    ...
    - - -``` - -Y esta sería la plantilla de la acción: - -```latte -{block title}Homepage{/block} - -{block content} -

    Homepage

    -... -{/block} -``` - -Define el bloque `content`, que se inserta en el lugar de `{include content}` del layout, y redefine además el bloque `title`, que sobrescribe el `{block title}` del layout. Trate de imaginar el resultado. - - -Búsqueda de plantillas ----------------------- - -En los presenters no necesita indicar qué plantilla debe renderizarse; el framework deduce la ruta automáticamente y le ahorra escribirla. - -Si usa una estructura de directorios en la que cada presenter tiene el suyo, basta con colocar la plantilla en ese directorio con el nombre de la acción (es decir, de la vista). Por ejemplo, para la acción `default`, use la plantilla `default.latte`: - -/--pre -app/ -└── Presentation/ - └── Home/ - ├── HomePresenter.php - └── default.latte -\-- - -Si usa una estructura en la que los presenters están juntos en un directorio y las plantillas en una carpeta `templates`, guárdela o bien en el archivo `..latte`, o bien en `/.latte`: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── Home/ - │ └── default.latte ← 1.ª variante - └── Home.default.latte ← 2.ª variante -\-- - -El directorio `templates` también se puede colocar un nivel más arriba, es decir, al mismo nivel que el directorio con las clases de los presenters. - -Si no se encuentra la plantilla, el presenter responde con un [error 404 - página no encontrada |presenters#Error 404 y otros]. - -Puede cambiar la vista con `$this->setView('otherView')`. También es posible indicar directamente el archivo de plantilla con `$this->template->setFile('/path/to/template.latte')`. - -.[note] -Los archivos en los que se buscan las plantillas se pueden cambiar sobrescribiendo el método [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], que devuelve un array con los posibles nombres de archivo. - - -Búsqueda de la plantilla de layout ----------------------------------- - -Nette busca también automáticamente el archivo del layout. - -Si usa una estructura de directorios en la que cada presenter tiene el suyo, coloque el layout o bien en la carpeta del presenter, si es específico solo de él, o bien un nivel más arriba, si es común a varios presenters: - -/--pre -app/ -└── Presentation/ - ├── @layout.latte ← layout común - └── Home/ - ├── @layout.latte ← solo para el presenter Home - ├── HomePresenter.php - └── default.latte -\-- - -Si usa una estructura en la que los presenters están agrupados en un directorio y las plantillas en una carpeta `templates`, el layout se buscará en estos lugares: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── @layout.latte ← layout común - ├── Home/ - │ └── @layout.latte ← solo para Home, 1.ª variante - └── Home.@layout.latte ← solo para Home, 2.ª variante -\-- - -Si el presenter está en un módulo, la búsqueda continuará además hacia arriba por los niveles de directorio, según el anidamiento de los módulos. - -El nombre del layout se puede cambiar con `$this->setLayout('layoutAdmin')`, y entonces se buscará en el archivo `@layoutAdmin.latte`. También puede indicar directamente el archivo de plantilla del layout con `$this->setLayout('/path/to/template.latte')`. - -Usar `$this->setLayout(false)` o la etiqueta `{layout none}` dentro de la plantilla desactiva la búsqueda del layout. - -.[note] -Los archivos en los que se buscan las plantillas de layout se pueden cambiar sobrescribiendo el método [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], que devuelve un array con los posibles nombres de archivo. - - -Variables de la plantilla -------------------------- - -Las variables se pasan a las plantillas escribiéndolas en `$this->template`. Quedan entonces disponibles en la plantilla como variables locales: - -```php -$this->template->article = $this->articles->getById($id); -``` - -Para pasar automáticamente a la plantilla el valor de una propiedad como variable, márquela con el atributo `#[TemplateVariable]` y con visibilidad pública: .{data-version:3.2.9} - -```php -use Nette\Application\Attributes\TemplateVariable; - -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - #[TemplateVariable] - public string $siteName = 'My blog'; -} -``` - -Si pasa a la plantilla una variable con el mismo nombre, `#[TemplateVariable]` no la sobrescribirá. - - -Variables predeterminadas -------------------------- - -Los presenters y los componentes pasan automáticamente a las plantillas varias variables útiles: - -- `$basePath` es la ruta URL absoluta al directorio raíz (por ejemplo, `/eshop`) -- `$baseUrl` es la URL absoluta al directorio raíz (por ejemplo, `http://localhost/eshop`) -- `$user` es un objeto que [representa al usuario |security:authentication] -- `$presenter` es el presenter actual -- `$control` es el componente o presenter actual -- `$flashes` es un array de [mensajes |presenters#Mensajes flash] enviados con la función `flashMessage()` - -Si usa una clase de plantilla propia, estas variables se pasan si crea una propiedad para ellas. - - -Plantillas con tipos seguros ----------------------------- - -Al desarrollar aplicaciones robustas resulta útil definir explícitamente qué variables espera la plantilla y de qué tipos son. Esto aporta comprobación de tipos en PHP, sugerencias inteligentes en su IDE y permite que el análisis estático detecte errores. - -¿Cómo se define esa lista? Sencillamente, como una clase con propiedades que representan las variables de la plantilla. Nómbrela como al presenter, solo que con `Template` al final: - -```php -/** - * @property-read ArticleTemplate $template - */ -class ArticlePresenter extends Nette\Application\UI\Presenter -{ -} - -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - public Model\Article $article; - public Nette\Security\User $user; - - // y otras variables -} -``` - -El objeto `$this->template` del presenter será ahora una instancia de la clase `ArticleTemplate`. PHP comprobará así los tipos declarados al escribir en él. - -Nette elige la clase de plantilla automáticamente. Primero busca una clase llamada `Template`, por ejemplo `ArticleEditTemplate` para la acción `edit`, y solo si no existe recurre a `Template`. - -La anotación `@property-read` es para el IDE y el análisis estático, y habilita el autocompletado; vea "PhpStorm y el autocompletado de $this⁠-⁠>⁠template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template. - -[* phpstorm-completion.webp *] - -También puede usar el autocompletado directamente en las plantillas. Basta con instalar el plugin de Latte para PhpStorm e indicar al principio de la plantilla el nombre de la clase de parámetros; más información en el capítulo [Latte: sistema de tipos |latte:type-system]: - -```latte -{templateType App\Presentation\Article\ArticleTemplate} -... -``` - -Lo mismo vale para los componentes. Basta con seguir la convención de nombres y crear una clase de parámetros `FifteenTemplate` para un componente como `FifteenControl`. - -Si necesita usar otra clase de parámetros, use el método `createTemplate()`: - -```php -public function renderDefault(): void -{ - $template = $this->createTemplate(SpecialTemplate::class); - $template->foo = 123; - // ... - $this->sendTemplate($template); -} -``` - -.{data-version:3.3.0} -Si necesita influir en cómo se completa la plantilla antes de renderizarla, por ejemplo para añadir variables compartidas por todas las acciones, puede sobrescribir el método `completeTemplate()` del presenter. Se llama justo antes de renderizar la plantilla: - -```php -protected function completeTemplate(Nette\Application\UI\Template $template): void -{ - parent::completeTemplate($template); - $template->siteName = 'My blog'; -} -``` - - -Creación de enlaces -------------------- - -En la plantilla, los enlaces a otros presenters y acciones se crean así: - -```latte -product detail -``` - -El atributo `n:href` resulta muy práctico en las etiquetas HTML ``. Si queremos imprimir el enlace en otro sitio, por ejemplo dentro de un texto, usamos `{link}`: - -```latte -URL is: {link Home:default} -``` - -Encontrará más información en el capítulo [Creación de enlaces URL|creating-links]. - - -Filtros, etiquetas y demás propios ----------------------------------- - -El sistema de plantillas Latte se puede ampliar con filtros, funciones, etiquetas y otros elementos propios. Hay tres enfoques disponibles, desde soluciones rápidas ad hoc hasta patrones arquitectónicos para aplicaciones enteras. - -**Ad hoc en métodos del presenter** - -El enfoque más rápido es añadir los filtros o funciones directamente en el código del presenter o del componente. En los presenters van bien para eso los métodos `beforeRender()` o `render()`: - -```php -protected function beforeRender(): void -{ - // añadir un filtro - $this->template->addFilter('money', fn($val) => '$' . number_format($val, 2)); - - // añadir una función - $this->template->addFunction('isWeekend', fn($date) => $date->format('N') >= 6); -} -``` - -En la plantilla: - -```latte -

    Price: {$price|money}

    - -{if isWeekend($now)} ... {/if} -``` - -Para lógica más compleja puede configurar directamente el objeto `Latte\Engine`: - -```php -protected function beforeRender(): void -{ - $latte = $this->template->getLatte(); - $latte->setFeature(Latte\Feature::MigrationWarnings); -} -``` - -**Con atributos** - -Un enfoque más elegante es definir los filtros y las funciones como métodos directamente en la [clase de parámetros de la plantilla|#Plantillas con tipos seguros] del presenter o del componente, marcados con atributos: - -```php -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - #[Latte\Attributes\TemplateFilter] - public function money(float $val): string - { - return '$' . number_format($val, 2); - } - - #[Latte\Attributes\TemplateFunction] - public function isWeekend(DateTimeInterface $date): bool - { - return $date->format('N') >= 6; - } -} -``` - -Latte descubre y registra automáticamente los métodos marcados con estos atributos. El nombre del filtro o de la función en las plantillas coincide con el del método. Estos métodos deben ser públicos. - -**De forma global, mediante extensiones** - -Los enfoques anteriores encajan con filtros y funciones que solo hacen falta en presenters o componentes concretos, no en toda la aplicación. Para la aplicación entera, lo mejor es crear una [extensión |latte:extending-latte#Latte Extension]. Esta clase centraliza todas las extensiones de Latte de su proyecto. Un ejemplo breve: - -```php -namespace App\Presentation\Accessory; - -final class LatteExtension extends Latte\Extension -{ - public function __construct( - private App\Model\Facade $facade, - private Nette\Security\User $user, - // ... - ) { - } - - public function getFilters(): array - { - return [ - 'timeAgoInWords' => $this->filterTimeAgoInWords(...), - 'money' => $this->filterMoney(...), - // ... - ]; - } - - public function getFunctions(): array - { - return [ - 'canEditArticle' => - fn($article) => $this->facade->canEditArticle($article, $this->user->getId()), - // ... - ]; - } - - private function filterTimeAgoInWords(DateTimeInterface $time): string - { - // ... - } - - // ... -} -``` - -Registre la extensión mediante la [configuración |configuration#Plantillas Latte]: - -```neon -latte: - extensions: - - App\Presentation\Accessory\LatteExtension -``` - -Las extensiones ofrecen varias ventajas: soporte de inyección de dependencias, acceso a la capa de modelo de su aplicación y gestión centralizada de todas las extensiones. También admiten etiquetas propias, proveedores, pases del compilador y más. - - -Configurar todas las plantillas -------------------------------- - -El servicio `TemplateFactory`, que crea todas las plantillas, ofrece un array público de callbacks `$onCreate`. Se llaman cada vez que se crea cualquier plantilla, así que puede configurar filtros, funciones o variables para todas las plantillas de la aplicación desde un solo sitio. Cada callback recibe la plantilla recién creada. Hágase [inyectar |dependency-injection:passing-dependencies] el servicio `TemplateFactory` y registre los callbacks, por ejemplo durante el arranque de la aplicación: - -```php -$templateFactory->onCreate[] = function (Nette\Bridges\ApplicationLatte\Template $template): void { - $template->addFilter('money', fn($val) => '$' . number_format($val, 2)); -}; -``` - - -Traducción ----------- - -Si programa una aplicación multilingüe, probablemente necesitará imprimir algunos textos de la plantilla en distintos idiomas. Nette Framework define para ello la interfaz de traducción [api:Nette\Localization\Translator], que tiene un único método, `translate()`. Este acepta el mensaje `$message`, que suele ser una cadena, y cualesquiera otros parámetros. Su tarea es devolver la cadena traducida. Nette no trae ninguna implementación predeterminada; puede elegir entre varias soluciones ya hechas disponibles en [Componette |https://componette.org/search/localization], según sus necesidades. Su documentación explica cómo configurar el traductor. - -Las plantillas se pueden configurar con un traductor que nos [hacemos pasar |dependency-injection:passing-dependencies], mediante el método `setTranslator()`: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator); -} -``` - -Como alternativa, el traductor se puede fijar mediante la [configuración |configuration#Plantillas Latte]: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -Después, el traductor se puede usar, por ejemplo, como filtro `|translate`, incluidos los parámetros adicionales que se pasan al método `translate()` (véase `foo, bar`): - -```latte -
    {='Basket'|translate} -{$item|translate} -{$item|translate, foo, bar} -``` - -O como etiqueta de guion bajo: - -```latte -{_'Basket'} -{_$item} -{_$item, foo, bar} -``` - -Para traducir una sección de la plantilla existe la etiqueta par `{translate}` (desde Latte 2.11; antes se usaba la etiqueta `{_}`): - -```latte -{translate}Order{/translate} -{translate foo, bar}Order{/translate} -``` - -El traductor se llama normalmente en tiempo de ejecución, al renderizar la plantilla. La versión 3 de Latte, sin embargo, sabe traducir todos los textos estáticos ya durante la compilación de la plantilla. Esto ahorra rendimiento, porque cada cadena se traduce una sola vez y la traducción resultante se escribe en la forma compilada. Así se crean varias versiones compiladas de la plantilla en el directorio de caché, una por idioma. Para ello basta con indicar el idioma como segundo parámetro: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator, $lang); -} -``` - -Por texto estático se entiende, por ejemplo, `{_'hello'}` o `{translate}hello{/translate}`. Los textos no estáticos, como `{_$foo}`, se seguirán traduciendo en tiempo de ejecución. diff --git a/application/es/upgrading.texy b/application/es/upgrading.texy deleted file mode 100644 index 405d660c16..0000000000 --- a/application/es/upgrading.texy +++ /dev/null @@ -1,47 +0,0 @@ -Actualización -************* - - -Actualización a la versión 3.0 -============================== - -Nette 3.0 añade declaraciones de tipo a los parámetros y a los valores de retorno de los métodos. Si sobrescribe uno de esos métodos en una clase que hereda de Nette (por ejemplo, en un presenter o en un componente), debe añadir las mismas declaraciones de tipo, o PHP lanzará el error "Declaration must be compatible". - -La interfaz `Nette\Application\IRouter` ha cambiado. El método `match()` devuelve ahora, y `constructUrl()` acepta, un array de parámetros en lugar de un objeto `Nette\Application\Request`. - -Nette comprueba ahora que cada señal se envía desde el mismo origen (es decir, el mismo dominio y subdominio). Esta política del mismo origen es un mecanismo de seguridad crítico que ayuda a reducir los posibles vectores de ataque. Si quiere permitir otros orígenes, añada al método manejador la anotación `@crossOrigin`: - -```php -/** - * @crossOrigin - */ -public function handleXy(): void -{ -} -``` - -Esto vale también para el envío de formularios. Si quiere permitir el envío desde otros orígenes, hágalo así: - -```php -$form = new Nette\Application\UI\Form; -$form->allowCrossOrigin(); -``` - -El constructor de `Nette\ComponentModel\Component` no se usaba desde hacía años y se eliminó en la versión 3.0. Es una ruptura de compatibilidad: si llama al constructor padre en un componente o presenter que hereda de `Nette\Application\UI\Presenter`, debe eliminar esa llamada. - - -Actualización a la versión 2.4 -============================== - -- `Route` y `SimpleRouter` generan ahora el mismo esquema HTTP/HTTPS con el que se accedió al sitio. Una ruta que exija un protocolo concreto se puede definir con el esquema, por ejemplo `Route('http://domain.cz/')`. -- En los parámetros de tipo bool de los métodos render/action (es decir, con valor predeterminado true o false) y en los parámetros persistentes se distingue ahora entre `false` y `null`. Si el parámetro no está presente en la URL, su valor es ahora `null` (antes era `false`). -- La clase devuelta por `Presenter::getReflection()` ya no es descendiente de `Nette\Reflection\ClassType`, y `getReflection()->getMethod()` ya no es descendiente de `Nette\Reflection\Method`. -- La bandera `SECURED` y `Route::$defaultFlags` están obsoletas. - - -Actualización a la versión 2.3 -============================== - -- las rutas y los nombres de los presenters **distinguen mayúsculas y minúsculas**. Nette le avisa si usa mayúsculas o minúsculas incorrectas en el nombre de un presenter; por motivos de rendimiento no se comprueba la máscara de Route, así que verifíquela usted mismo. -- `Route::addStyle()` y `Route::setStyleProperty()` están obsoletos y ahora emiten `E_USER_DEPRECATED`. -- la extensión de plantilla `.phtml` y la sintaxis antigua de los enlaces ya no están admitidas. diff --git a/application/files/lifecycle-component.svg b/application/files/lifecycle-component.svg deleted file mode 100644 index d6dce64327..0000000000 --- a/application/files/lifecycle-component.svg +++ /dev/null @@ -1,608 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - loadState($params) - saveState(&$params) - render*() - - - - - - - - UI - - handle<Signal>() - - - - - - - - - - - - - - - __construct() - - diff --git a/application/files/lifecycle.svg b/application/files/lifecycle.svg deleted file mode 100644 index bb8b96cb19..0000000000 --- a/application/files/lifecycle.svg +++ /dev/null @@ -1,911 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - $onStartup & startup() - action<Action>() - beforeRender() & $onRender - render<View>() - - - - - - Canonicalize - - - - - - Execution - - - UI - - - Rendering - - - - - - - - - handle<Signal>() - - - - - - - - - - - - - - - __construct() & inject*() - - - - - - - - $onShutdown & shutdown() - - - - - - - Response - - diff --git a/application/files/phpstorm-completion.webp b/application/files/phpstorm-completion.webp deleted file mode 100644 index 0217dddee7..0000000000 Binary files a/application/files/phpstorm-completion.webp and /dev/null differ diff --git a/application/files/request-flow.svg b/application/files/request-flow.svg deleted file mode 100644 index 4df47f2661..0000000000 --- a/application/files/request-flow.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -
    Front controller
    Front controller
    index.php
    index.php
    Front controller
    Front controller
    index.php
    index.php
    Bootstrap
    Bootstrap
    DI
    Container
    DI...
    Application
    Application
    Nette\Application\Application
    Nette\Appli...
    Routing + Mapping
    Routing + Mapping
    Http
    Request
    Http...
    Presenter
    & params
    Presenter...
    HomePresenter
    HomePresent...
    Presenter
    Presenter
    ContactPresenter
    ContactPres...
    Presenter
    Presenter
    PostPresenter
    PostPresent...
    Presenter
    Presenter
    Response
    Response
    Response
    Response
    Response
    Response
    render template
    render template
    error 404
    error 404
    redirect
    redirect
    Request
    Request
    Request
    Request
    Request
    Request
    Bootstrap.php
    Bootstrap.php
    RouterFactory.php
    + common.neon
    RouterFactory.php...
    URL: /
    URL: /
    URL: /foo
    URL: /foo
    URL: /blog/xxx
    URL: /blog/xxx
    Text is not SVG - cannot display
    \ No newline at end of file diff --git a/application/files/routing-debugger.webp b/application/files/routing-debugger.webp deleted file mode 100644 index 9f49bd1f2f..0000000000 Binary files a/application/files/routing-debugger.webp and /dev/null differ diff --git a/application/fr/@home.texy b/application/fr/@home.texy deleted file mode 100644 index b35a1bddef..0000000000 --- a/application/fr/@home.texy +++ /dev/null @@ -1,85 +0,0 @@ -Nette Application -***************** - -.[perex] -Nette Application est le cœur du framework Nette, offrant des outils puissants pour créer des applications web modernes. Il propose un certain nombre de caractéristiques exceptionnelles qui facilitent considérablement le développement et améliorent la sécurité ainsi que la maintenabilité du code. - - -Installation ------------- - -La bibliothèque peut être téléchargée et installée en utilisant l'outil [Composer|best-practices:composer] : - -```shell -composer require nette/application -``` - - -Pourquoi choisir Nette Application ? ------------------------------------- - -Nette a toujours été un pionnier dans le domaine des technologies web. - -**Routeur bidirectionnel :** Nette dispose d'un système de routage avancé, unique par sa bidirectionnalité - non seulement il traduit les URL en actions de l'application, mais il peut également générer des adresses URL en retour. Cela signifie que : -- Vous pouvez changer à tout moment la structure des URL de toute l'application sans avoir à modifier les templates -- Les URL sont automatiquement canonisées, ce qui améliore le SEO -- Le routage est défini à un seul endroit, plutôt que dispersé dans les annotations - -**Composants et signaux :** Le système de composants intégré, inspiré de Delphi et React.js, est tout à fait exceptionnel parmi les frameworks PHP : -- Permet de créer des éléments d'interface utilisateur réutilisables -- Prend en charge la composition hiérarchique des composants -- Offre un traitement élégant 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 manière révolutionnaire de travailler avec AJAX dès 2009, bien avant des solutions similaires comme Hotwire pour Ruby on Rails ou Symfony UX Turbo : -- Les snippets permettent de mettre à jour seulement des parties de la page sans avoir besoin d'écrire du JavaScript -- Intégration automatique avec le système de composants -- Invalidation intelligente des parties de pages -- Quantité minimale de données transférées - -**Templates intuitifs [Latte|latte:] :** Le système de templates le plus sûr pour PHP avec des fonctionnalités avancées : -- Protection automatique contre XSS avec échappement sensible au contexte -- Extensibilité grâce à des filtres, fonctions et balises personnalisés -- Héritage de templates et snippets pour AJAX -- Excellent support de PHP 8.x avec le système de types - -**Dependency Injection :** Nette utilise pleinement l'Injection de Dépendances : -- Passage automatique des dépendances (autowiring) -- Configuration en utilisant le format clair NEON -- Support pour les factories de composants - - -Principaux avantages --------------------- - -- **Sécurité** : Défense automatique contre [les vulnérabilités|nette:vulnerability-protection] telles que XSS, CSRF, etc. -- **Productivité** : Moins de code, plus de fonctionnalités grâce à une conception intelligente -- **Débogage** : [Débogueur Tracy |tracy:] avec le panneau de routage -- **Performance** : Cache intelligent, chargement différé (lazy loading) des composants -- **Flexibilité** : Modification facile des URL même après la finalisation de l'application -- **Composants** : Système unique d'éléments d'interface utilisateur réutilisables -- **Moderne** : Support complet de PHP 8.4+ et du système de types - - -Pour commencer --------------- - -1. [Comment fonctionnent les applications ? |how-it-works] - Comprendre l'architecture de base -2. [Presenters |presenters] - Travailler avec les presenters et les actions -3. [Templates |templates] - Création de templates en Latte -4. [Routage |routing] - Configuration des adresses URL -5. [Composants interactifs |components] - Utilisation du système de composants - - -Compatibilité avec PHP ----------------------- - -| version | compatible avec PHP -|-----------------------|------------------- -| Nette Application 3.3 | PHP 8.3 - 8.5 -| Nette Application 3.2 | PHP 8.1 - 8.5 -| 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 à la dernière version patch. diff --git a/application/fr/@left-menu.texy b/application/fr/@left-menu.texy deleted file mode 100644 index 650fd56ae6..0000000000 --- a/application/fr/@left-menu.texy +++ /dev/null @@ -1,24 +0,0 @@ -Nette Application -***************** -- [Introduction |@home] -- [Comment fonctionnent les applications ? |how-it-works] -- [Bootstrapping|bootstrapping] -- [Presenters|presenters] -- [Templates|templates] -- [Structure des répertoires |directory-structure] -- [Routage|routing] -- [Création de liens URL |creating-links] -- [Composants interactifs |components] -- [AJAX & snippets |ajax] -- [Multiplier |multiplier] -- [Configuration|configuration] -- [Mise à niveau|upgrading] - - -Pour aller plus loin -******************** -- [Pourquoi utiliser Nette ?|www:10-reasons-why-nette] -- [Installation |nette:installation] -- [Créez votre première application ! |quickstart:] -- [Bonnes pratiques |best-practices:] -- [Résolution de problèmes |nette:troubleshooting] diff --git a/application/fr/@meta.texy b/application/fr/@meta.texy deleted file mode 100644 index 72ae4b8db8..0000000000 --- a/application/fr/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentation Nette}} diff --git a/application/fr/ajax.texy b/application/fr/ajax.texy deleted file mode 100644 index bca4d9b42a..0000000000 --- a/application/fr/ajax.texy +++ /dev/null @@ -1,265 +0,0 @@ -AJAX & Snippets -*************** - -
    - -À l'ère des applications web modernes, où la fonctionnalité est souvent répartie entre le serveur et le navigateur, AJAX est un élément de liaison essentiel. Quelles possibilités Nette Framework nous offre-t-il dans ce domaine ? -- envoi de parties de template, appelées snippets -- transmission de variables entre PHP et JavaScript -- outils pour le débogage des requêtes AJAX - -
    - - -Requête AJAX -============ - -Une requête AJAX ne diffère fondamentalement pas d'une requête HTTP classique. Un presenter est appelé avec certains paramètres. Et c'est au presenter de décider comment réagir à la requête - il peut retourner des données au format JSON, envoyer une partie du code HTML, un document XML, etc. - -Côté navigateur, nous initialisons la requête AJAX à l'aide de la fonction `fetch()` : - -```js -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -.then(response => response.json()) -.then(payload => { - // traitement de la réponse -}); -``` - -Côté serveur, nous reconnaissons une requête AJAX avec la méthode `$httpRequest->isAjax()` du service [encapsulant la requête HTTP |http:request]. Pour la détection, il utilise l'en-tête HTTP `X-Requested-With`, il est donc important de l'envoyer. Au sein du presenter, vous pouvez utiliser la méthode `$this->isAjax()`. - -Si vous souhaitez envoyer des données au format JSON, utilisez la méthode [`sendJson()` |presenters#Envoi de la réponse]. La méthode termine également l'activité du presenter. - -```php -public function actionExport(): void -{ - $this->sendJson($this->model->getData); -} -``` - -Si vous prévoyez de répondre en utilisant un template spécial conçu pour AJAX, vous pouvez le faire comme suit : - -```php -public function handleClick($param): void -{ - if ($this->isAjax()) { - $this->template->setFile('path/to/ajax.latte'); - } - // ... -} -``` - - -Snippets -======== - -Le moyen le plus puissant offert par Nette pour connecter le serveur et le client sont les snippets. Grâce à eux, vous pouvez transformer une application ordinaire en une application AJAX avec un minimum d'effort et quelques lignes de code. Le fonctionnement est démontré par l'exemple Fifteen, dont le code se trouve sur [GitHub |https://github.com/nette-examples/fifteen]. - -Les snippets, ou extraits, permettent de mettre à jour uniquement des parties de la page, au lieu de recharger la page entière. C'est non seulement plus rapide et plus efficace, mais cela offre également une expérience utilisateur 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 ? Lors du premier chargement de la page (requête non-AJAX), la page entière est chargée, y compris tous les snippets. Lorsque l'utilisateur interagit avec la page (par exemple, clique sur un bouton, soumet un formulaire, etc.), une requête AJAX est déclenchée au lieu de charger la page entière. Le code dans le presenter exécute l'action et décide quels snippets doivent être mis à jour. Nette rend ces snippets et les envoie dans une charge utile JSON contenant un tableau de snippets. Le code de gestion dans le navigateur réinsère les snippets reçus dans la page. Ainsi, seul le code des snippets modifiés est transféré, ce qui économise de la bande passante et accélère le chargement par rapport au transfert du contenu de la page entière. Si aucun snippet n'est invalidé par `redrawControl()`, Nette renvoie la page entière même pour une requête AJAX : les snippets ne sont envoyés que lorsque quelque chose est invalidé. - - -Naja ----- - -Pour gérer les snippets côté navigateur, la [bibliothèque Naja |https://naja.js.org] est utilisée. [Installez-la |https://naja.js.org/#/guide/01-install-setup-naja] en tant que paquet node.js (pour une utilisation avec des applications comme Webpack, Rollup, Vite, Parcel, et autres) : - -```shell -npm install naja -``` - -…ou insérez-la directement dans le template de la page : - -```latte - -``` - -Tout d'abord, la bibliothèque doit être [initialisée |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization] : - -```js -naja.initialize(); -``` - -Pour transformer un lien ordinaire (signal) ou une soumission de formulaire en une requête AJAX, il suffit de marquer le lien, le formulaire ou le bouton correspondant avec la classe `ajax` : - -```latte -Go - -
    - -
    - -ou - -
    - -
    -``` - - -Redessiner les Snippets ------------------------ - -Chaque objet de la classe [Control |components] (y compris le Presenter lui-même) enregistre si des changements nécessitant son redessin se sont produits. La méthode `redrawControl()` est utilisée pour cela : - -```php -public function handleLogin(string $user): void -{ - // après la connexion, la partie pertinente doit être redessinée - $this->redrawControl(); - // ... -} -``` - -Nette permet un contrôle encore plus fin de ce qui doit être redessiné. En effet, la méthode mentionnée peut accepter le nom du snippet comme argument. Il est donc possible d'invalider (c'est-à-dire : forcer le redessin) au niveau des parties du template. Si le composant entier est invalidé, chacun de ses snippets sera également redessiné : - -```php -// invalide le snippet 'header' -$this->redrawControl('header'); -``` - -Vous pouvez aussi annuler une invalidation en attente à l'aide du second paramètre `$redraw` : l'appel `$this->redrawControl('header', redraw: false)` marque le snippet comme n'ayant pas besoin d'être redessiné. La signature complète est `redrawControl(?string $snippet = null, bool $redraw = true)`. - - -Snippets dans Latte -------------------- - -L'utilisation des snippets dans Latte est extrêmement facile. Pour définir une partie du template comme snippet, enveloppez-la simplement avec les balises `{snippet}` et `{/snippet}` : - -```latte -{snippet header} -

    Bonjour ...

    -{/snippet} -``` - -Le snippet crée un élément `
    ` dans la page HTML avec un `id` spécial généré. Lors du redessin du snippet, le contenu de cet élément est mis à jour. Il est donc nécessaire que lors du rendu initial de la page, tous les snippets soient également rendus, même s'ils peuvent être vides au début. - -Vous pouvez également créer un snippet avec un élément autre que `
    ` en utilisant un n:attribut : - -```latte -
    -

    Bonjour ...

    -
    -``` - - -Zones de Snippets ------------------ - -Les noms des snippets peuvent aussi être des expressions : - -```latte -{foreach $items as $id => $item} -
  • {$item}
  • -{/foreach} -``` - -Pris isolément, c'est une étape intermédiaire non fonctionnelle : rendu en dehors d'un `{snippet}` ou `{snippetArea}` statique, un snippet dynamique déclenche un `E_USER_WARNING` avec le message *Dynamic snippets are allowed only inside static snippet/snippetArea.* Nous corrigeons cela ci-dessous. - -Cela crée plusieurs snippets `item-0`, `item-1`, etc. Si nous invalidions directement un snippet dynamique (par exemple `item-1`), rien ne serait redessiné. La raison est que les snippets fonctionnent vraiment comme des extraits et ne sont rendus qu'eux-mêmes directement. Cependant, il n'y a en fait aucun snippet nommé `item-1` dans le template. Il n'est créé que lors de l'exécution du code autour du snippet, c'est-à-dire la boucle foreach. Nous marquons donc la partie du template qui doit être exécutée à l'aide de la balise `{snippetArea}` : - -```latte -
      - {foreach $items as $id => $item} -
    • {$item}
    • - {/foreach} -
    -``` - -Et nous laissons redessiner à la fois le snippet lui-même et toute la zone parente : - -```php -$this->redrawControl('itemsContainer'); -$this->redrawControl('item-1'); -``` - -En même temps, il est conseillé de s'assurer que le tableau `$items` ne contient que les éléments qui doivent être redessinés. - -Si nous insérons un autre template contenant des snippets dans le template principal à l'aide de la balise `{include}`, il est nécessaire d'inclure à nouveau l'insertion du template dans une `snippetArea` et de l'invalider avec le snippet : - -```latte -{snippetArea include} - {include 'included.latte'} -{/snippetArea} -``` - -```latte -{* included.latte *} -{snippet item} - ... -{/snippet} -``` - -```php -$this->redrawControl('include'); -$this->redrawControl('item'); -``` - - -Snippets dans les Composants ----------------------------- - -Vous pouvez également créer des snippets dans les [composants|components] et Nette les redessinera automatiquement. Mais il y a une certaine limitation : pour redessiner les snippets, il appelle la méthode `render()` sans paramètres. Par conséquent, la transmission de paramètres dans le template ne fonctionnera pas : - -```latte -OK -{control productGrid} - -ne fonctionnera pas : -{control productGrid, $arg, $arg} -{control productGrid:paginator} -``` - - -Envoi de Données Utilisateur ----------------------------- - -Avec les snippets, vous pouvez envoyer n'importe quelles autres données au client. Il suffit de les écrire dans l'objet `payload` : - -```php -public function actionDelete(int $id): void -{ - // ... - if ($this->isAjax()) { - $this->payload->message = 'Succès'; - } -} -``` - - -Redirection ------------ - -Pendant une requête AJAX, les méthodes `redirect()` et `redirectUrl()` n'envoient pas de redirection HTTP. Elles écrivent l'URL cible dans le payload (l'objet de données envoyé dans la réponse AJAX), plus précisément dans sa propriété `payload.redirect`, et l'envoient ; la redirection elle-même est ensuite effectuée par la bibliothèque côté client (Naja). - - -Transmission de Paramètres -========================== - -Si nous envoyons des paramètres à un composant via une requête AJAX, qu'il s'agisse de paramètres de signal ou de paramètres persistants, nous devons spécifier leur nom global dans la requête, qui inclut également le nom du composant. Le nom complet du paramètre est retourné par la méthode `getParameterId()`. - -```js -let url = new URL({link //foo!}); -url.searchParams.set({$control->getParameterId('bar')}, bar); - -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -``` - -Et la méthode handle avec les paramètres correspondants dans le composant : - -```php -public function handleFoo(int $bar): void -{ -} -``` - - -Lectures complémentaires -======================== - -- [Snippets dynamiques |best-practices:dynamic-snippets] diff --git a/application/fr/bootstrapping.texy b/application/fr/bootstrapping.texy deleted file mode 100644 index d1c2d317b9..0000000000 --- a/application/fr/bootstrapping.texy +++ /dev/null @@ -1,312 +0,0 @@ -Bootstrapping -************* - -
    - -Le bootstrapping est le processus d'initialisation de l'environnement de l'application, de création d'un conteneur d'injection de dépendances (DI) et de démarrage de l'application. Nous discuterons : - -- comment la classe Bootstrap initialise l'environnement -- comment les applications sont configurées en utilisant des fichiers NEON -- comment distinguer entre le mode de production et de développement -- comment créer et configurer le conteneur DI - -
    - - -Les applications, qu'elles soient web ou des scripts exécutés depuis la ligne de commande, commencent leur exécution par une forme d'initialisation de l'environnement. Autrefois, un fichier nommé par exemple `include.inc.php` s'en chargeait, inclus par le fichier initial. Dans les applications Nette modernes, il a été remplacé par la classe `Bootstrap`, que vous trouverez dans le fichier `app/Bootstrap.php` en tant que partie de l'application. Elle peut ressembler à ceci, par exemple : - -```php -namespace App; - -use Nette; -use Nette\Bootstrap\Configurator; - -class Bootstrap -{ - private Configurator $configurator; - private string $rootDir; - - public function __construct() - { - $this->rootDir = dirname(__DIR__); - // Le Configurator est responsable de la configuration de l'environnement et des services de l'application. - $this->configurator = new Configurator; - // Définit le répertoire pour les fichiers temporaires générés par Nette (par exemple, les templates 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 - { - // 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 "couteau suisse" pour le débogage. - $this->configurator->enableTracy($this->rootDir . '/log'); - - // RobotLoader : charge automatiquement toutes les classes dans le répertoire sélectionné - $this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); - } - - private function setupContainer(): void - { - // Charge les fichiers de configuration - $this->configurator->addConfig($this->rootDir . '/config/common.neon'); - } -} -``` - - -index.php -========= - -Le fichier initial pour les applications web est `index.php`, situé dans le [répertoire public |directory-structure#Répertoire public www/] `www/`. Il demande à la classe Bootstrap d'initialiser l'environnement et de créer le conteneur DI. Ensuite, il obtient le service `Application` à partir de celui-ci, qui lance l'application web : - -```php -$bootstrap = new App\Bootstrap; -// Initialisation de l'environnement + création du conteneur DI -$container = $bootstrap->bootWebApplication(); -// Le conteneur DI crée l'objet Nette\Application\Application -$application = $container->getByType(Nette\Application\Application::class); -// Lancement de l'application Nette et traitement de la requête entrante -$application->run(); -``` - -.[note] -L'objet `$application` émet des [événements |nette:glossary#Événements] au fil du traitement de la requête : `onStartup`, `onRequest`, `onPresenter`, `onResponse`, `onShutdown` et `onError` (sur une exception non traitée). Vous pouvez y attacher des gestionnaires, ce qui est pratique pour la journalisation ou la surveillance à l'échelle de l'application. - -Comme vous pouvez le voir, la classe [api:Nette\Bootstrap\Configurator] aide à configurer l'environnement et à créer le conteneur d'injection de dépendances (DI), que nous allons maintenant présenter plus en détail. - - -Mode Développement vs Production -================================ - -Nette se comporte différemment selon qu'il s'exécute sur un serveur de développement ou de production : - -🛠️ Mode Développement (Development): - - Affiche la barre de débogage Tracy avec des informations utiles (requêtes SQL, temps d'exécution, mémoire utilisée) - - En cas d'erreur, affiche une page d'erreur détaillée avec les appels de fonction et le contenu des variables - - Rafraîchit automatiquement le cache lors de la modification des templates Latte, des fichiers de configuration, etc. - - -🚀 Mode Production (Production): - - N'affiche aucune information de débogage, toutes les erreurs sont écrites dans le journal - - En cas d'erreur, affiche l'ErrorPresenter ou une page générique "Erreur Serveur" - - Le cache n'est jamais rafraîchi 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 quoi que ce soit ou de basculer manuellement : - -- mode développement : sur localhost (adresse IP `127.0.0.1` ou `::1`) s'il n'y a pas de proxy présent (c'est-à-dire son en-tête HTTP) -- mode production : partout ailleurs - -Si nous voulons activer le mode développement dans d'autres cas, par exemple pour les programmeurs accédant depuis une adresse IP spécifiques, nous utilisons `setDebugMode()` : - -```php -$this->configurator->setDebugMode('23.75.345.200'); // un tableau d'adresses IP peut également être fourni -``` - -Nous recommandons vivement de combiner l'adresse IP avec un cookie. Nous stockons un jeton secret, par exemple `secret1234`, dans le cookie `nette-debug`, et activons ainsi le mode développement pour les programmeurs accédant depuis une adresse IP spécifique et ayant également le jeton mentionné dans le cookie : - -```php -$this->configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Nous pouvons également désactiver complètement le mode développement, même pour localhost : - -```php -$this->configurator->setDebugMode(false); -``` - -Attention, la valeur `true` active le mode développement de manière forcée, ce qui ne doit jamais se produire sur un serveur de production. - -En interne, la détection automatique est assurée par la méthode statique `Configurator::detectDebugMode()`, que vous pouvez aussi appeler vous-même, par exemple pour détecter le mode développement en dehors du configurateur. Elle accepte une liste blanche facultative d'adresses IP ou de noms de machines et retourne si la requête courante doit s'exécuter en mode développement : - -```php -$debug = Nette\Bootstrap\Configurator::detectDebugMode('23.75.345.200'); -``` - - -Outil de Débogage Tracy -======================= - -Pour faciliter le débogage, nous activons également l'excellent outil [Tracy |tracy:]. En mode développement, il visualise les erreurs, et en mode production, il enregistre les erreurs dans le répertoire spécifié : - -```php -$this->configurator->enableTracy($this->rootDir . '/log'); -``` - - -Fichiers Temporaires -==================== - -Nette utilise un cache pour le conteneur DI, RobotLoader, les templates, etc. Il est donc nécessaire de définir le chemin vers le répertoire où le cache sera stocké : - -```php -$this->configurator->setTempDirectory($this->rootDir . '/temp'); -``` - -Sous Linux ou macOS, définissez les [permissions d'écriture |nette:troubleshooting#Régler les permissions des répertoires] pour les répertoires `log/` et `temp/`. - - -RobotLoader -=========== - -En règle générale, nous voudrons charger automatiquement les classes à l'aide de [RobotLoader |robot-loader:], nous devons donc le démarrer et le laisser charger les classes du répertoire où se trouve `Bootstrap.php` (c'est-à-dire `__DIR__`), ainsi que de tous ses sous-répertoires : - -```php -$this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); -``` - -Une approche alternative consiste à laisser les classes être chargées uniquement via [Composer |best-practices:composer] tout en respectant PSR-4. - - -Fuseau Horaire -============== - -Via le configurateur, vous pouvez définir le fuseau horaire par défaut. - -```php -$this->configurator->setTimeZone('Europe/Prague'); -``` - - -Configuration du Conteneur DI -============================= - -Une partie du processus de démarrage est la création du conteneur DI, ou factory d'objets, qui est le cœur de toute l'application. Il s'agit en fait d'une classe PHP générée par Nette et stockée dans le répertoire cache. La factory produit les objets clés de l'application, et à l'aide des fichiers de configuration, nous lui indiquons comment les créer et les configurer, influençant ainsi le comportement de toute l'application. - -Les fichiers de configuration sont généralement écrits au format [NEON |neon:format]. Dans un chapitre séparé, vous apprendrez [tout ce qui peut être configuré |nette:configuring]. - -.[tip] -En mode développement, le conteneur est automatiquement mis à jour à chaque modification du code ou des fichiers de configuration. En mode production, il n'est généré qu'une seule fois et les modifications ne sont pas vérifiées pour maximiser les performances. - -Là où `createContainer()` construit le conteneur et retourne son instance, la méthode `loadContainer()` ne retourne que le nom de la classe du conteneur généré, que vous pouvez ensuite instancier vous-même. C'est utile dans les scénarios avancés. - -Nous chargeons les fichiers de configuration à l'aide de `addConfig()`: - -```php -$this->configurator->addConfig($this->rootDir . '/config/common.neon'); -``` - -Si nous voulons ajouter plusieurs fichiers de configuration, nous pouvons appeler la fonction `addConfig()` plusieurs fois. - -```php -$configDir = $this->rootDir . '/config'; -$this->configurator->addConfig($configDir . '/common.neon'); -$this->configurator->addConfig($configDir . '/services.neon'); -if (PHP_SAPI === 'cli') { - $this->configurator->addConfig($configDir . '/cli.php'); -} -``` - -Le nom `cli.php` n'est pas une faute de frappe ; la configuration peut également être écrite dans un fichier PHP qui la retourne sous forme de tableau. - -Nous pouvons également ajouter d'autres fichiers de configuration dans la [section `includes` |dependency-injection:configuration#Inclure des fichiers]. - -Si des éléments avec les mêmes clés apparaissent dans les fichiers de configuration, ils seront écrasés ou, dans le cas des [tableaux, fusionnés |dependency-injection:configuration#Fusion]. Le fichier inclus ultérieurement a une priorité plus élevée que le précédent. Le fichier dans lequel la section `includes` est listée a une priorité plus élevée que les fichiers qui y sont inclus. - - -Paramètres Statiques --------------------- - -Les paramètres utilisés dans les fichiers de configuration peuvent être définis [dans la section `parameters` |dependency-injection:configuration#Paramètres] et également transmis (ou écrasés) via la méthode `addStaticParameters()` (qui a un alias `addParameters()`). Il est important de noter que différentes valeurs de paramètres entraîneront la génération de conteneurs DI supplémentaires, c'est-à-dire de classes supplémentaires. - -```php -$this->configurator->addStaticParameters([ - 'projectId' => 23, -]); -``` - -Le paramètre `projectId` peut être référencé dans la configuration en utilisant la notation habituelle `%projectId%`. - - -Paramètres Dynamiques ---------------------- - -Nous pouvons également ajouter des paramètres dynamiques au conteneur, dont les différentes valeurs, contrairement aux paramètres statiques, ne provoquent pas la génération de nouveaux conteneurs DI. - -```php -$this->configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Nous pouvons ainsi facilement ajouter, par exemple, des variables d'environnement, qui peuvent ensuite être référencées dans la configuration en utilisant la notation `%env.variable%`. - -```php -$this->configurator->addDynamicParameters([ - 'env' => getenv(), -]); -``` - - -Paramètres par Défaut ---------------------- - -Dans les fichiers de configuration, vous pouvez utiliser ces paramètres statiques : - -- `%appDir%` est le chemin absolu vers le répertoire contenant le fichier `Bootstrap.php` -- `%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 -- `%baseUrl%` est l'URL absolue vers le répertoire racine (paramètre dynamique résolu à l'exécution) -- `%debugMode%` indique si l'application est en mode débogage -- `%consoleMode%` indique si la requête provient de la ligne de commande - - -Services Importés ------------------ - -Maintenant, allons plus en profondeur. Bien que le but du conteneur DI soit de créer des objets, il peut exceptionnellement être nécessaire d'insérer un objet existant dans le conteneur. Nous le faisons en définissant le service avec l'indicateur `imported: true`. - -```neon -services: - myservice: - type: App\Model\MyCustomService - imported: true -``` - -Et dans le bootstrap, nous insérons l'objet dans le conteneur : - -```php -$this->configurator->addServices([ - 'myservice' => new App\Model\MyCustomService('foobar'), -]); -``` - - -Environnements Différents -========================= - -N'hésitez pas à modifier la classe `Bootstrap` selon vos besoins. Vous pouvez ajouter des paramètres à la méthode `bootWebApplication()` pour distinguer les projets web. Ou nous pouvons ajouter d'autres méthodes, par exemple `bootTestEnvironment()`, qui initialise l'environnement pour les tests unitaires, `bootConsoleApplication()` pour les scripts appelés depuis la ligne de commande, etc. - -```php -public function bootTestEnvironment(): Nette\DI\Container -{ - Tester\Environment::setup(); // initialisation de Nette Tester - $this->setupContainer(); - return $this->configurator->createContainer(); -} - -public function bootConsoleApplication(): Nette\DI\Container -{ - $this->configurator->setDebugMode(false); - $this->initializeEnvironment(); - $this->setupContainer(); - return $this->configurator->createContainer(); -} -``` diff --git a/application/fr/components.texy b/application/fr/components.texy deleted file mode 100644 index 3435eed77d..0000000000 --- a/application/fr/components.texy +++ /dev/null @@ -1,504 +0,0 @@ -Composants interactifs -********************** - -
    - -Les composants sont des objets réutilisables indépendants que nous insérons dans les pages. Il peut s'agir de formulaires, de datagrids, de sondages, en fait de tout ce qu'il est judicieux d'utiliser de manière répétée. Nous allons montrer : - -- comment utiliser les composants ? -- comment les écrire ? -- que sont les signaux ? - -
    - -Nette intègre un système de composants. Les vétérans peuvent se souvenir de quelque chose de similaire dans Delphi ou ASP.NET Web Forms ; React ou Vue.js sont basés sur quelque chose de vaguement similaire. Cependant, dans le monde des frameworks PHP, c'est une caractéristique unique. - -Pourtant, les composants influencent fondamentalement l'approche de la création d'applications. Vous pouvez composer des pages à partir d'unités préfabriquées. Besoin d'un datagrid dans l'administration ? Vous le trouverez sur [Componette |https://componette.org/search/component], un dépôt d'add-ons open-source (donc pas seulement des composants) pour Nette, et l'insérerez simplement dans le presenter. - -Vous pouvez intégrer n'importe quel nombre de composants dans un presenter. Et vous pouvez insérer d'autres composants dans certains composants. Cela crée un arbre de composants, dont la racine est le presenter. - - -Méthodes Factory -================ - -Comment les composants sont-ils insérés dans le presenter et ensuite utilisés ? Généralement via des méthodes factory. - -Une factory de composants représente une manière élégante de créer des composants uniquement lorsqu'ils sont réellement nécessaires (lazy / on demand). Toute la magie réside dans l'implémentation d'une méthode nommée `createComponent()`, où `` est le nom du composant à créer, et qui crée et retourne le composant. - -```php .{file:DefaultPresenter.php} -class DefaultPresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentPoll(): PollControl - { - $poll = new PollControl; - $poll->items = $this->item; - return $poll; - } -} -``` - -Grâce au fait que tous les composants sont créés dans des méthodes séparées, le code gagne en clarté. - -.[note] -Les noms des composants commencent toujours par une lettre minuscule, même s'ils sont écrits avec une majuscule dans le nom de la méthode. - -Nous n'appelons jamais les factories directement ; elles s'appellent elles-mêmes la première fois que nous utilisons le composant. Grâce à cela, le composant est créé au bon moment et seulement s'il est réellement nécessaire. Si nous n'utilisons pas le composant (par exemple, lors d'une requête AJAX où seule une partie de la page est transmise, ou lors de la mise en cache du template), il n'est pas créé du tout et nous économisons les performances du serveur. - -```php .{file:DefaultPresenter.php} -// nous accédons au composant et si c'est la première fois, -// createComponentPoll() est appelée, qui le crée -$poll = $this->getComponent('poll'); -// syntaxe alternative : $poll = $this['poll']; -``` - -Dans le template, il est possible de rendre un composant à l'aide de la balise [{control} |#Rendu]. Il n'est donc pas nécessaire de transmettre manuellement les composants au template. - -```latte -

    Votez

    - -{control poll} -``` - -.[tip] -Pour créer dynamiquement un nombre variable de composants, utilisez [Multiplier |multiplier]. - -Les méthodes fabriques `createComponent()` ne fonctionnent pas qu'au sein des presenters. Vous pouvez imbriquer un composant dans un autre exactement de la même façon et les composer en arbre, ce qui est pratique par exemple pour un formulaire rendu séparément à l'intérieur d'un composant. - - -Style Hollywood -=============== - -Les composants utilisent couramment une technique fraîche que nous aimons appeler le style Hollywood. Vous connaissez sûrement la phrase célèbre que les participants aux auditions de films entendent si souvent : "Ne nous appelez pas, nous vous appellerons". Et c'est exactement de cela qu'il s'agit. - -Dans Nette, au lieu de devoir constamment demander ("le formulaire a-t-il été soumis ?", "était-il valide ?" ou "l'utilisateur a-t-il appuyé sur ce bouton ?"), vous dites au framework "quand cela arrivera, appelle cette méthode" et vous lui laissez le reste du travail. Si vous programmez en JavaScript, vous connaissez bien ce style de programmation. Vous écrivez des fonctions qui sont appelées lorsqu'un certain événement se produit. Et le langage leur transmet les paramètres appropriés. - -Cela change complètement la façon d'écrire des applications. Plus vous pouvez laisser de tâches au framework, moins vous avez de travail. Et moins vous risquez d'oublier quelque chose. - - -Écrire un composant -=================== - -Par le terme composant, nous entendons généralement un descendant de la classe [api:Nette\Application\UI\Control]. (Il serait donc plus précis d'utiliser le terme "controls", mais "contrôles" a un sens différent en français et "composants" s'est plutôt imposé.) Le presenter lui-même [api:Nette\Application\UI\Presenter] est d'ailleurs aussi un descendant de la classe `Control`. - -```php .{file:PollControl.php} -use Nette\Application\UI\Control; - -class PollControl extends Control -{ -} -``` - - -Rendu -===== - -Nous savons déjà que pour rendre un composant, on utilise la balise `{control componentName}`. Celle-ci appelle en fait la méthode `render()` du composant, dans laquelle nous nous occupons du rendu. Nous avons à notre disposition, tout comme dans le presenter, un [template Latte|templates] dans la variable `$this->template`, auquel nous passons des paramètres. Contrairement au presenter, nous devons spécifier le fichier de template et le faire rendre : - -```php .{file:PollControl.php} -public function render(): void -{ - // nous insérons quelques paramètres dans le template - $this->template->param = $value; - // et nous le rendons - $this->template->render(__DIR__ . '/poll.latte'); -} -``` - -La balise `{control}` permet de passer des paramètres à la méthode `render()` : - -```latte -{control poll, $id, $message} -``` - -```php .{file:PollControl.php} -public function render(int $id, string $message): void -{ - // ... -} -``` - -Parfois, un composant peut être constitué de plusieurs parties que nous voulons rendre séparément. Pour chacune d'elles, nous créons notre propre méthode de rendu, ici dans l'exemple `renderPaginator()` : - -```php .{file:PollControl.php} -public function renderPaginator(): void -{ - // ... -} -``` - -Et dans le template, nous l'appelons ensuite en utilisant : - -```latte -{control poll:paginator} -``` - -Pour une meilleure compréhension, il est bon de savoir comment cette balise est traduite en PHP. - -```latte -{control poll} -{control poll:paginator, 123, 'hello'} -``` - -se traduit par : - -```php -$control->getComponent('poll')->render(); -$control->getComponent('poll')->renderPaginator(123, 'hello'); -``` - -La méthode `getComponent()` retourne le composant `poll` et appelle la méthode `render()` sur ce composant, ou `renderPaginator()` si un autre mode de rendu est spécifié dans la balise après les deux-points. - -.[caution] -Attention, si **`=>`** apparaît n'importe où dans les paramètres, tous les paramètres seront enveloppés dans un tableau et passés comme premier argument : - -```latte -{control poll, id: 123, message: 'hello'} -``` - -se traduit par : - -```php -$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']); -``` - -Rendu d'un sous-composant : - -```latte -{control cartControl-someForm} -``` - -se traduit par : - -```php -$control->getComponent("cartControl-someForm")->render(); -``` - -Les composants, tout comme les presenters, transmettent automatiquement plusieurs variables utiles aux templates : - -- `$basePath` est le chemin URL absolu vers le répertoire racine (par ex. `/eshop`) -- `$baseUrl` est l'URL absolue vers le répertoire racine (par ex. `http://localhost/eshop`) -- `$user` est l'objet [représentant l'utilisateur |security:authentication] -- `$presenter` est le presenter actuel -- `$control` est le composant actuel -- `$flashes` tableau des [messages |#Messages Flash] envoyés par la fonction `flashMessage()` - - -Signal -====== - -Nous savons déjà que la navigation dans une application Nette consiste à créer des liens ou des redirections vers des paires `Presenter:action`. Mais que faire si nous voulons simplement effectuer une action sur la **page actuelle** ? Par exemple, changer le tri des colonnes dans un tableau ; supprimer un élément ; basculer entre le mode clair/sombre ; soumettre un formulaire ; voter dans un sondage ; etc. - -Ce type de requête est appelé signal. Et tout comme les actions appellent les méthodes `action()` ou `render()`, les signaux appellent les méthodes `handle()`. Alors que le concept d'action (ou de vue) est purement lié aux presenters, les signaux concernent tous les composants. Et donc aussi les presenters, car `UI\Presenter` est un descendant de `UI\Control`. - -```php -public function handleClick(int $x, int $y): void -{ - // ... traitement du signal ... -} -``` - -Nous créons un lien qui appelle un signal de la manière habituelle, c'est-à-dire dans le template avec l'attribut `n:href` ou la balise `{link}`, dans le code avec la méthode `link()`. Plus d'informations dans le chapitre [Création de liens URL |creating-links#Liens vers un signal]. - -```latte -cliquez ici -``` - -Un signal est toujours appelé sur le presenter et l'action actuels ; il n'est pas possible de l'appeler sur un autre presenter ou une autre action. - -Le signal provoque donc un rechargement de la page exactement comme lors de la requête initiale, mais appelle en plus la méthode de gestion du signal avec les paramètres appropriés. Si la méthode n'existe pas, une exception [api:Nette\Application\UI\BadSignalException] est levée, qui s'affiche à l'utilisateur comme une page d'erreur 403 Interdit. - - -Snippets et AJAX -================ - -Les signaux vous rappellent peut-être un peu AJAX : des gestionnaires qui sont appelés sur la page actuelle. Et vous avez raison, les signaux sont en effet souvent appelés via AJAX, puis seules les parties modifiées de la page sont transmises au navigateur. C'est-à-dire les fameux snippets. Plus d'informations peuvent être trouvées sur la [page dédiée à AJAX |ajax]. - - -Messages Flash -============== - -Un composant possède son propre stockage de messages flash indépendant du presenter. Ce sont des messages qui informent par exemple du résultat d'une opération. Une caractéristique importante des messages flash est qu'ils sont disponibles dans le template même après une redirection. Même après affichage, ils restent actifs pendant 30 secondes supplémentaires - par exemple, au cas où l'utilisateur rafraîchirait la page en raison d'une erreur de transmission - le message ne disparaîtra donc pas immédiatement. - -L'envoi est géré par la méthode [flashMessage |api:Nette\Application\UI\Control::flashMessage()]. Le premier paramètre est le texte du message (`string`, `Stringable`) ou un objet `stdClass` représentant le message. Le deuxième paramètre facultatif est son type (erreur, avertissement, info, etc.). La méthode `flashMessage()` retourne une instance du message flash sous forme d'objet `stdClass`, auquel des informations supplémentaires peuvent être ajoutées. - -```php -$this->flashMessage('L\'élément a été supprimé.'); -$this->redirect(/* ... */); // et nous redirigeons -``` - -Dans le template, ces messages sont disponibles dans la variable `$flashes` sous forme d'objets `stdClass`, qui contiennent les propriétés `message` (texte du message), `type` (type de message) et peuvent contenir les informations utilisateur mentionnées précédemment. Nous les rendons par exemple comme ceci : - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -Redirection après un signal -=========================== - -Le traitement d'un signal de composant est souvent suivi d'une redirection. C'est une situation similaire à celle des formulaires - après leur soumission, nous redirigeons également pour éviter que les données ne soient renvoyées si la page est rafraîchie dans le navigateur. - -```php -$this->redirect('this') // redirige vers le presenter et l'action actuels -``` - -Comme un composant est un élément réutilisable et ne devrait généralement pas avoir de lien direct avec des presenters spécifiques, les méthodes `redirect()` et `link()` interprètent automatiquement le paramètre comme un signal du composant : - -```php -$this->redirect('click') // redirige vers le signal 'click' du même composant -``` - -Si vous avez besoin de rediriger vers un autre presenter ou une autre action, vous pouvez le faire via le presenter : - -```php -$this->getPresenter()->redirect('Product:show'); // redirige vers un autre presenter/action -``` - - -Paramètres persistants -====================== - -Les paramètres persistants sont utilisés pour maintenir l'état dans les composants entre différentes requêtes. Leur valeur reste la même même après avoir cliqué sur un lien. Contrairement aux données de session, ils sont transmis dans l'URL. Et cela de manière entièrement automatique, y compris pour les liens créés dans d'autres composants sur la même page. - -Vous avez par exemple un composant pour la pagination du contenu. Il peut y avoir plusieurs de ces composants sur une page. Et nous souhaitons qu'après avoir cliqué sur un lien, tous les composants restent sur leur page actuelle. C'est pourquoi nous faisons du numéro de page (`page`) un paramètre persistant. - -La création d'un paramètre persistant est extrêmement simple dans Nette. Il suffit de créer une propriété publique et de la marquer avec un attribut : (auparavant, `/** @persistent */` était utilisé) - -```php -use Nette\Application\Attributes\Persistent; // cette ligne est importante - -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; // doit être public -} -``` - -Nous recommandons d'indiquer également le type de données pour la propriété (par ex. `int`) et vous pouvez également spécifier une valeur par défaut. Les valeurs des paramètres peuvent être [validées |#Validation des paramètres persistants]. - -Lors de la création d'un lien, la valeur du paramètre persistant peut être modifiée : - -```latte -suivant -``` - -Ou il peut être *réinitialisé*, c'est-à-dire supprimé de l'URL. Il prendra alors sa valeur par défaut : - -```latte -réinitialiser -``` - - -Composants persistants -====================== - -Non seulement les paramètres, mais aussi les composants peuvent être persistants. Pour un tel composant, ses paramètres persistants sont également transmis entre différentes actions du presenter ou entre plusieurs presenters. Nous marquons les composants persistants par un attribut sur la classe du presenter. Par exemple, nous marquons ainsi les composants `calendar` et `poll` : - -```php -use Nette\Application\Attributes\Persistent; - -#[Persistent('calendar', 'poll')] -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Il n'est pas nécessaire de marquer les sous-composants à l'intérieur de ces composants ; ils deviennent persistants eux aussi. - -L'ancienne annotation `@persistent` fonctionne encore, mais elle est dépréciée et déclenche un avertissement : - -```php -/** - * @persistent(calendar, poll) - */ -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Composants avec dépendances -=========================== - -Comment créer des composants avec des dépendances sans "polluer" les presenters qui les utiliseront ? Grâce aux fonctionnalités intelligentes du conteneur DI de Nette, comme pour l'utilisation de services classiques, la majeure partie du travail peut être laissée au framework. - -Prenons comme exemple un composant qui a une dépendance envers le service `PollFacade` : - -```php -class PollControl extends Control -{ - public function __construct( - private int $id, // Id du sondage pour lequel nous créons le composant - private PollFacade $facade, - ) { - } - - public function handleVote(int $voteId): void - { - $this->facade->vote($this->id, $voteId); - // ... - } -} -``` - -Si nous écrivions un service classique, il n'y aurait rien à faire. Le conteneur DI se chargerait invisiblement de transmettre toutes les dépendances. Mais avec les composants, nous les traitons généralement en créant leur nouvelle instance directement dans le presenter dans les [#Méthodes Factory] `createComponent…()`. Mais transmettre toutes les dépendances de tous les composants au presenter pour ensuite les transmettre aux composants est lourd. Et tout ce code écrit… - -La question logique est, pourquoi ne pas simplement enregistrer le composant comme un service classique, le passer au presenter et ensuite le retourner dans la méthode `createComponent…()` ? Une telle approche est cependant inappropriée, car nous voulons pouvoir créer le composant plusieurs fois si nécessaire. - -La solution correcte est d'écrire une factory pour le composant, c'est-à-dire une classe qui nous créera le composant : - -```php -class PollControlFactory -{ - public function __construct( - private PollFacade $facade, - ) { - } - - public function create(int $id): PollControl - { - return new PollControl($id, $this->facade); - } -} -``` - -Nous enregistrons cette factory dans notre conteneur dans la configuration : - -```neon -services: - - PollControlFactory -``` - -et enfin, nous l'utilisons dans notre presenter : - -```php -class PollPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private PollControlFactory $pollControlFactory, - ) { - } - - protected function createComponentPollControl(): PollControl - { - $pollId = 1; // nous pouvons passer notre paramètre - return $this->pollControlFactory->create($pollId); - } -} -``` - -Ce qui est génial, c'est que Nette DI peut [générer |dependency-injection:factory] de telles factories simples, donc au lieu de tout son code, il suffit d'écrire seulement son interface : - -```php -interface PollControlFactory -{ - public function create(int $id): PollControl; -} -``` - -Et c'est tout. Nette implémente intérieurement cette interface et la transmet au presenter, où nous pouvons déjà l'utiliser. Il ajoute magiquement le paramètre `$id` et l'instance de la classe `PollFacade` à notre composant. - - -Composants en profondeur -======================== - -Les composants dans Nette Application représentent des parties réutilisables d'une application web que nous insérons dans les pages et auxquelles ce chapitre entier est d'ailleurs consacré. Quelles sont exactement les capacités d'un tel composant ? - -1) il est rendable dans un template -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 sauvegarder son état dans l'URL (paramètres persistants) -4) il a la capacité de réagir aux actions de l'utilisateur (signaux) -5) il crée une structure hiérarchique (où la racine est le presenter) - -Chacune de ces fonctions est assurée par l'une des classes de la lignée d'héritage. Le rendu (1 + 2) est géré par [api:Nette\Application\UI\Control], l'intégration dans le [cycle de vie |presenters#Cycle de vie du presenter] (3, 4) par la classe [api:Nette\Application\UI\Component] et la création d'une structure hiérarchique (5) par les classes [Container et Component |component-model:]. - -``` -Nette\ComponentModel\Component { IComponent } -| -+- Nette\ComponentModel\Container { IContainer } - | - +- Nette\Application\UI\Component { SignalReceiver, StatePersistent } - | - +- Nette\Application\UI\Control { Renderable } - | - +- Nette\Application\UI\Presenter { IPresenter } -``` - - -Cycle de vie du composant -------------------------- - -[* lifecycle-component.svg *] *** *Cycle de vie du composant* .<> - - -Validation des paramètres persistants -------------------------------------- - -Les valeurs des [#Paramètres persistants] reçues de l'URL sont écrites dans les propriétés par la méthode `loadState()`. Celle-ci vérifie également si le type de données indiqué pour la propriété correspond, sinon elle répond par une erreur 404 et la page ne s'affiche pas. - -Ne faites jamais confiance aveuglément aux paramètres persistants, car ils peuvent être facilement modifiés par l'utilisateur dans l'URL. Voici comment nous vérifions, par exemple, si le numéro de page `$this->page` est supérieur à 0. Une bonne approche consiste à redéfinir la méthode `loadState()` mentionnée : - -```php -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; - - public function loadState(array $params): void - { - parent::loadState($params); // ici $this->page est défini - // suit la vérification personnalisée de la valeur : - if ($this->page < 1) { - $this->error(); - } - } -} -``` - -Le processus inverse, c'est-à-dire la collecte des valeurs des propriétés persistantes, est géré par la méthode `saveState()`. - - -Raccordement au presenter -------------------------- - -Au moment où un composant devient partie de la hiérarchie du presenter, les callbacks stockés dans son tableau `$onAnchor` sont invoqués. À partir de là, le composant a le presenter à disposition, peut créer des liens en toute sécurité, lire les paramètres persistants, etc. - -```php -$control->onAnchor[] = function ($control): void { - // le composant a maintenant le presenter à disposition -}; -``` - - -Signaux en profondeur ---------------------- - -Un signal provoque un rechargement de la page exactement comme lors de la requête initiale (sauf s'il est appelé via AJAX) et appelle la méthode `signalReceived($signal)`, dont l'implémentation par défaut dans la classe `Nette\Application\UI\Component` tente d'appeler une méthode composée des mots `handle{signal}`. Le traitement ultérieur dépend de l'objet donné. Les objets qui héritent de `Component` (c'est-à-dire `Control` et `Presenter`) réagissent en essayant d'appeler la méthode `handle{signal}` avec les paramètres appropriés. - -En d'autres termes : la définition de la fonction `handle{signal}` est prise, ainsi que tous les paramètres qui sont arrivés avec la requête, et les paramètres de l'URL sont substitués aux arguments par nom, puis on tente d'appeler la méthode donnée. Par exemple, la valeur du paramètre `id` dans l'URL est passée comme paramètre `$id`, `something` de l'URL est passé comme `$something`, etc. Et si la méthode n'existe pas, la méthode `signalReceived` lève une [exception |api:Nette\Application\UI\BadSignalException]. - -Outre les paramètres de l'URL, un signal lit aussi les paramètres envoyés dans le **corps POST de la requête**. C'est bien pratique, car les signaux sont souvent invoqués depuis JavaScript, où il est naturel d'envoyer les données par la méthode POST. Si toutefois un paramètre du même nom arrive à la fois de l'URL et du corps POST, c'est la valeur **de l'URL qui l'emporte**. Évitez donc de donner à un champ POST le même nom qu'à un paramètre d'URL ou de route, sans quoi la valeur de l'URL l'écraserait en silence. Les paramètres des signaux partagent un espace commun avec les paramètres d'action et les paramètres persistants, voyez [Espace de paramètres partagé |presenters#Espace de paramètres partagé]. - -Un signal peut être reçu par n'importe quel composant, presenter ou objet qui implémente l'interface `SignalReceiver` et est connecté à l'arbre des composants. - -Les principaux destinataires des signaux seront les `Presenters` et les composants visuels héritant de `Control`. Un signal doit servir de signe à un objet qu'il doit faire quelque chose - un sondage doit compter un vote d'un utilisateur, un bloc d'actualités doit se déplier et afficher deux fois plus d'actualités, un formulaire a été soumis et doit traiter les données, etc. - -Nous créons l'URL pour un signal à l'aide de la méthode [Component::link() |api:Nette\Application\UI\Component::link()]. Comme paramètre `$destination`, nous passons la chaîne `{signal}!` et comme `$args`, un tableau d'arguments que nous voulons passer au signal. Le signal est toujours appelé sur le presenter et l'action actuels avec les paramètres actuels ; les paramètres du signal sont simplement ajoutés. De plus, le **paramètre `?do`, qui spécifie le signal**, est ajouté au début. - -Son format est soit `{signal}`, soit `{signalReceiver}-{signal}`. `{signalReceiver}` est le nom du composant dans le presenter. C'est pourquoi un trait d'union ne peut pas être utilisé dans le nom d'un composant - il est utilisé pour séparer le nom du composant et le signal, mais il est possible d'imbriquer plusieurs composants de cette manière. - -La méthode [isSignalReceiver()|api:Nette\Application\UI\Presenter::isSignalReceiver()] vérifie si le composant (premier argument) est le destinataire du signal (deuxième argument). Nous pouvons omettre le deuxième argument - il vérifie alors si le composant est le destinataire de n'importe quel signal. On peut passer `true` comme deuxième paramètre pour vérifier si non seulement le composant spécifié est le destinataire, mais aussi n'importe lequel de ses descendants. - -À n'importe quelle étape précédant `handle{signal}`, nous pouvons exécuter le signal manuellement en appelant la méthode [processSignal()|api:Nette\Application\UI\Presenter::processSignal()], qui se charge de traiter le signal - elle prend le composant désigné comme destinataire du signal (s'il n'y a pas de destinataire spécifié, c'est le presenter lui-même) et lui envoie le signal. - -Exemple : - -```php -if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) { - $this->processSignal(); -} -``` - -Le signal est ainsi exécuté prématurément et ne sera plus appelé à nouveau. diff --git a/application/fr/configuration.texy b/application/fr/configuration.texy deleted file mode 100644 index ec195628c6..0000000000 --- a/application/fr/configuration.texy +++ /dev/null @@ -1,186 +0,0 @@ -Configuration des applications -****************************** - -.[perex] -Aperçu des options de configuration pour les applications Nette. - - -Application -=========== - -```neon -application: - # afficher le panneau "Nette Application" dans Tracy BlueScreen ? - debugger: ... # (bool) par défaut true - - # l'error-presenter sera-t-il appelé en cas d'erreur ? - # n'a d'effet qu'en mode développement - catchExceptions: ... # (bool) par défaut true - - # nom de l'error-presenter - errorPresenter: Error # (string|array) par défaut 'Nette:Error' - - # définit les alias pour les presenters et les actions - aliases: ... - - # définit les règles pour traduire le nom du presenter en classe - mapping: ... - - # les liens invalides ne génèrent pas d'avertissement ? - # n'a d'effet qu'en mode développement - silentLinks: ... # (bool) par défaut false -``` - -Depuis la version 3.2 de `nette/application`, il est possible de définir une paire d'error-presenters : - -```neon -application: - errorPresenter: - 4xx: Error4xx # pour l'exception Nette\Application\BadRequestException - 5xx: Error5xx # pour les autres exceptions -``` - -Les séparer est utile, car les deux situations sont fondamentalement différentes. Une `BadRequestException` (codes 4xx) signifie que l'application va bien et que le visiteur a simplement demandé quelque chose qui n'existe pas. Vous pouvez donc y employer un presenter complet, qui affiche un message avenant dans le layout de votre site. À l'inverse, une erreur 5xx signifie que quelque chose s'est cassé dans l'application, sans qu'on sache quoi. Gardez le presenter 5xx aussi minimal que possible, pour que rien d'autre ne puisse échouer pendant son rendu : idéalement, il ne devrait toucher ni à la base de données, ni au layout, ni à l'utilisateur connecté. - -L'option `silentLinks` détermine comment Nette se comporte en mode développement lorsque la génération d'un lien échoue (par exemple, parce que le presenter n'existe pas, etc.). La valeur par défaut `false` signifie que Nette lèvera une erreur `E_USER_WARNING`. La définir sur `true` supprimera ce message d'erreur. En environnement de production, `E_USER_WARNING` est toujours levé. Ce comportement peut également être influencé en définissant la variable du presenter [$invalidLinkMode |creating-links#Liens invalides]. - -Les [alias simplifient la création de liens |creating-links#Alias] vers les presenters fréquemment utilisés. - -Le [mapping définit les règles |directory-structure#Mapping des presenters] selon lesquelles le nom de la classe est dérivé du nom du presenter. - - -Enregistrement automatique des presenters ------------------------------------------ - -Nette ajoute automatiquement les presenters en tant que services au conteneur DI, ce qui accélère considérablement leur création. La manière dont Nette trouve les presenters peut être configurée : - -```neon -application: - # rechercher les presenters dans la class map de Composer ? - scanComposer: ... # (bool) par défaut true - - # masque auquel le nom de la classe et du fichier doit correspondre - scanFilter: ... # (string) par défaut '*Presenter' - - # dans quels répertoires rechercher les presenters ? - scanDirs: # (string[]|false) par défaut '%appDir%' - - %vendorDir%/mymodule -``` - -Les répertoires spécifiés dans `scanDirs` ne remplacent pas la valeur par défaut `%appDir%`, mais la complètent, donc `scanDirs` contiendra les deux chemins `%appDir%` et `%vendorDir%/mymodule`. Si nous voulions omettre le répertoire par défaut, nous utiliserions un [point d'exclamation |dependency-injection:configuration#Fusion], qui écrase la valeur : - -```neon -application: - scanDirs!: - - %vendorDir%/mymodule -``` - -L'analyse des répertoires peut être désactivée en spécifiant la valeur `false`. Les presenters ne sont alors plus enregistrés comme services : on ne peut donc plus les ajuster via la section [decorator |dependency-injection:configuration#Decorator] et leur création est plus lente. Nous ne recommandons pas de supprimer complètement l'enregistrement automatique, car cela réduirait les performances de l'application. - - -Templates Latte -=============== - -Ce paramètre permet d'influencer globalement le comportement de Latte dans les composants et les presenters. - -```neon -latte: - # afficher le panneau Latte dans la barre Tracy pour le template principal (true) ou pour tous les composants (all) ? - debugger: ... # (true|false|'all') activé si Tracy est disponible (mode développement uniquement) - - # génère les templates avec l'en-tête declare(strict_types=1) - strictTypes: ... # (bool) par défaut false - - # active le mode [parseur strict |latte:develop#strict mode] - strictParsing: ... # (bool) par défaut false - - # limite la portée des variables au corps de la boucle - scopedLoopVariables: ... # (bool) par défaut false - - # supprime l'indentation due à l'imbrication dans les balises paires - dedent: ... # (bool) par défaut false - - # active le [contrôle du code généré |latte:develop#Checking Generated Code] - phpLinter: ... # (string) par défaut null - - # définit la locale - locale: cs_CZ # (string) par défaut null - - # classe de l'objet $this->template - templateClass: App\MyTemplateClass # par défaut Nette\Bridges\ApplicationLatte\DefaultTemplate -``` - -Vous pouvez ajouter de nouvelles [extensions |latte:extending-latte#Latte Extension] avec : - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - - -Routage -======= - -Paramètres de base : - -```neon -routing: - # afficher le panneau de routage dans la barre Tracy ? - debugger: ... # (bool) par défaut true - - # sérialise le routeur dans le conteneur DI - cache: ... # (bool) par défaut false -``` - -Le routage est généralement défini dans la classe [RouterFactory |routing#Collection de routes]. Alternativement, les routes peuvent également être définies dans la configuration en utilisant des paires `masque: action`, mais cette méthode n'offre pas une aussi grande variété de paramètres : - -```neon -routing: - routes: - 'detail/': Admin:Home:default - '/': Front:Home:default -``` - - -Constantes -========== - -Création de constantes PHP. - -```neon -constants: - Foobar: 'baz' -``` - -Après le démarrage de l'application, la constante `Foobar` sera créée. - -.[note] -Les constantes ne doivent pas servir de variables globales. Pour transmettre des valeurs aux objets, utilisez l'[injection de dépendances |dependency-injection:passing-dependencies]. - - -PHP -=== - -Configuration des directives PHP. Un aperçu de toutes les directives se trouve sur [php.net |https://www.php.net/manual/en/ini.list.php]. - -```neon -php: - date.timezone: Europe/Prague -``` - - -Services DI -=========== - -Ces services sont ajoutés au conteneur DI : - -| Nom | Type | Description -|----------------------------|---------------------------------------------------|----------------------------------------- -| `application.application` | [api:Nette\Application\Application] | [lanceur de toute l'application |how-it-works#Nette Application] -| `application.linkGenerator` | [api:Nette\Application\LinkGenerator] | [LinkGenerator |creating-links#LinkGenerator] -| `application.presenterFactory` | [api:Nette\Application\IPresenterFactory] | factory de presenters -| `application.###` | [api:Nette\Application\UI\Presenter] | presenters individuels -| `routing.router` | [api:Nette\Routing\Router] | routeur -| `latte.latteFactory` | [api:Nette\Bridges\ApplicationLatte\LatteFactory] | factory de l'objet `Latte\Engine` -| `latte.templateFactory` | [api:Nette\Application\UI\TemplateFactory] | factory pour [`$this->template` |templates] diff --git a/application/fr/creating-links.texy b/application/fr/creating-links.texy deleted file mode 100644 index 26f7af2fcd..0000000000 --- a/application/fr/creating-links.texy +++ /dev/null @@ -1,319 +0,0 @@ -Création de liens URL -********************* - -
    - -Créer des liens dans Nette est aussi simple que de pointer du doigt. Il suffit de viser et le framework fait tout le travail pour vous. Nous allons montrer : - -- comment créer des liens dans les templates et ailleurs -- comment distinguer un lien vers la page actuelle -- que faire des liens invalides - -
    - - -Grâce au [routage bidirectionnel |routing], vous n'aurez jamais à écrire en dur les URL de votre application dans les templates ou le code, URL qui pourraient changer plus tard, ou à les composer de manière compliquée. Dans le lien, il suffit d'indiquer le presenter et l'action, de passer d'éventuels paramètres, et le framework générera l'URL lui-même. En fait, c'est très similaire à l'appel d'une fonction. Vous allez adorer ça. - - -Dans le template du presenter -============================= - -Le plus souvent, nous créons des liens dans les templates et l'attribut `n:href` est un excellent assistant : - -```latte -détail -``` - -Notez qu'au lieu de l'attribut HTML `href`, nous avons utilisé un [n:attribut |latte:syntax#n:attributs] `n:href`. Sa valeur n'est alors pas une URL, comme ce serait le cas avec l'attribut `href`, mais le nom du presenter et de l'action. - -Cliquer sur le lien est, pour simplifier, quelque chose comme appeler la méthode `ProductPresenter::renderShow()`. Et si elle a des paramètres dans sa signature, nous pouvons l'appeler avec des arguments : - -```latte -détail du produit -``` - -Il est également possible de passer des paramètres nommés. Le lien suivant passe le paramètre `lang` avec la valeur `cs` : - -```latte -détail du produit -``` - -Si la méthode `ProductPresenter::renderShow()` n'a pas `$lang` dans sa signature, elle peut récupérer la valeur du paramètre en utilisant `$lang = $this->getParameter('lang')` ou depuis une [propriété |presenters#Paramètres de la requête]. - -Si les paramètres sont stockés dans un tableau, ils peuvent être développés avec l'opérateur `...` : - -```latte -{var $args = [$product->id, lang => cs]} -détail du produit -``` - -Les liens transmettent également automatiquement les [paramètres persistants |presenters#Paramètres persistants]. - -L'attribut `n:href` est très pratique pour les balises HTML ``. Si nous voulons afficher le lien ailleurs, par exemple dans du texte, nous utilisons `{link}` : - -```latte -L'adresse est : {link Home:default} -``` - - -Dans le code -============ - -Pour créer un lien dans le presenter, la méthode `link()` est utilisée : - -```php -$url = $this->link('Product:show', $product->id); -``` - -Les paramètres peuvent également être passés via un tableau, où des paramètres nommés peuvent également être spécifiés : - -```php -$url = $this->link('Product:show', [$product->id, 'lang' => 'cs']); -``` - -Les liens peuvent également être créés sans presenter, c'est à cela que sert [#LinkGenerator] et sa méthode `link()`. - -Il arrive que vous ayez besoin de créer un lien maintenant, mais de ne générer l'URL réelle que plus tard. La méthode `lazyLink()` est là pour ça : elle retourne un objet `Nette\Application\UI\Link`. L'avantage est que vous pouvez transmettre cet objet, par exemple à un template, et ajuster encore ses paramètres avec la méthode `setParameter()` avant qu'il ne soit rendu. L'URL elle-même n'est assemblée qu'à la conversion de l'objet en chaîne : - -```php -$link = $this->lazyLink('Product:show', $id); -// ... -echo $link; // l'URL n'est générée qu'ici -``` - - -Liens vers un presenter -======================= - -Si la cible du lien est un presenter et une action, la syntaxe est la suivante : - -``` -[//] [[[[:]module:]presenter:]action | this] [#fragment] -``` - -Ce format est pris en charge par toutes les balises Latte et toutes les méthodes du presenter qui fonctionnent avec des liens, c'est-à-dire `n:href`, `{link}`, `{plink}`, `link()`, `lazyLink()`, `isLinkCurrent()`, `redirect()`, `redirectPermanent()`, `forward()`, `canonicalize()` et aussi [#LinkGenerator]. Ainsi, même si `n:href` est utilisé dans les exemples, n'importe laquelle de ces fonctions pourrait être là. - -La forme de base est donc `Presenter:action` : - -```latte -page d'accueil -``` - -Si nous lions vers une action du presenter actuel, nous pouvons omettre son nom : - -```latte -page d'accueil -``` - -Si la cible est l'action `default`, nous pouvons l'omettre, mais les deux-points doivent rester : - -```latte -page d'accueil -``` - -Les liens peuvent également pointer vers d'autres [modules |directory-structure#Presenters et templates]. Ici, les liens sont distingués comme relatifs à un sous-module imbriqué, ou absolus. Le principe est analogue aux chemins sur disque, sauf que les deux-points remplacent les barres obliques. Supposons que le presenter actuel fasse partie du module `Front`, alors nous écririons : - -```latte -lien vers Front:Shop:Product:show -lien vers Admin:Product:show -``` - -Un cas spécial est un lien [vers soi-même |#Lien vers la page actuelle], où nous spécifions `this` comme cible. - -```latte -rafraîchir -``` - -Nous pouvons lier vers une partie spécifique de la page via un fragment après le signe dièse `#` : - -```latte -lien vers Home:default et le fragment #main -``` - -.{data-version:3.3.0} -Le fragment peut aussi être défini dynamiquement, comme argument portant la clé `#`. Sa valeur est encodée automatiquement et l'emporte sur le fragment indiqué dans la destination : - -```php -$this->link('Home:default', ['#' => $fragment]); -``` - - -Chemins absolus -=============== - -Les liens générés à l'aide de `link()` ou `n:href` sont toujours des chemins absolus (c'est-à-dire qu'ils commencent par `/`), mais pas des URL absolues avec protocole et domaine comme `https://domain`. - -Pour générer une URL absolue, ajoutez deux barres obliques au début (par ex. `n:href="//Home:"`). Ou vous pouvez configurer le presenter pour ne générer que des liens absolus en définissant `$this->absoluteUrls = true`. - -Le filtre `|absoluteUrl` peut également servir dans le template à convertir un chemin relatif en chemin absolu. - - -Lien vers la page actuelle -========================== - -La cible `this` crée un lien vers la page actuelle : - -```latte -rafraîchir -``` - -En même temps, tous les paramètres spécifiés dans la signature de la méthode `action()` ou `render()` sont également transmis, si `action()` n'est pas définie. Donc, si nous sommes sur la page `Product:show` avec `id: 123`, le lien vers `this` transmettra également ce paramètre. - -Bien sûr, il est possible de spécifier les paramètres directement : - -```latte -rafraîchir -``` - -La fonction `isLinkCurrent()` vérifie si la cible du lien est identique à la page actuelle. Cela peut être utilisé, par exemple, dans un template pour distinguer les liens, etc. - -Les paramètres sont les mêmes que pour la méthode `link()`, mais il est en plus possible de spécifier un caractère générique `*` à la place d'une action spécifiques, ce qui signifie n'importe quelle action du presenter donné. - -```latte -{if !isLinkCurrent('Admin:login')} - Connectez-vous -{/if} - -
  • - ... -
  • -``` - -En combinaison avec `n:href` dans un seul élément, une forme abrégée peut être utilisée : - -```latte -... -``` - -Le caractère générique `*` ne peut être utilisé qu'à la place de l'action, pas du presenter. - -Pour vérifier si nous sommes dans un certain module ou son sous-module, utilisez la méthode `isModuleCurrent(moduleName)`. - -```latte -
  • - ... -
  • -``` - - -Changer la base des liens .{data-version:3.2.7} -=============================================== - -Par défaut, les liens relatifs sont dérivés du presenter courant. On peut changer cela avec `{linkBase}` : - -```latte -{linkBase Admin:Dashboard} -product detail -``` - -Le lien mènera vers `Admin:Dashboard:Product:show`. Seuls les liens relatifs sont concernés : les liens absolus commençant par un deux-points et les liens vers le presenter courant (`this`, `show`) restent inchangés. - -`{linkBase}` s'applique à tout le template et se révèle particulièrement utile dans les templates de layout, où il garantit des liens cohérents quel que soit le presenter appelant. -La balise doit être placée au début du template, sinon elle lève une `CompileException`. - - -Liens vers un signal -==================== - -La cible d'un lien ne doit pas nécessairement être seulement un presenter et une action, mais aussi un [signal |components#Signal] (ils appellent la méthode `handle()`). La syntaxe est alors la suivante : - -``` -[//] [sub-component:]signal! [#fragment] -``` - -Le signal est donc distingué par un point d'exclamation : - -```latte -signal -``` - -Il est également possible de créer un lien vers le signal d'un sous-composant (ou sous-sous-composant) : - -```latte -signal -``` - - -Liens dans un composant -======================= - -Parce que les [composants|components] sont des unités autonomes et réutilisables qui ne devraient avoir aucun lien avec les presenters environnants, les liens fonctionnent un peu différemment ici. L'attribut Latte `n:href` et la balise `{link}` ainsi que les méthodes de composant comme `link()` et autres considèrent la cible du lien **toujours comme le nom d'un signal**. Par conséquent, il n'est même pas nécessaire d'inclure un point d'exclamation : - -```latte -signal, pas une action -``` - -Si nous voulions lier vers des presenters dans le template d'un composant, nous utiliserions la balise `{plink}` : - -```latte -accueil -``` - -ou dans le code - -```php -$this->getPresenter()->link('Home:default') -``` - - -Alias .{data-version:v3.2.2} -============================ - -Parfois, il peut être utile d'attribuer un alias facile à mémoriser à une paire Presenter:action. Par exemple, nommer la page d'accueil `Front:Home:default` simplement `home` ou `Admin:Dashboard:default` comme `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 ensuite écrits en utilisant un arobase, par exemple : - -```latte -administration -``` - -Ils sont également pris en charge dans toutes les méthodes fonctionnant avec des liens, comme `redirect()` et similaires. - - -Liens invalides -=============== - -Il peut arriver que nous créions un lien invalide - soit parce qu'il mène à un presenter inexistant, soit parce qu'il passe plus de paramètres que la méthode cible n'en accepte dans sa signature, soit lorsqu'une URL ne peut pas être générée pour l'action cible. La manière de traiter les liens invalides est déterminée par la variable statique `Presenter::$invalidLinkMode`. Elle peut prendre une combinaison de ces valeurs (constantes) : - -- `Presenter::InvalidLinkSilent` - mode silencieux, le caractère # est retourné comme URL -- `Presenter::InvalidLinkWarning` - un avertissement E_USER_WARNING est généré, qui sera enregistré en mode production, mais n'interrompra pas l'exécution du script -- `Presenter::InvalidLinkTextual` - avertissement visuel, affiche l'erreur directement dans le lien -- `Presenter::InvalidLinkException` - une exception InvalidLinkException est levée - -Le paramètre par défaut est `InvalidLinkWarning` en mode production et `InvalidLinkWarning | InvalidLinkTextual` en mode développement. `InvalidLinkWarning` en environnement de production ne provoque pas l'interruption du script, mais l'avertissement sera enregistré. En environnement de développement, il est intercepté par [Tracy |tracy:] et affiche un écran bleu. `InvalidLinkTextual` fonctionne en retournant un message d'erreur comme URL, commençant par les caractères `#error:`. Pour rendre ces liens visibles au premier coup d'œil, ajoutons à notre CSS : - -```css -a[href^="#error:"] { - background: red; - color: white; -} -``` - -Si nous ne voulons pas que des avertissements soient produits en environnement de développement, nous pouvons définir le mode silencieux directement dans la [configuration|configuration]. - -```neon -application: - silentLinks: true -``` - - -LinkGenerator -============= - -Comment créer des liens avec un confort similaire à celui de la méthode `link()`, mais sans la présence d'un presenter ? C'est là qu'intervient [api:Nette\Application\LinkGenerator]. - -LinkGenerator est un service que vous pouvez vous faire passer via le constructeur et ensuite créer des liens avec sa méthode `link()`. - -Il y a une différence par rapport aux presenters. LinkGenerator crée tous les liens directement comme des URL absolues. De plus, il n'y a pas de "presenter actuel", il n'est donc pas possible de spécifier uniquement le nom de l'action comme cible `link('default')` ou d'utiliser des chemins relatifs vers les modules. - -Les liens invalides lèvent toujours `Nette\Application\UI\InvalidLinkException`. diff --git a/application/fr/directory-structure.texy b/application/fr/directory-structure.texy deleted file mode 100644 index 1c9f1a4f77..0000000000 --- a/application/fr/directory-structure.texy +++ /dev/null @@ -1,526 +0,0 @@ -Structure des répertoires de l'application -****************************************** - -
    - -Comment concevoir une structure de répertoires claire et évolutive pour les projets Nette Framework ? Nous vous montrerons les meilleures pratiques qui vous aideront à organiser votre code. Vous apprendrez : - -- comment **diviser logiquement** l'application en répertoires -- comment concevoir la structure pour qu'elle **évolue bien** avec 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'impose aucune structure spécifique. Il est conçu pour être facilement adaptable à tous les besoins et préférences. - - -Structure de base du projet -=========================== - -Bien que Nette Framework ne dicte aucune structure de répertoires fixe, il existe une disposition par défaut éprouvée sous la forme du [Web Project|https://github.com/nette/web-project] : - -/--pre -web-project/ -├── app/ ← répertoire de l'application -├── assets/ ← fichiers SCSS, JS, images..., alternativement resources/ -├── bin/ ← scripts pour la ligne de commande -├── config/ ← configuration -├── log/ ← erreurs journalisées -├── temp/ ← fichiers temporaires, cache -├── tests/ ← tests -├── vendor/ ← bibliothèques installées par Composer -└── www/ ← répertoire public (document-root) -\-- - -Vous pouvez modifier cette structure librement selon vos besoins - renommer ou déplacer des dossiers. Ensuite, il suffit de modifier les chemins relatifs vers les répertoires dans le fichier `Bootstrap.php` et éventuellement `composer.json`. Rien de plus n'est nécessaire, pas de reconfiguration complexe, pas de changement de constantes. 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 -================================ - -Lorsque vous explorez un nouveau projet pour la première fois, vous devriez pouvoir vous y orienter rapidement. Imaginez que vous ouvrez le répertoire `app/Model/` et voyez cette structure : - -/--pre -app/Model/ -├── Services/ -├── Repositories/ -└── Entities/ -\-- - -Vous en déduisez seulement que le projet utilise des services, des dépôts et des entités. Vous n'apprenez rien sur le but réel de l'application. - -Regardons une autre approche - **l'organisation par domaines** : - -/--pre -app/Model/ -├── Cart/ -├── Payment/ -├── Order/ -└── Product/ -\-- - -Ici, c'est différent - au premier coup d'œil, il est clair qu'il s'agit d'une boutique en ligne. Les noms mêmes des répertoires révèlent ce que l'application fait - elle travaille avec les paiements, les commandes et les produits. - -La première approche (organisation par type de classe) pose en pratique de nombreux problèmes : 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 organiserons par domaines. - - -Espaces de noms ---------------- - -Il est d'usage que la structure des répertoires corresponde aux espaces de noms dans l'application. Cela signifie que l'emplacement physique des fichiers correspond à leur namespace. Par exemple, une classe située dans `app/Model/Product/ProductRepository.php` devrait avoir le namespace `App\Model\Product`. Ce principe aide à s'orienter dans le code et simplifie l'autoloading. - - -Singulier vs pluriel dans les noms ----------------------------------- - -Notez que pour les répertoires principaux de l'application, nous utilisons le singulier : `app`, `config`, `log`, `temp`, `www`. De même à l'intérieur de l'application : `Model`, `Core`, `Presentation`. C'est parce que chacun d'eux représente un concept cohérent unique. - -De même, par exemple, `app/Model/Product` représente tout ce qui concerne les produits. Nous ne l'appellerons pas `Products`, car ce n'est pas un dossier plein de produits (il y aurait des fichiers `nokia.php`, `samsung.php`). C'est un namespace contenant des classes pour travailler avec les produits - `ProductRepository.php`, `ProductService.php`. - -Le dossier `app/Tasks` est au pluriel car il contient un ensemble de scripts exécutables distincts - `CleanupTask.php`, `ImportTask.php`. Chacun d'eux est une unité distincte. - -Pour la cohérence, nous recommandons d'utiliser : -- Le singulier pour un namespace représentant une unité fonctionnelle (même s'il travaille avec plusieurs entités) -- Le pluriel pour les collections d'unités distinctes -- En cas d'incertitude ou si vous ne voulez pas y penser, choisissez le singulier - - -Répertoire public `www/` -======================== - -Ce répertoire est le seul accessible depuis le web (appelé document-root). Vous pouvez souvent rencontrer le nom `public/` au lieu de `www/` - c'est juste une question de convention et cela n'affecte pas la fonctionnalité du framework. Le répertoire contient : -- Le [point d'entrée |bootstrapping#index.php] de l'application `index.php` -- Le fichier `.htaccess` avec les règles pour mod_rewrite (pour Apache) -- Les fichiers statiques (CSS, JavaScript, images) -- Les fichiers uploadés - -Pour une sécurité correcte de l'application, il est essentiel d'avoir le [document-root correctement configuré |nette:troubleshooting#Comment changer ou supprimer le répertoire www de l'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 devraient pas être accessibles publiquement. - - -Répertoire applicatif `app/` -============================ - -C'est le répertoire principal contenant le code de l'application. Structure de base : - -/--pre -app/ -├── Core/ ← questions d'infrastructure -├── Model/ ← logique métier -├── Presentation/ ← presenters et templates -├── Tasks/ ← scripts de commande -└── Bootstrap.php ← classe de démarrage de l'application -\-- - -`Bootstrap.php` est la [classe de démarrage de l'application|bootstrapping] qui initialise l'environnement, charge la configuration et crée le conteneur DI. - -Examinons maintenant plus en détail les différents sous-répertoires. - - -Presenters et templates -======================= - -La partie présentation de l'application se trouve dans le répertoire `app/Presentation`. Une alternative est le court `app/UI`. C'est l'endroit pour tous les presenters, leurs templates et d'éventuelles classes auxiliaires. - -Nous organisons cette couche par domaines. Dans un projet complexe combinant une boutique en ligne, un blog et une API, la structure ressemblerait à ceci : - -/--pre -app/Presentation/ -├── Shop/ ← frontend de la boutique en ligne -│ ├── Product/ -│ ├── Cart/ -│ └── Order/ -├── Blog/ ← blog -│ ├── Home/ -│ └── Post/ -├── Admin/ ← administration -│ ├── Dashboard/ -│ └── Products/ -└── Api/ ← points de terminaison API - └── V1/ -\-- - -Inversement, pour un blog simple, nous utiliserions la division suivante : - -/--pre -app/Presentation/ -├── Front/ ← frontend du site -│ ├── Home/ -│ └── Post/ -├── Admin/ ← administration -│ ├── Dashboard/ -│ └── Posts/ -├── Error/ -└── Export/ ← RSS, sitemaps etc. -\-- - -Les dossiers comme `Home/` ou `Dashboard/` contiennent les presenters et les templates. Les dossiers comme `Front/`, `Admin/` ou `Api/` sont appelés **modules**. Techniquement, ce sont des répertoires normaux qui servent à la division logique de l'application. - -Chaque dossier avec un presenter contient un presenter du même nom et ses templates. Par exemple, le dossier `Dashboard/` contient : - -/--pre -Dashboard/ -├── DashboardPresenter.php ← presenter -└── default.latte ← template -\-- - -Cette structure de répertoires se reflète dans les espaces de noms des classes. Par exemple, `DashboardPresenter` se trouve dans le namespace `App\Presentation\Admin\Dashboard` (voir [#Mapping des presenters]) : - -```php -namespace App\Presentation\Admin\Dashboard; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -Nous faisons référence au presenter `Dashboard` à l'intérieur du module `Admin` dans l'application en utilisant la notation à deux points comme `Admin:Dashboard`. À son action `default` ensuite comme `Admin:Dashboard:default`. En cas de modules imbriqués, nous utilisons plus de deux points, par exemple `Shop:Order:Detail:default`. - - -Développement flexible de la structure --------------------------------------- - -L'un des grands avantages de cette structure est la manière élégante dont elle s'adapte aux besoins croissants du projet. Prenons comme exemple la partie générant les flux XML. Au début, nous avons une forme simple : - -/--pre -Export/ -├── ExportPresenter.php ← un presenter pour toutes les exportations -├── sitemap.latte ← template pour le sitemap -└── feed.latte ← template pour le flux RSS -\-- - -Avec le 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 est absolument fluide - 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:zbozi`). Grâce à cela, nous pouvons étendre progressivement la structure selon les besoins, le niveau d'imbrication n'est pas limité. - -Si, par exemple, dans l'administration, vous avez de nombreux presenters concernant la gestion des commandes, tels que `OrderDetail`, `OrderEdit`, `OrderDispatch`, etc., vous pouvez créer un module (dossier) `Order` à cet endroit pour une meilleure organisation, qui contiendra (les dossiers pour) les presenters `Detail`, `Edit`, `Dispatch` et autres. - - -Emplacement des templates -------------------------- - -Dans les exemples précédents, nous avons vu que les templates sont placés directement dans le dossier avec le presenter : - -/--pre -Dashboard/ -├── DashboardPresenter.php ← presenter -├── DashboardTemplate.php ← classe optionnelle pour le template -└── default.latte ← template -\-- - -Cet emplacement s'avère le plus pratique en pratique - vous avez tous les fichiers associés à portée de main. - -Alternativement, vous pouvez placer les templates dans un sous-dossier `templates/`. Nette prend en charge les deux variantes. Vous pouvez même placer les templates complètement en dehors du dossier `Presentation/`. Tout sur les options d'emplacement des templates se trouve dans le chapitre [Recherche de templates |templates#Recherche de templates]. - - -Classes auxiliaires et composants ---------------------------------- - -Aux presenters et templates s'ajoutent souvent d'autres fichiers auxiliaires. Nous les plaçons logiquement en fonction de leur portée : - -1. **Directement avec le presenter** dans le cas de composants spécifiques pour ce presenter : - -/--pre -Product/ -├── ProductPresenter.php -├── ProductGrid.php ← composant pour l'affichage des produits -└── FilterForm.php ← formulaire pour le filtrage -\-- - -2. **Pour le module** - nous recommandons d'utiliser le dossier `Accessory`, qui se place de manière claire au début de l'alphabet : - -/--pre -Front/ -├── Accessory/ -│ ├── NavbarControl.php ← composants pour le frontend -│ └── TemplateFilters.php -├── Product/ -└── Cart/ -\-- - -3. **Pour toute l'application** - dans `Presentation/Accessory/` : -/--pre -app/Presentation/ -├── Accessory/ -│ ├── LatteExtension.php -│ └── TemplateFilters.php -├── Front/ -└── Admin/ -\-- - -Ou vous pouvez placer les classes auxiliaires 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 habitudes de l'équipe. - - -Modèle - cœur de l'application -============================== - -Le modèle contient toute la logique métier de l'application. Pour son organisation, la règle s'applique à nouveau - nous structurons par domaines : - -/--pre -app/Model/ -├── Payment/ ← tout ce qui concerne les paiements -│ ├── PaymentFacade.php ← point d'entrée principal -│ ├── PaymentRepository.php -│ ├── Payment.php ← entité -├── Order/ ← tout ce qui concerne les commandes -│ ├── OrderFacade.php -│ ├── OrderRepository.php -│ ├── Order.php -└── Shipping/ ← tout ce qui concerne la livraison -\-- - -Dans le modèle, vous rencontrerez généralement ces types de classes : - -**Façades**: représentent le point d'entrée principal dans un domaine spécifique de l'application. Elles agissent comme un orchestrateur qui coordonne la coopération entre différents services afin d'implémenter des cas d'utilisation complets (comme "créer une commande" ou "traiter un paiement"). Sous sa couche d'orchestration, la façade masque les détails d'implémentation au reste de l'application, offrant ainsi une interface propre pour travailler avec le domaine donné. - -```php -class OrderFacade -{ - public function createOrder(Cart $cart): Order - { - // validation - // création de la commande - // envoi d'e-mail - // écriture dans les statistiques - } -} -``` - -**Services**: se concentrent sur une opération métier spécifique au sein du domaine. Contrairement à la façade, qui orchestre des cas d'utilisation entiers, un service implémente une logique métier spécifique (comme le calcul des prix ou le traitement des paiements). Les services sont typiquement 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 - } -} -``` - -**Dépôts (Repositories)**: assurent toute la communication avec le stockage de données, typiquement une base de données. Leur tâche est de charger et de sauvegarder des entités et d'implémenter des méthodes pour leur recherche. Le dépôt isole le reste de l'application des détails d'implémentation 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 métier dans l'application, qui ont leur propre identité et changent avec le 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 métier concernant leurs données et une logique de validation. - -```php -// Entité mappée sur la table de base de données 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, - ]); - } -} -``` - -**Objets Valeur (Value Objects)**: objets immuables représentant des valeurs sans identité propre - par exemple, un montant monétaire ou une adresse e-mail. Deux instances d'un objet valeur avec les mêmes valeurs sont considérées comme identiques. - - -Code d'infrastructure -===================== - -Le dossier `Core/` (ou aussi `Infrastructure/`) abrite 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 les services ext. - ├── Slack/ - └── Stripe/ -\-- - -Pour les petits projets, une structure plate suffit bien sûr : - -/--pre -Core/ -├── RouterFactory.php -├── Authenticator.php -└── QueueMailer.php -\-- - -Il s'agit de 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 toute l'application (mail, base de données) -- Est généralement indépendant du domaine spécifique - le cache ou le logger fonctionne de la même manière pour une boutique en ligne ou un blog. - -Vous hésitez si une certaine classe appartient ici ou au modèle ? La différence clé est que le code dans `Core/` : - -- Ne sait rien du domaine (produits, commandes, articles) -- Est généralement portable vers un autre projet -- Gère "comment ça marche" (comment envoyer un mail), pas "ce que ça fait" (quel mail envoyer) - -Exemple pour une meilleure compréhension : - -- `App\Core\MailerFactory` - crée des instances de la classe pour l'envoi d'e-mails, gère les paramètres SMTP -- `App\Model\OrderMailer` - utilise `MailerFactory` pour envoyer des e-mails concernant les commandes, connaît leurs templates et sait quand ils doivent être envoyés - - -Scripts de commande -=================== - -Les applications ont souvent besoin d'effectuer des activités 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. Pour l'exécution, des scripts simples dans le répertoire `bin/` sont utilisés, la logique d'implémentation elle-même est placée dans `app/Tasks/` (ou `app/Commands/`). - -Exemple : - -/--pre -app/Tasks/ -├── Maintenance/ ← scripts de maintenance -│ ├── CleanupCommand.php ← suppression des anciennes données -│ └── DbOptimizeCommand.php ← optimisation de la base de données -├── Integration/ ← intégration avec des systèmes externes -│ ├── ImportProducts.php ← importation depuis le système fournisseur -│ └── SyncOrders.php ← synchronisation des commandes -└── Scheduled/ ← tâches régulières - ├── NewsletterCommand.php ← envoi de newsletters - └── ReminderCommand.php ← notifications aux clients -\-- - -Qu'est-ce qui appartient au modèle et qu'est-ce qui appartient aux scripts de commande ? Par exemple, la logique pour envoyer un seul e-mail fait partie du modèle, l'envoi en masse de milliers d'e-mails appartient déjà à `Tasks/`. - -Les tâches sont généralement lancées depuis la ligne de commande ou via cron : le script de `bin/` crée le conteneur DI à l'aide de la méthode [bootConsoleApplication() |bootstrapping#Environnements Différents] et en tire le service nécessaire. Elles peuvent également être lancées via une requête HTTP, mais il faut penser à la sécurité. Le presenter qui lance la tâche doit être sécurisé, par exemple uniquement pour les utilisateurs connectés ou avec un jeton fort et un accès depuis des adresses IP autorisées. Pour les tâches longues, il est nécessaire d'augmenter la limite de temps du script et d'utiliser `session_write_close()` pour ne pas verrouiller la session. - - -Autres répertoires possibles -============================ - -En plus des répertoires de base mentionnés, vous pouvez ajouter d'autres dossiers spécialisés en fonction des besoins du projet. Jetons un coup d'œil aux plus courants et à leur utilisation : - -/--pre -app/ -├── Api/ ← logique pour l'API indépendante de la couche de présentation -├── Database/ ← scripts de migration et seeders pour les données de test -├── Components/ ← composants visuels partagés dans toute l'application -├── Event/ ← utile si vous utilisez une architecture événementielle -├── Mail/ ← templates d'e-mail et logique associée -└── Utils/ ← classes utilitaires -\-- - -Pour les composants visuels partagés utilisés dans les presenters à travers l'application, le dossier `app/Components` ou `app/Controls` peut être utilisé : - -/--pre -app/Components/ -├── Form/ ← composants de formulaire partagés -│ ├── SignInForm.php -│ └── UserForm.php -├── Grid/ ← composants pour l'affichage de données -│ └── DataGrid.php -└── Navigation/ ← éléments de navigation - ├── Breadcrumbs.php - └── Menu.php -\-- - -C'est ici que se trouvent les composants qui ont une logique plus complexe. Si vous souhaitez partager des composants entre plusieurs projets, il est conseillé de les extraire dans un paquet composer distinct. - -Dans le répertoire `app/Mail`, vous pouvez placer la gestion de la communication par e-mail : - -/--pre -app/Mail/ -├── templates/ ← templates d'e-mail -│ ├── order-confirmation.latte -│ └── welcome.latte -└── OrderMailer.php -\-- - - -Mapping des presenters -====================== - -Le mapping définit les règles pour dériver le nom de la classe à partir du nom du presenter. Nous les spécifions dans la [configuration|configuration] sous la clé `application › mapping`. - -Sur cette page, nous avons montré que nous plaçons les presenters dans le dossier `app/Presentation` (ou `app/UI`). Depuis Nette Application 3.3, c'est la convention par défaut, qu'il n'est pas nécessaire de configurer. Si vous utilisez une autre structure ou si vous voulez indiquer le mapping explicitement, le réglage par défaut correspond à cette ligne : - -```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 presenter appartiennent au namespace `App\Presentation`, afin que le presenter `Home` soit mappé sur la classe `App\Presentation\HomePresenter`. Ce que nous obtenons avec cette configuration : - -```neon -application: - mapping: App\Presentation\*Presenter -``` - -Le mapping fonctionne de telle sorte que le nom du presenter `Home` remplace l'astérisque dans le masque `App\Presentation\*Presenter`, ce qui donne le nom de classe résultant `App\Presentation\HomePresenter`. Simple ! - -Mais comme vous pouvez le voir dans les exemples de ce chapitre et d'autres, nous plaçons les classes de presenter dans des sous-répertoires éponymes, par exemple le presenter `Home` est mappé sur la classe `App\Presentation\Home\HomePresenter`. Nous obtenons cela avec la double astérisque `**` (nécessite Nette Application 3.2.3) : - -```neon -application: - mapping: App\Presentation\**Presenter -``` - -Passons maintenant au mapping des presenters dans les modules. Pour chaque module, nous pouvons définir un mapping spécifique : - -```neon -application: - mapping: - Front: App\Presentation\Front\**Presenter - Admin: App\Presentation\Admin\**Presenter - Api: App\Api\*Presenter -``` - -Selon cette configuration, le presenter `Front:Home` est mappé sur la classe `App\Presentation\Front\Home\HomePresenter`, tandis que le presenter `Api:OAuth` est mappé sur la classe `App\Api\OAuthPresenter`. - -Comme les modules `Front` et `Admin` ont un mode de mapping similaire et qu'il y aura probablement plus de modules de ce type, il est possible de créer une règle générale qui les remplacera. Une nouvelle astérisque pour le module est ainsi ajoutée au masque de classe : - -```neon -application: - mapping: - *: App\Presentation\*\**Presenter - Api: App\Api\*Presenter -``` - -Cela fonctionne également pour les structures de répertoires plus profondément imbriquées, comme par exemple le presenter `Admin:User:Edit`, le segment avec l'astérisque se répète pour chaque niveau et le résultat est la classe `App\Presentation\Admin\User\Edit\EditPresenter`. - -Une notation alternative 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 deleted file mode 100644 index 579b3e71fb..0000000000 --- a/application/fr/how-it-works.texy +++ /dev/null @@ -1,200 +0,0 @@ -Comment fonctionnent les applications ? -*************************************** - -
    - -Vous lisez en ce moment le chapitre fondateur de la documentation de Nette. Vous y apprendrez de A à Z tout le principe de fonctionnement des applications web, depuis l'instant où naît une requête jusqu'à la fin de l'exécution du script PHP. Après cette lecture, vous saurez : - -- comment tout cela fonctionne -- ce qu'est Bootstrap, un Presenter et un conteneur DI -- à quoi ressemble la structure des répertoires - -
    - - -Structure des répertoires -========================= - -Ouvrez l'exemple de squelette d'application web appelé [WebProject|https://github.com/nette/web-project] et pendant la lecture, examinez les fichiers mentionnés. - -La structure des répertoires ressemble à quelque chose comme ceci : - -/--pre -web-project/ -├── app/ ← répertoire de l'application -│ ├── Core/ ← classes de base nécessaires au fonctionnement -│ │ └── RouterFactory.php ← configuration des adresses URL -│ ├── Presentation/ ← presenters, templates & co. -│ │ ├── @layout.latte ← template de layout -│ │ └── Home/ ← répertoire du presenter Home -│ │ ├── HomePresenter.php ← classe du presenter Home -│ │ └── default.latte ← template de l'action default -│ └── Bootstrap.php ← classe de démarrage Bootstrap -├── assets/ ← ressources (SCSS, TypeScript, images sources) -├── bin/ ← scripts exécutés depuis la ligne de commande -├── config/ ← fichiers de configuration -│ ├── common.neon -│ └── services.neon -├── log/ ← erreurs journalisées -├── temp/ ← fichiers temporaires, cache, … -├── vendor/ ← bibliothèques installées par Composer -│ ├── ... -│ └── autoload.php ← autoloading de tous les paquets installés -├── www/ ← répertoire public ou document-root du projet -│ ├── assets/ ← fichiers statiques compilés (CSS, JS, images, ...) -│ ├── .htaccess ← règles mod_rewrite -│ └── index.php ← fichier initial par lequel l'application est lancée -└── .htaccess ← interdit l'accès à tous les répertoires sauf www -\-- - -Vous pouvez modifier la structure des répertoires comme vous le souhaitez, renommer ou déplacer des dossiers, elle est entièrement flexible. De plus, Nette dispose d'une autodétection intelligente et reconnaît automatiquement l'emplacement de l'application, y compris sa base d'URL. - -Pour les applications légèrement plus grandes, nous pouvons [diviser les dossiers avec les presenters et les templates en sous-répertoires |directory-structure#Presenters et templates] et les classes en espaces de noms, que nous appelons modules. - -Le répertoire `www/` représente le répertoire public ou document-root du projet. Vous pouvez le renommer sans aucune configuration supplémentaire côté application. Il suffit de [configurer l'hébergement |nette:troubleshooting#Comment changer ou supprimer le répertoire www de l'URL ?] pour que le document-root pointe vers ce répertoire. - -Vous pouvez également télécharger directement WebProject incluant Nette en utilisant [Composer |best-practices:composer] : - -```shell -composer create-project nette/web-project -``` - -Sous Linux ou macOS, définissez les [permissions d'écriture |nette:troubleshooting#Régler les permissions des répertoires] pour les répertoires `log/` et `temp/`. - -L'application WebProject est prête à être lancée, il n'y a absolument rien à configurer et vous pouvez l'afficher directement dans le navigateur en accédant au dossier `www/`. - - -Requête HTTP -============ - -Tout commence au moment où l'utilisateur ouvre une page dans le navigateur. C'est-à-dire lorsque le navigateur contacte le serveur avec une requête HTTP. La requête pointe vers un seul fichier PHP, qui se trouve dans le répertoire public `www/`, et c'est `index.php`. Supposons qu'il s'agisse d'une requête pour l'adresse `https://example.com/product/123`. Grâce à une [configuration serveur appropriée |nette:troubleshooting#Comment configurer un serveur pour des URL élégantes ?], cette URL est également associée au fichier `index.php`, qui est ensuite exécuté. - -Ses tâches sont : - -1) initialiser l'environnement -2) obtenir la factory -3) lancer l'application Nette, qui traitera la requête - -Quelle factory ? Nous ne fabriquons pas de tracteurs, mais des sites web ! Patientez, cela va s'éclaircir. - -Par "initialisation de l'environnement", nous entendons par exemple l'activation de [Tracy|tracy:], qui est un outil exceptionnel pour la journalisation ou la visualisation des erreurs. Sur un serveur de production, il journalise les erreurs, sur un serveur de développement, il les affiche directement. L'initialisation comprend donc également la décision de savoir si le site fonctionne en mode production ou développement. Pour cela, Nette utilise une [autodétection intelligente |bootstrapping#Mode Développement vs Production] : si vous lancez le site sur localhost, il fonctionne en mode développement. Vous n'avez donc rien à configurer et l'application est prête à la fois pour le développement et le déploiement en production. Ces étapes sont effectuées et décrites en détail dans le chapitre sur la [classe Bootstrap|bootstrapping]. - -Le troisième point (oui, nous avons sauté le deuxième, mais nous y reviendrons) est le lancement de l'application. Le traitement des requêtes HTTP dans Nette est géré par la classe `Nette\Application\Application` (ci-après `Application`), donc quand nous disons lancer l'application, nous entendons spécifiquement appeler la méthode `run()` sur l'objet de cette classe. - -Nette est un mentor qui vous guide vers l'écriture d'applications propres selon des méthodologies éprouvées. Et l'une de celles qui sont absolument les plus éprouvées s'appelle **l'injection de dépendances**, en abrégé DI. Pour le moment, nous ne voulons pas vous surcharger avec l'explication de la DI, il y a un [chapitre séparé|dependency-injection:introduction] pour cela, l'important est la conséquence que les objets clés nous seront généralement créés par une factory d'objets appelée **conteneur DI** (abrégé en DIC). Oui, c'est la factory dont il était question il y a un instant. Et elle nous créera aussi l'objet `Application`, c'est pourquoi nous avons d'abord besoin du conteneur. Nous l'obtenons à l'aide de la classe `Configurator` et nous lui demandons de créer l'objet `Application`, nous appelons sa méthode `run()` et l'application Nette est ainsi lancée. C'est exactement ce qui se passe dans le fichier [index.php |bootstrapping#index.php]. - - -Nette Application -================= - -La classe `Application` n'a qu'une seule tâche : répondre à la requête HTTP. - -Les applications écrites en Nette sont divisées en de nombreux presenters (dans d'autres frameworks, vous pouvez rencontrer le terme controller, c'est la même chose), qui sont des classes, dont chacune représente une page web spécifique : par ex. la page d'accueil ; un produit dans une boutique en ligne ; un formulaire de connexion ; un flux sitemap, etc. Une application peut avoir d'un à des milliers de presenters. - -`Application` commence par demander au routeur de décider à quel presenter transmettre la requête actuelle pour traitement. Le routeur détermine qui est responsable. Il regarde l'URL d'entrée `https://example.com/product/123` et sur la base de sa configuration, décide que c'est le travail, par ex., du **presenter** `Product`, auquel il demandera comme **action** l'affichage (`show`) du produit avec `id: 123`. Il est de bonne pratique de noter la paire presenter + action séparée par deux-points, comme `Product:show`. - -Le routeur a donc transformé l'URL en une paire `Presenter:action` + paramètres, dans notre cas `Product:show` + `id: 123`. À quoi ressemble un tel routeur, vous pouvez le voir dans le fichier `app/Core/RouterFactory.php` et nous le décrivons en détail dans le chapitre [Routage |Routing]. - -Continuons. `Application` connaît désormais le nom du presenter et peut poursuivre. Elle le fait en créant une instance de la classe `ProductPresenter`, qui contient le code du presenter `Product`. Plus précisément, elle demande au conteneur DI de créer le presenter, car créer les objets est son rôle. - -Le presenter peut ressembler à ceci : - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ProductRepository $repository, - ) { - } - - public function renderShow(int $id): void - { - // nous obtenons les données du modèle et les passons au template - $this->template->product = $this->repository->getProduct($id); - } -} -``` - -Le presenter prend en charge le traitement de la requête. Et la tâche est claire : effectuer l'action `show` avec `id: 123`. Ce qui, dans le langage des presenters, signifie que la méthode `renderShow()` est appelée et reçoit `123` dans le paramètre `$id`. - -Un presenter peut gérer plusieurs actions, c'est-à-dire avoir plusieurs méthodes `render()`. Mais nous recommandons de concevoir des presenters avec une seule ou le moins d'actions possible. - -Donc, la méthode `renderShow(123)` a été appelée, dont le code est un exemple fictif, mais vous pouvez y voir comment les données sont passées au template, c'est-à-dire en écrivant dans `$this->template`. - -Ensuite, le presenter retourne une réponse. Cela peut être une page HTML, une image, un document XML, l'envoi d'un fichier depuis le disque, du JSON ou même une redirection vers une autre page. Il est important que si nous ne disons pas explicitement comment répondre (ce qui est le cas de `ProductPresenter`), la réponse sera le rendu d'un template avec une page HTML. Pourquoi ? Parce que dans 99% des cas, nous voulons rendre un template, donc le presenter considère ce comportement comme celui par défaut et veut nous faciliter le travail. C'est le but de Nette. - -Nous n'avons même pas besoin de spécifier quel template rendre, il déduit le chemin lui-même. Dans le cas de l'action `show`, il essaie simplement de charger le template `show.latte` dans le répertoire de la classe `ProductPresenter`. Il essaie également de trouver le layout dans le fichier `@layout.latte` (plus de détails sur la [recherche de templates |templates#Recherche de templates]). - -Et ensuite, il rend les templates. La tâche du presenter et de toute l'application est ainsi terminée. Si le template n'existait pas, une page d'erreur 404 serait retournée. Vous en apprendrez plus sur les presenters sur la page [Presenters |Presenters]. - -[* request-flow.svg *] - -Pour être sûr, essayons de récapituler tout le processus avec une URL légèrement différente : - -1) L'URL sera `https://example.com` -2) nous démarrons l'application, le conteneur est créé et `Application::run()` est lancé -3) le routeur décode l'URL comme la paire `Home:default` -4) l'objet de la classe `HomePresenter` est créé -5) la méthode `renderDefault()` est appelée (si elle existe) -6) le template (par ex. `default.latte`) avec le layout (par ex. `@layout.latte`) est rendu - - -Vous avez peut-être rencontré de nombreux nouveaux termes maintenant, mais nous espérons qu'ils ont du sens. Créer des applications avec Nette est incroyablement simple et agréable. - - -Templates -========= - -Puisque nous parlons de templates, Nette utilise le système de templates [Latte |latte:]. C'est pourquoi les fichiers de template portent l'extension `.latte`. Latte est employé avant tout parce que c'est le système de templates le plus sûr pour PHP, et aussi le plus intuitif. Vous n'avez pas besoin d'apprendre beaucoup de nouveautés, la connaissance de PHP et de quelques balises suffit. Vous apprendrez tout dans [la documentation |templates]. - -Dans le template, des [liens |creating-links] sont créés vers d'autres presenters & actions comme ceci : - -```latte -détail du produit -``` - -Au lieu d'une URL réelle, écrivez simplement la paire connue `Presenter:action` et spécifiez d'éventuels paramètres. L'astuce réside dans `n:href`, qui indique que cet attribut sera traité par Nette. Et il générera : - -```latte -détail du produit -``` - -La génération d'URL est gérée par le routeur mentionné précédemment. En effet, les routeurs dans Nette sont exceptionnels car ils savent effectuer non seulement la transformation d'une URL en paire `Presenter:action`, mais aussi l'inverse, c'est-à-dire générer une URL à partir du nom du presenter + action + paramètres. Grâce à cela, dans Nette, vous pouvez complètement changer les formes d'URL dans toute une application terminée, sans changer un seul caractère dans le template ou le presenter. Juste en modifiant le routeur. Grâce à cela fonctionne également la canonisation, qui est une autre caractéristique unique de Nette, contribuant à un meilleur SEO (optimisation pour les moteurs de recherche) en empêchant automatiquement l'existence de contenu dupliqué sur différentes URL. De nombreux programmeurs trouvent cela impressionnant. - - -Composants interactifs -====================== - -Nous devons vous dire encore une chose sur les presenters : ils ont un système de composants intégré. Les vétérans peuvent se souvenir de quelque chose de similaire dans Delphi ou ASP.NET Web Forms ; React ou Vue.js sont basés sur quelque chose de vaguement similaire. Dans le monde des frameworks PHP, c'est une caractéristique absolument unique. - -Les composants sont des unités autonomes et réutilisables que nous insérons dans les pages (c'est-à-dire les presenters). Il peut s'agir de [formulaires |forms:in-presenter], de [datagrids |https://componette.org/contributte/datagrid/], de menus, de sondages, en fait de tout ce qu'il est judicieux d'utiliser de manière répétée. Nous pouvons créer nos propres composants ou utiliser certains de la [vaste offre |https://componette.org] de composants open source. - -Les composants influencent fondamentalement l'approche de la création d'applications. Ils vous ouvrent de nouvelles possibilités de composition de pages à partir d'unités préfabriquées. Et en plus, ils ont quelque chose en commun avec [Hollywood |components#Style Hollywood]. - - -Conteneur DI et configuration -============================= - -Le conteneur DI ou factory d'objets est le cœur de toute l'application. - -Ne vous inquiétez pas, ce n'est pas une boîte noire magique, comme on pourrait le penser d'après les lignes précédentes. En fait, c'est une classe PHP plutôt simple, générée par Nette et stockée dans le répertoire cache. Elle a beaucoup de méthodes nommées comme `createServiceAbcd()` et chacune d'elles sait comment créer et retourner un objet. Oui, il y a aussi une méthode `createServiceApplication()`, qui crée `Nette\Application\Application`, dont nous avions besoin dans le fichier `index.php` pour lancer l'application. Et il y a des méthodes créant les presenters individuels. Et ainsi de suite. - -Les objets que le conteneur DI crée sont, pour une raison quelconque, appelés services. - -Ce qui est vraiment spécial à propos de cette classe, c'est que ce n'est pas vous qui la programmez, mais le framework. Il génère réellement le code PHP et le sauvegarde sur le disque. Vous donnez simplement des instructions sur les objets que le conteneur doit savoir créer et comment précisément. Et ces instructions sont écrites dans des [fichiers de configuration |bootstrapping#Configuration du Conteneur DI], pour lesquels le format [NEON|neon:format] est utilisé et qui ont donc aussi l'extension `.neon`. - -Les fichiers de configuration servent uniquement à instruire le conteneur DI. Ainsi, par exemple, si j'indique dans la section [session |http:configuration#Session] l'option `expiration: 14 days`, alors le conteneur DI, lors de la création de l'objet `Nette\Http\Session` représentant la session, appellera sa méthode `setExpiration('14 days')` et la configuration deviendra ainsi réalité. - -Il y a tout un chapitre préparé pour vous décrivant tout ce qui peut être [configuré |nette:configuring] et comment [définir vos propres services |dependency-injection:services]. - -Dès que vous vous familiariserez un peu avec la création de services, vous rencontrerez le mot [autowiring |dependency-injection:autowiring]. C'est une astuce qui vous simplifiera incroyablement la vie. Elle sait comment passer automatiquement des objets là où vous en avez besoin (par exemple dans les constructeurs de vos classes), sans que vous ayez à faire quoi que ce soit. Vous découvrirez que le conteneur DI de Nette est un petit miracle. - - -Où aller ensuite ? -================== - -Nous avons parcouru les principes de base des applications en Nette. Pour l'instant très superficiellement, mais vous approfondirez bientôt et créerez avec le temps de merveilleuses applications web. Où continuer ? Avez-vous déjà essayé le tutoriel [Écrivons notre première application|quickstart:] ? - -En plus de ce qui a été décrit ci-dessus, Nette dispose de tout un arsenal de [classes utiles|utils:], d'une [couche de base de données|database:], etc. Essayez juste de parcourir la documentation. Ou le [blog|https://blog.nette.org]. Vous découvrirez beaucoup de choses intéressantes. - -Que le framework vous apporte beaucoup de joie 💙 diff --git a/application/fr/multiplier.texy b/application/fr/multiplier.texy deleted file mode 100644 index 0948ec3122..0000000000 --- a/application/fr/multiplier.texy +++ /dev/null @@ -1,63 +0,0 @@ -Multiplier : composants dynamiques -********************************** - -.[perex] -Outil pour la création dynamique de composants interactifs - -Partons d'un exemple typique : nous avons une liste de produits dans une boutique en ligne, et pour chacun, nous voulons afficher un formulaire pour ajouter le produit au panier. Une possibilité est d'englober toute la liste dans un seul formulaire. Cependant, [api:Nette\Application\UI\Multiplier] nous offre une manière beaucoup plus pratique. - -Multiplier permet de définir facilement une petite factory pour plusieurs composants. Il fonctionne sur le principe des composants imbriqués - chaque composant héritant de [api:Nette\ComponentModel\Container] peut contenir d'autres composants. - -.[tip] -Voir le chapitre sur le [modèle de composant |components#Composants en profondeur] dans la documentation. - -L'essence de Multiplier est qu'il agit en tant que parent capable de créer dynamiquement ses enfants à l'aide d'un callback passé dans le constructeur. Voir l'exemple : - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function () { - $form = new Nette\Application\UI\Form; - $form->addInteger('count', 'Nombre de produits :') - ->setRequired(); - $form->addSubmit('send', 'Ajouter au panier'); - return $form; - }); -} -``` - -Maintenant, nous pouvons simplement faire afficher le formulaire pour chaque produit dans le template - et chacun sera réellement un composant unique. - -```latte -{foreach $items as $item} -

    {$item->title}

    - {$item->description} - - {control "shopForm-$item->id"} -{/foreach} -``` - -L'argument passé dans la balise `{control}` est dans un format qui dit : - -1. obtenir le composant `shopForm` -2. et à partir de celui-ci, obtenir l'enfant `$item->id` - -Lors du premier appel du point **1.**, `shopForm` n'existe pas encore, donc sa factory `createComponentShopForm` est appelée. Sur le composant obtenu (instance de Multiplier), la factory du formulaire spécifique est ensuite appelée - c'est la fonction anonyme que nous avons passée à Multiplier dans le constructeur. - -Dans l'itération suivante de foreach, la méthode `createComponentShopForm` ne sera plus appelée (le composant existe), mais comme nous recherchons un autre de ses enfants (`$item->id` sera différent à chaque itération), la fonction anonyme sera à nouveau appelée et nous retournera un nouveau formulaire. - -La seule chose qui reste à faire est de s'assurer que le formulaire ajoute réellement au panier le produit qu'il doit ajouter - actuellement, le formulaire est exactement le même pour chaque produit. La propriété de Multiplier (et en général de chaque factory de composant dans Nette Framework) nous aide, à savoir que chaque factory reçoit comme premier argument le nom du composant créé. Dans notre cas, ce sera `$item->id`, ce qui est exactement l'information dont nous avons besoin. Il suffit donc de modifier légèrement la création du formulaire : - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function ($itemId) { - $form = new Nette\Application\UI\Form; - $form->addInteger('count', 'Nombre de produits :') - ->setRequired(); - $form->addHidden('itemId', $itemId); - $form->addSubmit('send', 'Ajouter au panier'); - return $form; - }); -} -``` diff --git a/application/fr/presenters.texy b/application/fr/presenters.texy deleted file mode 100644 index e479751763..0000000000 --- a/application/fr/presenters.texy +++ /dev/null @@ -1,595 +0,0 @@ -Presenters -********** - -
    - -Nous allons découvrir comment écrire des presenters et des templates en Nette. Après lecture, vous saurez : - -- comment fonctionne un presenter -- ce que sont les paramètres persistants -- comment les templates sont rendus - -
    - -[Nous savons déjà |how-it-works#Nette Application] qu'un presenter est une classe qui représente une page web spécifique d'une application, par ex. la page d'accueil ; un produit dans une boutique en ligne ; un formulaire de connexion ; un flux sitemap, etc. Une application peut avoir d'un à des milliers de presenters. Dans d'autres frameworks, on les appelle aussi controllers. - -Habituellement, par le terme presenter, on entend un descendant de la classe [api:Nette\Application\UI\Presenter], qui est adapté à la génération d'interfaces web et auquel nous nous consacrerons dans le reste de ce chapitre. Au sens général, un presenter est n'importe quel objet implémentant l'interface [api:Nette\Application\IPresenter]. - - -Cycle de vie du presenter -========================= - -La tâche du presenter est de traiter une requête et de retourner une réponse (qui peut être une page HTML, une image, une redirection, etc.). - -Donc, au début, une requête lui est transmise. Ce n'est pas directement une requête HTTP, mais un objet [api:Nette\Application\Request], dans lequel la requête HTTP a été transformée à l'aide du routeur. Nous n'interagissons généralement pas directement avec cet objet, car le presenter délègue intelligemment le traitement de la requête à d'autres méthodes, que nous allons présenter maintenant. - -[* lifecycle.svg *] *** *Cycle de vie du presenter* .<> - -L'image représente la liste des méthodes qui sont appelées successivement de haut en bas, si elles existent. Aucune d'elles n'est obligatoire, nous pouvons avoir un presenter complètement vide sans une seule méthode et construire un site web statique simple dessus. - - -`__construct()` ---------------- - -Le constructeur n'appartient pas tout à fait au cycle de vie du presenter, car il est appelé au moment de la création de l'objet. Mais nous le mentionnons en raison de son importance. Le constructeur (avec la [méthode inject|best-practices:inject-method-attribute]) sert à passer les dépendances. - -Un presenter ne devrait pas gérer la logique métier de l'application, écrire et lire dans la base de données, effectuer des calculs, etc. C'est le rôle des classes de la couche que nous appelons modèle. Par exemple, la classe `ArticleRepository` peut être responsable du chargement et de la sauvegarde des articles. Pour que le presenter puisse travailler avec elle, il la reçoit via [l'injection de dépendances |dependency-injection:passing-dependencies] : - - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articles, - ) { - } -} -``` - - -`startup()` ------------ - -Immédiatement après réception de la requête, la méthode `startup()` est appelée. Vous pouvez l'utiliser pour initialiser les propriétés, vérifier les permissions utilisateur, etc. Il est requis que la méthode appelle toujours le parent `parent::startup()`. - - -`action(args...)` .{toc: action()} --------------------------------------------------- - -Analogue à la méthode `render()`. Alors que `render()` est destinée à préparer les données pour un template spécifique qui sera ensuite rendu, `action()` traite la requête sans lien avec le rendu du template. Par exemple, elle traite les données, connecte ou déconnecte l'utilisateur, etc., puis [redirige ailleurs |#Redirection]. - -Il est important que `action()` soit appelée *avant* `render()`. Cela nous permet de modifier le cours de la requête dans la méthode d'action, par exemple en changeant le template qui sera rendu, voire la méthode `render()` qui sera appelée, à l'aide de `setView('otherView')`. - -.{data-version:3.2.3} -Vous pouvez même basculer vers une action entièrement différente avec la méthode `switch('otherAction')`. Elle interrompt la méthode courante et exécute à la place les méthodes `action()` et `render()` de la nouvelle action (et désactive la [canonisation|#Canonisation] automatique). La requête elle-même se poursuit ; seule la méthode en cours d'exécution est interrompue. - -Les paramètres de la requête sont passés à la méthode. Il est possible et recommandé de préciser leurs types, par ex. `actionShow(int $id, ?string $slug = null)`. Si le paramètre `id` est absent ou n'est pas un entier, le presenter retourne une [erreur 404 |#Erreur 404 et autres] et termine son activité. - - -`handle(args...)` .{toc: handle()} --------------------------------------------------- - -La méthode traite les signaux, que nous découvrirons dans le chapitre consacré aux [composants |components#Signal]. Elle est en effet principalement destinée aux composants et au traitement des requêtes AJAX. - -Les paramètres de la requête sont passés à la méthode, comme dans le cas de `action()`, y compris la vérification de type. - - -`beforeRender()` ----------------- - -La méthode `beforeRender`, comme son nom l'indique, est appelée avant chaque méthode `render()`. Elle est utilisée pour la configuration commune du template, passer des variables au layout, etc. - - -`render(args...)` .{toc: render()} ----------------------------------------------- - -L'endroit où nous préparons le template pour le rendu ultérieur, lui passons des données, etc. - -Les paramètres de la requête sont passés à la méthode, comme dans le cas de `action()`, y compris la vérification de type. - -```php -public function renderShow(int $id): void -{ - // nous obtenons les données du modèle et les passons au template - $this->template->article = $this->articles->getById($id); -} -``` - - -`afterRender()` ---------------- - -La méthode `afterRender`, comme son nom l'indique encore une fois, est appelée après chaque méthode `render()`. Elle est utilisée plutôt exceptionnellement. - - -`shutdown()` ------------- - -Appelée à la fin du cycle de vie du presenter. - - -Événements ----------- - -En plus des méthodes `startup()`, `beforeRender()` et `shutdown()`, qui sont appelées dans le cadre du cycle de vie du presenter, il est possible de définir d'autres fonctions appelées automatiquement. Le presenter définit ce qu'on appelle des [événements |nette:glossary#Événements], dont vous ajoutez les gestionnaires aux tableaux `$onStartup`, `$onRender` et `$onShutdown`. - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct() - { - $this->onStartup[] = function () { - // ... - }; - } -} -``` - -Les gestionnaires du tableau `$onStartup` sont appelés juste avant la méthode `startup()`, ceux de `$onRender` entre `beforeRender()` et `render()`, et enfin ceux de `$onShutdown` juste avant `shutdown()`. - - -**Un bon conseil avant de continuer :** comme vous pouvez le voir, un presenter peut gérer plusieurs actions/vues, c'est-à-dire avoir plusieurs méthodes `render()`. Nous recommandons toutefois de concevoir des presenters avec une seule action, ou le moins d'actions possible. - - -Envoi de la réponse -=================== - -La réponse d'un presenter est généralement le [rendu d'un template avec une page HTML |templates], mais elle peut aussi être l'envoi d'un fichier, de JSON ou même une redirection vers une autre page. - -À tout moment du cycle de vie, nous pouvons envoyer une réponse avec l'une des méthodes suivantes et ainsi terminer le presenter : - -- `redirect()`, `redirectPermanent()`, `redirectUrl()` et `forward()` [redirigent |#Redirection] -- `error()` termine le presenter [en raison d'une erreur |#Erreur 404 et autres] -- `sendJson($data)` termine le presenter et [envoie les données |#Envoi de JSON] au format JSON -- `sendTemplate()` termine le presenter et [rend immédiatement le template |templates] -- `sendResponse($response)` termine le presenter et envoie une [réponse personnalisée |#Réponses] -- `terminate()` termine le presenter sans réponse - -Chacune de ces méthodes met immédiatement fin au presenter en levant une exception de terminaison silencieuse `Nette\Application\AbortException`. - -Si vous n'appelez aucune de ces méthodes, le presenter procède automatiquement au rendu du template. Pourquoi ? Parce que dans 99 % des cas, nous voulons rendre un template : le presenter adopte donc ce comportement par défaut pour nous simplifier le travail. - - -Création de liens -================= - -Le presenter dispose de la méthode `link()`, à l'aide de laquelle il est possible de créer des liens URL vers d'autres presenters. Le premier paramètre est le presenter & action cible, suivi des arguments passés, qui peuvent être spécifiés sous forme de tableau : - -```php -$url = $this->link('Product:show', $id); - -$url = $this->link('Product:show', [$id, 'lang' => 'cs']); -``` - -Dans le template, les liens vers d'autres presenters & actions sont créés de cette manière : - -```latte -détail du produit -``` - -Au lieu d'une URL réelle, écrivez simplement la paire connue `Presenter:action` et spécifiez d'éventuels paramètres. L'astuce réside dans `n:href`, qui indique que cet attribut sera traité par Latte et générera une URL réelle. Dans Nette, vous n'avez donc pas du tout à penser aux URL, seulement aux presenters et aux actions. - -Plus d'informations peuvent être trouvées dans le chapitre [Création de liens URL|creating-links]. - - -Redirection -=========== - -Pour passer à un autre presenter, les méthodes `redirect()` et `forward()` sont utilisées, qui ont une syntaxe très similaire à la méthode [link() |#Création de liens]. - -La méthode `forward()` passe immédiatement au nouveau presenter sans redirection HTTP : - -```php -$this->forward('Product:show'); -``` - -Exemple de redirection dite temporaire avec le code HTTP 302 (ou 303 si la méthode de la requête actuelle est POST) : - -```php -$this->redirect('Product:show', $id); -``` - -Vous obtenez une redirection permanente avec le code HTTP 301 comme ceci : - -```php -$this->redirectPermanent('Product:show', $id); -``` - -Il est possible de rediriger vers une autre URL en dehors de l'application avec la méthode `redirectUrl()`. Le code HTTP peut être spécifié comme deuxième paramètre, la valeur par défaut est 302 (ou 303 si la méthode de la requête actuelle est POST) : - -```php -$this->redirectUrl('https://nette.org'); -``` - -La redirection termine immédiatement l'activité du presenter en levant une exception de terminaison silencieuse appelée `Nette\Application\AbortException`. - -Avant la redirection, il est possible d'envoyer des [#Messages Flash], c'est-à-dire des messages qui seront affichés dans le template après la redirection. - - -Messages Flash -============== - -Ce sont des messages informant généralement du résultat d'une opération. Une caractéristique importante des messages flash est qu'ils sont disponibles dans le template même après une redirection. Même après affichage, ils restent actifs pendant 30 secondes supplémentaires - par exemple, au cas où l'utilisateur rafraîchirait la page en raison d'une erreur de transmission - le message ne disparaîtra donc pas immédiatement. - -Il suffit d'appeler la méthode [flashMessage() |api:Nette\Application\UI\Control::flashMessage()] et le presenter se chargera de la transmettre au template. Le premier paramètre est le texte du message et le deuxième paramètre facultatif est son type (error, warning, info, etc.). La méthode `flashMessage()` retourne une instance du message flash, à laquelle des informations supplémentaires peuvent être ajoutées. - -```php -$this->flashMessage('L\'élément a été supprimé.'); -$this->redirect(/* ... */); // et nous redirigeons -``` - -Dans le template, ces messages sont disponibles dans la variable `$flashes` sous forme d'objets `stdClass`, qui contiennent les propriétés `message` (texte du message), `type` (type de message) et peuvent contenir les informations utilisateur mentionnées précédemment. Nous les rendons par exemple comme ceci : - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -Erreur 404 et autres -==================== - -Si la requête ne peut pas être satisfaite, par exemple parce que l'article que nous voulons afficher n'existe pas dans la base de données, nous levons une erreur 404 avec la méthode `error(?string $message = null, int $httpCode = 404)`. - -```php -public function renderShow(int $id): void -{ - $article = $this->articles->getById($id); - if (!$article) { - $this->error(); - } - // ... -} -``` - -Le code d'erreur HTTP peut être passé comme deuxième paramètre, la valeur par défaut est 404. La méthode fonctionne en levant une exception `Nette\Application\BadRequestException`, après quoi `Application` passe le contrôle à l'error-presenter. C'est un presenter dont la tâche est d'afficher une page informant de l'erreur survenue. La configuration de l'error-preseter se fait dans la [configuration application|configuration]. - - -Envoi de JSON -============= - -La méthode `sendJson($data)` encode les données données en JSON, les envoie comme réponse HTTP et termine le presenter. Exemple : - -```php -public function actionData(): void -{ - $data = ['hello' => 'nette']; - $this->sendJson($data); -} -``` - - -Paramètres de la requête .{data-version:3.1.14} -=============================================== - -Le presenter ainsi que chaque composant obtiennent leurs paramètres de la requête HTTP. Vous pouvez connaître leur valeur avec la méthode `getParameter($name)` ou `getParameters()`. Les valeurs sont des chaînes ou des tableaux de chaînes, il s'agit essentiellement de données brutes obtenues directement de l'URL. - -Pour plus de commodité, nous recommandons de rendre les paramètres accessibles via une propriété. Il suffit de les marquer avec l'attribut `#[Parameter]` : - -```php -use Nette\Application\Attributes\Parameter; // cette ligne est importante - -class HomePresenter extends Nette\Application\UI\Presenter -{ - #[Parameter] - public string $theme; // doit être public -} -``` - -Nous recommandons d'indiquer également le type de données pour la propriété (par ex. `string`) et Nette transtypera automatiquement la valeur en conséquence. Les valeurs des paramètres peuvent également être [validées |#Validation des paramètres]. - -Lors de la création d'un lien, la valeur des paramètres peut être définie directement : - -```latte -cliquer -``` - - -Paramètres persistants -====================== - -Les paramètres persistants sont utilisés pour maintenir l'état entre différentes requêtes. Leur valeur reste la même même après avoir cliqué sur un lien. Contrairement aux données de session, ils sont transmis dans l'URL. Et cela de manière entièrement automatique, il n'est donc pas nécessaire de les spécifier explicitement dans `link()` ou `n:href`. - -Exemple d'utilisation ? Vous avez une application multilingue. La langue actuelle est un paramètre qui doit constamment faire partie de l'URL. Mais il serait incroyablement fastidieux de l'indiquer dans chaque lien. Vous en faites donc un paramètre persistant `lang` et il sera transmis automatiquement. Génial ! - -La création d'un paramètre persistant est extrêmement simple dans Nette. Il suffit de créer une propriété publique et de la marquer avec un attribut : (auparavant, `/** @persistent */` était utilisé) - -```php -use Nette\Application\Attributes\Persistent; // cette ligne est importante - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; // doit être public -} -``` - -Si `$this->lang` a la valeur, par exemple, `'en'`, alors les liens créés à l'aide de `link()` ou `n:href` contiendront également le paramètre `lang=en`. Et après avoir cliqué sur le lien, `$this->lang` sera à nouveau `'en'`. - -Nous recommandons d'indiquer également le type de données pour la propriété (par ex. `string`) et vous pouvez également spécifier une valeur par défaut. Les valeurs des paramètres peuvent être [validées |#Validation des paramètres]. - -Les paramètres persistants sont transmis par défaut entre toutes les actions du presenter donné. Pour qu'ils soient également transmis entre plusieurs presenters, il faut les définir soit : - -- dans un ancêtre commun dont les presenters héritent -- dans un trait que les presenters utilisent : - -```php -trait LanguageAware -{ - #[Persistent] - public string $lang; -} - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - use LanguageAware; -} -``` - -Lors de la création d'un lien, la valeur du paramètre persistant peut être modifiée : - -```latte -détail en tchèque -``` - -Ou il peut être *réinitialisé*, c'est-à-dire supprimé de l'URL. Il prendra alors sa valeur par défaut : - -```latte -cliquer -``` - - -Espace de paramètres partagé -============================ - -Les paramètres de la requête, les [paramètres persistants |#Paramètres persistants] et les paramètres des méthodes `action`, `render` et `handle` (signal) partagent un seul et même espace, où chacun est identifié par son nom. Si le même nom apparaît dans plusieurs d'entre eux, il désigne une seule et même valeur. - -On en tire souvent parti. Un paramètre persistant `lang` et l'argument `$lang` d'une méthode d'action ou de signal ne font qu'un : vous pouvez lire la valeur courante d'un paramètre persistant simplement en le déclarant dans la signature de la méthode : - -```php -#[Persistent] -public string $lang; - -public function handleSearch(string $query, string $lang): void -{ - // $lang contient la valeur courante du paramètre persistant lang -} -``` - -Comme cet espace est partagé, veillez à ce que les noms de paramètres soient uniques, sauf si vous voulez délibérément qu'ils partagent une valeur. Cela vaut aussi pour les signaux, qui lisent en outre les paramètres du corps POST de la requête, voyez [Les signaux en profondeur |components#Signaux en profondeur]. - - -Composants interactifs -====================== - -Les presenters ont un système de composants intégré. Les composants sont des unités autonomes et réutilisables que nous insérons dans les presenters. Il peut s'agir de [formulaires |forms:in-presenter], de datagrids, de menus, en fait de tout ce qu'il est judicieux d'utiliser de manière répétée. - -Comment les composants sont-ils insérés dans le presenter et ensuite utilisés ? Vous l'apprendrez dans le chapitre [Composants |components]. Vous découvrirez même ce qu'ils ont en commun avec Hollywood. - -Et où puis-je obtenir des composants ? Sur la page [Componette |https://componette.org/search/component], vous trouverez des composants open-source ainsi que de nombreux autres add-ons pour Nette, placés ici par des bénévoles de la communauté autour du framework. - - -Allons plus en profondeur -========================= - -.[tip] -Ce que nous avons montré jusqu'à présent dans ce chapitre vous suffira probablement amplement. Les lignes suivantes sont destinées à ceux qui s'intéressent aux presenters en profondeur et veulent tout savoir. - - -Validation des paramètres -------------------------- - -Les valeurs des [#Paramètres de la requête] et des [#Paramètres persistants] reçues de l'URL sont écrites dans les propriétés par la méthode `loadState()`. Celle-ci vérifie également si le type de données indiqué pour la propriété correspond, sinon elle répond par une erreur 404 et la page ne s'affiche pas. - -Ne faites jamais confiance aveuglément aux paramètres, car ils peuvent être facilement modifiés par l'utilisateur dans l'URL. Voici comment nous vérifions, par exemple, si la langue `$this->lang` fait partie des langues prises en charge. Une bonne approche consiste à redéfinir la méthode `loadState()` mentionnée : - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; - - public function loadState(array $params): void - { - parent::loadState($params); // ici $this->lang est défini - // suit la vérification personnalisée de la valeur : - if (!in_array($this->lang, ['en', 'cs'])) { - $this->error(); - } - } -} -``` - - -Sauvegarde et restauration de la requête ----------------------------------------- - -La requête traitée par le presenter est un objet [api:Nette\Application\Request] et est retournée par la méthode du presenter `getRequest()`. - -La requête actuelle peut être sauvegardée dans la session ou au contraire restaurée à partir de celle-ci et laisser le presenter l'exécuter à nouveau. C'est utile par exemple dans une situation où l'utilisateur remplit un formulaire et sa connexion expire. Pour ne pas perdre les données, avant de rediriger vers la page de connexion, nous sauvegardons la requête actuelle dans la session à l'aide de `$reqId = $this->storeRequest()`, qui retourne son identifiant sous forme de chaîne courte, et nous le passons comme paramètre au presenter de connexion. - -Après la connexion, nous appelons la méthode `$this->restoreRequest($reqId)`, qui récupère la requête de la session et la transmet (forward). La méthode vérifie en même temps que la requête a été créée par le même utilisateur que celui qui vient de se connecter. Si un autre utilisateur se connecte ou si la clé est invalide, elle ne fait rien et le programme continue. - -Consultez le guide [Comment revenir à une page précédente |best-practices:restore-request]. - - -Canonisation ------------- - -Les presenters ont une fonctionnalité vraiment géniale qui contribue à un meilleur SEO (optimisation pour les moteurs de recherche). Ils empêchent automatiquement l'existence de contenu dupliqué sur différentes URL. Si plusieurs adresses URL mènent à une certaine cible, par ex. `/index` et `/index?page=1`, le framework détermine l'une d'elles comme étant la principale (canonique) et redirige les autres vers elle à l'aide du code HTTP 301. Grâce à cela, les moteurs de recherche n'indexent pas vos pages deux fois et ne diluent pas leur page rank. - -Ce processus est appelé canonisation. L'URL canonique est celle générée par le [routeur|routing], généralement la première route correspondante dans la collection. - -La canonisation est activée par défaut et peut être désactivée via `$this->autoCanonicalize = false`. - -La redirection ne se produit pas lors d'une requête AJAX ou POST, car cela entraînerait une perte de données ou n'aurait aucune valeur ajoutée du point de vue du SEO. - -Vous pouvez également déclencher la canonisation manuellement à l'aide de la méthode `canonicalize()`, à laquelle, comme à la méthode `link()`, sont passés le presenter, l'action et les paramètres. Elle crée un lien et le compare à l'URL actuelle. S'ils diffèrent, elle redirige vers le lien généré. - -```php -public function actionShow(int $id, ?string $slug = null): void -{ - $realSlug = $this->facade->getSlugForId($id); - // redirige si $slug diffère de $realSlug - $this->canonicalize('Product:show', [$id, $realSlug]); -} -``` - -Pour un motif complet combinant les filtres de route et `canonicalize()` afin de produire des URL favorables au référencement, voyez [URL propres avec slugs |best-practices:pretty-urls]. - - -Réponses --------- - -La réponse retournée par le presenter est un objet implémentant l'interface [api:Nette\Application\Response]. Il existe un certain nombre de réponses prêtes à l'emploi : - -- [api:Nette\Application\Responses\CallbackResponse] - envoie un callback -- [api:Nette\Application\Responses\FileResponse] - envoie un fichier -- [api:Nette\Application\Responses\ForwardResponse] - forward() -- [api:Nette\Application\Responses\JsonResponse] - envoie du JSON -- [api:Nette\Application\Responses\RedirectResponse] - redirection -- [api:Nette\Application\Responses\TextResponse] - envoie du texte -- [api:Nette\Application\Responses\VoidResponse] - réponse vide - -Les réponses sont envoyées avec la méthode `sendResponse()` : - -```php -use Nette\Application\Responses; - -// Texte simple -$this->sendResponse(new Responses\TextResponse('Hello Nette!')); - -// Envoie un fichier -$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf')); - -// La réponse sera un callback -$callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) { - if ($httpResponse->getHeader('Content-Type') === 'text/html') { - echo '

    Hello

    '; - } -}; -$this->sendResponse(new Responses\CallbackResponse($callback)); -``` - -Vous pouvez aussi écrire votre propre réponse. Il suffit d'implémenter l'interface `Nette\Application\Response`, qui n'a qu'une méthode `send()` recevant la requête et la réponse HTTP. C'est utile, par exemple, pour diffuser des données que vous ne voulez pas garder en mémoire : - -```php -class CsvResponse implements Nette\Application\Response -{ - public function __construct( - private string $fileName, - private iterable $rows, - ) { - } - - public function send(Nette\Http\IRequest $request, Nette\Http\IResponse $response): void - { - $response->setContentType('text/csv', 'utf-8'); - $response->sendAsFile($this->fileName); - - $handle = fopen('php://output', 'w'); - foreach ($this->rows as $row) { - fputcsv($handle, $row); - } - - fclose($handle); - } -} -``` - -Vous l'envoyez ensuite dans le presenter comme d'habitude : `$this->sendResponse(new CsvResponse('export.csv', $rows));` - - -Cache HTTP ----------- - -La méthode `lastModified()` permet de tirer facilement parti du cache HTTP. Vous lui passez la date et l'heure de dernière modification du contenu (sous forme de timestamp, de chaîne ou d'objet `DateTimeInterface`) et, éventuellement, un validateur ETag (une courte chaîne identifiant la version courante du contenu, comme son hachage) et une durée d'expiration. Si le navigateur détient déjà une version correspondante, le presenter envoie une réponse `304 Not Modified` et termine : la page n'est donc ni rendue ni transférée inutilement : - -```php -public function renderArticle(int $id): void -{ - $article = $this->articles->getById($id); - $this->lastModified($article->updatedAt); - // ... -} -``` - - -Finalisation du template .{data-version:3.3.0} ----------------------------------------------- - -Quand le presenter rend un template, la méthode `sendTemplate()` appelle `completeTemplate()` juste avant le rendu. Cette méthode remplit les variables marquées de l'attribut `#[TemplateVariable]` et localise le fichier de template (les variables par défaut sont déjà définies par la `TemplateFactory` lors de la création du template). Vous pouvez redéfinir cette méthode protégée pour ajouter des variables partagées par toutes les vues ou pour indiquer un autre fichier : - -```php -protected function completeTemplate(Nette\Application\UI\Template $template): void -{ - parent::completeTemplate($template); - $template->siteName = 'My App'; -} -``` - - -Restriction d'accès avec `#[Requires]` .{data-version:3.2.2} ------------------------------------------------------------- - -L'attribut `#[Requires]` offre des options avancées pour restreindre l'accès aux presenters et à leurs méthodes. Il peut être utilisé pour spécifier les méthodes HTTP, exiger une requête AJAX, limiter à la même origine (same origin), et l'accès uniquement via le forwarding. L'attribut peut être appliqué à la fois aux classes de presenter et aux méthodes individuelles `action()`, `render()`, `handle()` et `createComponent()`. - -Vous pouvez spécifier ces restrictions : -- sur les méthodes HTTP : `#[Requires(methods: ['GET', 'POST'])]` -- exiger une requête AJAX : `#[Requires(ajax: true)]` -- accès uniquement depuis la même origine : `#[Requires(sameOrigin: true)]` -- accès uniquement via forward : `#[Requires(forward: true)]` -- restriction à des actions spécifiques : `#[Requires(actions: 'default')]` - -.[note] -Depuis la version 3.3, la correspondance de même origine est vérifiée à l'aide de l'en-tête `Sec-Fetch-Site` du navigateur (auparavant via un cookie SameSite), ce qui est plus fiable et contrôle la correspondance exacte du schéma, du domaine et du port. - -Les détails se trouvent dans le guide [Comment utiliser l'attribut Requires |best-practices:attribute-requires]. - - -Vérification de la méthode HTTP -------------------------------- - -Les presenters dans Nette vérifient automatiquement la méthode HTTP de chaque requête entrante. La raison de cette vérification est principalement la sécurité. Par défaut, les méthodes `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH` sont autorisées. - -Si vous souhaitez autoriser en plus, par exemple, la méthode `OPTIONS`, utilisez l'attribut `#[Requires]` (depuis Nette Application v3.2) : - -```php -#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])] -class MyPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Depuis la version 3.1.13, la vérification est effectuée dans `checkHttpMethod()`, qui contrôle si la méthode indiquée dans la requête figure dans le tableau `$presenter->allowedMethods`. Depuis la version 3.2.3, cette approche est dépréciée au profit de `#[Requires]`. Vous pouvez redéfinir la méthode ainsi : - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - protected function checkHttpMethod(): void - { - $this->allowedMethods[] = 'OPTIONS'; - parent::checkHttpMethod(); - } -} -``` - -Il est important de souligner que si vous autorisez la méthode `OPTIONS`, vous devez ensuite la gérer correctement dans votre presenter. La méthode est souvent utilisée comme une requête dite preflight, que le navigateur envoie 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 la méthode mais n'implémentez pas la réponse correcte, cela peut entraîner des incohérences et des problèmes de sécurité potentiels. - - -Marquer les actions obsolètes .{data-version:3.2.3} ---------------------------------------------------- - -L'attribut `#[Deprecated]` marque des actions, des signaux ou des presenters entiers comme obsolètes et voués à disparaître. Lors de la génération de liens vers ces parties obsolètes de l'application, Nette lève un avertissement pour alerter les développeurs. - -Vous pouvez appliquer l'attribut à toute la classe du presenter ou à chaque méthode `action()`, `render()` et `handle()`. - - -Lectures complémentaires -======================== - -- [Méthodes et attributs inject |best-practices:inject-method-attribute] -- [Composition de presenters à partir de traits |best-practices:presenter-traits] -- [Passer des paramètres aux presenters |best-practices:passing-settings-to-presenters] -- [Comment revenir à une page précédente |best-practices:restore-request] diff --git a/application/fr/routing.texy b/application/fr/routing.texy deleted file mode 100644 index 64ec3dafce..0000000000 --- a/application/fr/routing.texy +++ /dev/null @@ -1,749 +0,0 @@ -Routage -******* - -
    - -Le routeur s'occupe de tout ce qui concerne les adresses URL, afin que vous n'ayez plus à y penser. Nous allons montrer : - -- comment configurer le routeur pour que les URL soient conformes aux attentes -- nous parlerons de SEO et de redirection -- et nous montrerons comment écrire votre propre routeur - -
    - - -Les URL conviviales (aussi appelées cool ou pretty URL) sont plus utilisables, plus faciles à mémoriser et contribuent positivement au SEO. Nette y pense et répond pleinement aux attentes des développeurs. Vous pouvez concevoir pour votre application exactement la structure d'adresses URL que vous souhaitez. Vous pouvez même la concevoir lorsque l'application est déjà terminée, car cela se fait sans intervention dans le code ou les templates. Elle est définie de manière élégante en un [seul endroit |#Intégration dans l'application], dans le routeur, et n'est donc pas dispersée sous forme d'annotations dans tous les presenters. - -Le routeur dans Nette est exceptionnel car il est **bidirectionnel.** Il sait à la fois décoder les URL dans la requête HTTP et créer des liens. Il joue donc un rôle essentiel dans [Nette Application |how-it-works#Nette Application], car il décide non seulement quel presenter et quelle action exécuteront la requête actuelle, mais il est également utilisé pour la [génération d'URL |creating-links] dans le template, etc. - -Cependant, le routeur n'est pas limité à cette seule utilisation, vous pouvez l'utiliser dans des applications où les presenters ne sont pas du tout utilisés, pour des API REST, etc. Plus d'informations dans la section [#Utilisation autonome]. - - -Collection de routes -==================== - -La manière la plus agréable de définir la forme des adresses URL dans l'application est offerte par la classe [api:Nette\Application\Routers\RouteList]. La définition est constituée d'une liste de routes, c'est-à-dire de masques d'adresses URL et des presenters et actions qui leur sont associés via une API simple. Nous n'avons pas besoin de nommer les routes. - -```php -$router = new Nette\Application\Routers\RouteList; -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('article/', 'Article:view'); -// ... -``` - -L'exemple indique que si nous ouvrons `https://domain.com/rss.xml` dans le navigateur, le presenter `Feed` avec l'action `rss` sera affiché, si `https://domain.com/article/12`, le presenter `Article` avec l'action `view` sera affiché, etc. En cas de non-correspondance de route appropriée, Nette Application réagit en levant une exception [BadRequestException |api:Nette\Application\BadRequestException], qui s'affiche à l'utilisateur comme une page d'erreur 404 Not Found. - - -Ordre des routes ----------------- - -L'**ordre** dans lequel les différentes routes sont listées est **absolument crucial**, car elles sont évaluées séquentiellement de haut en bas. La règle est que nous déclarons les routes **des plus spécifiques aux plus générales** : - -```php -// MAUVAIS : 'rss.xml' est capturé par la première route et interprète cette chaîne comme -$router->addRoute('', 'Article:view'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// BON -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('', 'Article:view'); -``` - -Les routes sont également évaluées de haut en bas lors de la génération de liens : - -```php -// MAUVAIS : le lien vers 'Feed:rss' générera 'admin/feed/rss' -$router->addRoute('admin//', 'Admin:default'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// BON -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('admin//', 'Admin:default'); -``` - -Nous n'allons pas vous cacher que la composition correcte des routes demande une certaine compétence. Avant de la maîtriser, le [panneau de routage |#Débogage du routeur] vous sera un outil utile. - - -Masque et paramètres --------------------- - -Le masque décrit le chemin relatif depuis le répertoire racine du site web. Le masque le plus simple est une URL statique : - -```php -$router->addRoute('products', 'Products:default'); -``` - -Souvent, les masques contiennent des **paramètres**. Ils sont indiqués entre chevrons (par ex. ``) et sont transmis au presenter cible, par exemple à la méthode `renderShow(int $year)` ou au paramètre persistant `$year` : - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -L'exemple indique que si nous ouvrons `https://example.com/chronicle/2020` dans le navigateur, le presenter `History` avec l'action `show` et le paramètre `year: 2020` sera affiché. - -Nous pouvons spécifier une valeur par défaut pour les paramètres directement dans le masque, ce qui les rend facultatifs : - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -La route acceptera désormais également l'URL `https://example.com/chronicle/`, qui affichera à nouveau `History:show` avec le paramètre `year: 2020`. - -Le paramètre peut bien sûr aussi être le nom du presenter et de l'action. Par exemple comme ceci : - -```php -$router->addRoute('/', 'Home:default'); -``` - -La route spécifiée accepte par ex. des URL de la forme `/article/edit` ou aussi `/catalog/list` et les interprète comme des presenters et actions `Article:edit` et `Catalog:list`. - -En même temps, elle donne aux paramètres `presenter` et `action` les valeurs par défaut `Home` et `default` et ils sont donc également facultatifs. Ainsi, la route accepte également une URL de la forme `/article` et l'interprète comme `Article:default`. Ou inversement, un lien vers `Product:default` générera le chemin `/product`, un lien vers le `Home:default` par défaut générera le chemin `/`. - -Le masque peut décrire non seulement le chemin relatif depuis le répertoire racine du site web, mais aussi le chemin absolu s'il commence par une barre oblique, ou même une URL absolue entière si elle commence par deux barres obliques : - -```php -// relatif au document root -$router->addRoute('/', /* ... */); - -// chemin absolu (relatif au domaine) -$router->addRoute('//', /* ... */); - -// URL absolue incluant le domaine (relative au schéma) -$router->addRoute('//.example.com//', /* ... */); - -// URL absolue incluant le schéma -$router->addRoute('https://.example.com//', /* ... */); -``` - - -Expressions de validation -------------------------- - -Pour chaque paramètre, une condition de validation peut être établie à l'aide d'une [expression régulière|https://www.php.net/manual/en/reference.pcre.pattern.syntax.php]. Par exemple, pour le paramètre `id`, nous spécifions qu'il ne peut contenir que des chiffres à l'aide de l'expression régulière `\d+` : - -```php -$router->addRoute('/[/]', /* ... */); -``` - -L'expression régulière par défaut pour tous les paramètres est `[^/]+`, c'est-à-dire tout sauf la barre oblique. Si un paramètre doit également accepter des barres obliques, nous spécifions l'expression `.+` : - -```php -// accepte https://example.com/a/b/c, path sera 'a/b/c' -$router->addRoute('', /* ... */); -``` - - -Séquences facultatives ----------------------- - -Dans le masque, les parties facultatives peuvent être marquées à l'aide de crochets. N'importe quelle partie du masque peut être facultative, elle peut également contenir des paramètres : - -```php -$router->addRoute('[/]', /* ... */); - -// Accepte les chemins : -// /cs/download => lang => cs, name => download -// /download => lang => null, name => download -``` - -Lorsqu'un paramètre fait partie d'une séquence facultative, il devient bien sûr également facultatif. S'il n'a pas de valeur par défaut spécifiée, il sera null. - -Les parties facultatives peuvent également se trouver dans le domaine : - -```php -$router->addRoute('//[.]example.com//', /* ... */); -``` - -Les séquences peuvent être imbriquées et combinées librement : - -```php -$router->addRoute( - '[[-]/][/page-]', - 'Home:default', -); - -// Accepte les chemins : -// /cs/hello -// /en-us/hello -// /hello -// /hello/page-12 -``` - -Lors de la génération d'URL, on s'efforce d'obtenir la variante la plus courte, donc tout ce qui peut être omis est omis. C'est pourquoi, par exemple, la route `index[.html]` génère le chemin `/index`. Il est possible d'inverser ce comportement en ajoutant un point d'exclamation après le crochet gauche : - -```php -// accepte /hello et /hello.html, génère /hello -$router->addRoute('[.html]', /* ... */); - -// accepte /hello et /hello.html, génère /hello.html -$router->addRoute('[!.html]', /* ... */); -``` - -Les paramètres facultatifs (c'est-à-dire les paramètres ayant une valeur par défaut) sans crochets se comportent essentiellement comme s'ils étaient encadrés de la manière suivante : - -```php -$router->addRoute('//', /* ... */); - -// correspond à ceci : -$router->addRoute('[/[/[]]]', /* ... */); -``` - -Si nous voulions influencer le comportement de la barre oblique finale, pour que par exemple `/home` soit généré au lieu de `/home/`, cela peut être réalisé comme suit : - -```php -$router->addRoute('[[/[/]]]', /* ... */); -``` - - -Caractères génériques ---------------------- - -Dans le masque d'un chemin absolu, nous pouvons utiliser les caractères génériques suivants et éviter ainsi, par exemple, d'avoir à écrire le domaine dans le masque, qui peut différer entre les environnements de développement et de production : - -- `%tld%` = domaine de premier niveau, par ex. `com` ou `org` -- `%sld%` = domaine de second niveau, par ex. `example` -- `%domain%` = domaine sans sous-domaines, par ex. `example.com` -- `%host%` = hôte complet, par ex. `www.example.com` -- `%basePath%` = chemin vers le répertoire racine - -```php -$router->addRoute('//www.%domain%/%basePath%//', /* ... */); -$router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('/[/]', [ - 'presenter' => 'Home', - 'action' => 'default', -]); -``` - -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, nous pouvons également définir d'autres propriétés des paramètres, comme par exemple l'expression régulière de validation (voir le paramètre `id`) : - -```php -use Nette\Routing\Route; - -$router->addRoute('/[/]', [ - 'presenter' => [ - Route::Value => 'Home', - ], - 'action' => [ - Route::Value => 'default', - ], - 'id' => [ - Route::Pattern => '\d+', - ], -]); -``` - -Il est important de noter que si les paramètres définis dans le tableau ne sont pas spécifiés dans le masque du chemin, leurs valeurs ne peuvent pas être modifiées, même à l'aide des paramètres de requête spécifiés après le point d'interrogation dans l'URL. - -C'est utile pour les **paramètres fixes** : donner à une page précise une URL courte et facile à retenir. Par exemple, pour que `/tos` ouvre toujours `Article:view` avec `id: 123` : - -```php -$router->addRoute('tos', [ - 'presenter' => 'Article', - 'action' => 'view', - 'id' => 123, -]); -``` - - -Filtres et traductions ----------------------- - -Nous écrivons le code source de l'application en anglais, mais si le site web doit avoir des URL en français, alors un routage simple du type : - -```php -$router->addRoute('/', 'Home:default'); -``` - -générera des URL en anglais, comme `/product/123` ou `/cart`. Si nous voulons que les presenters et les actions dans l'URL soient représentés par des mots français (par ex. `/produit/123` ou `/panier`), nous pouvons utiliser un dictionnaire de traduction. Pour l'écrire, nous avons besoin de la variante "plus verbeuse" du deuxième paramètre : - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterTable => [ - // chaîne dans l'URL => presenter - 'produit' => 'Product', - 'panier' => 'Cart', - 'catalogue' => 'Catalog', - ], - ], - 'action' => [ - Route::Value => 'default', - Route::FilterTable => [ - 'liste' => 'list', - ], - ], -]); -``` - -Plusieurs clés du dictionnaire de traduction peuvent mener au même presenter. Cela crée différents alias pour lui. La variante canonique (c'est-à-dire celle qui sera dans l'URL générée) est considérée comme la dernière clé. - -La table de traduction peut être utilisée de cette manière pour n'importe quel paramètre. Si la traduction n'existe pas, la valeur d'origine est prise. Ce comportement peut être modifié en ajoutant `Route::FilterStrict => true` et la route refusera alors l'URL si la valeur n'est pas dans le dictionnaire. - -En plus du dictionnaire de traduction sous forme de tableau, des fonctions de traduction personnalisées peuvent également être utilisées. - -```php -use Nette\Routing\Route; - -$router->addRoute('//', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterIn => function (string $s): string { /* ... */ }, - Route::FilterOut => function (string $s): string { /* ... */ }, - ], - 'action' => 'default', - 'id' => null, -]); -``` - -La fonction `Route::FilterIn` convertit entre le paramètre dans l'URL et la chaîne qui est ensuite transmise au presenter, la fonction `FilterOut` assure la conversion dans le sens inverse. - -Les paramètres `presenter`, `action` et `module` ont déjà des filtres prédéfinis qui convertissent entre le style PascalCase ou camelCase et le style kebab-case utilisé dans les URL. La valeur par défaut des paramètres se écrit déjà sous forme transformée, donc par exemple dans le cas du presenter, nous écrivons ``, et non ``. - - -Filtres généraux ----------------- - -En plus des filtres destinés à des paramètres spécifiques, nous pouvons également définir des filtres généraux qui reçoivent un tableau associatif de tous les paramètres, qu'ils peuvent modifier de n'importe quelle manière, puis les retournent. Nous définissons les filtres généraux sous la clé vide. - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => 'Home', - 'action' => 'default', - '' => [ - Route::FilterIn => function (array $params): array { /* ... */ }, - Route::FilterOut => function (array $params): array { /* ... */ }, - ], -]); -``` - -Les filtres généraux donnent la possibilité de modifier le comportement de la route de manière absolument quelconque. Nous pouvons les utiliser par exemple pour modifier des paramètres en fonction d'autres paramètres. Par exemple, la traduction de `` et `` en fonction de la valeur actuelle du paramètre ``. - -Si un paramètre a un filtre personnalisé défini et qu'un filtre général existe également, le `FilterIn` personnalisé est exécuté avant le général et inversement, le `FilterOut` général est exécuté avant le personnalisé. Ainsi, à l'intérieur du filtre général, les valeurs des paramètres `presenter` ou `action` sont écrites dans le style PascalCase ou camelCase. - -Voyez [URL propres avec slugs |best-practices:pretty-urls] pour un usage concret de ces filtres : générer des URL favorables au référencement, du type `/article/123-how-to-bake-bread`, sans toucher au moindre template. - - -Routes unidirectionnelles OneWay --------------------------------- - -Les routes unidirectionnelles sont utilisées pour maintenir la fonctionnalité des anciennes URL que l'application ne génère plus, mais accepte toujours. Nous les marquons avec l'indicateur `OneWay` : - -```php -// ancienne URL /product-info?id=123 -$router->addRoute('product-info', 'Product:detail', $router::ONE_WAY); -// nouvelle URL /product/123 -$router->addRoute('product/', 'Product:detail'); -``` - -Lors de l'accès à l'ancienne URL, le presenter redirige automatiquement vers la nouvelle URL, de sorte que les moteurs de recherche n'indexeront pas ces pages deux fois (voir [#SEO et canonisation]). - - -Routage dynamique avec callbacks --------------------------------- - -Le routage dynamique avec callbacks vous permet d'assigner directement des fonctions (callbacks) aux routes, qui seront exécutées lorsque le chemin donné sera visité. Cette fonctionnalité flexible vous permet de créer rapidement et efficacement différents points de terminaison (endpoints) pour votre application : - -```php -$router->addRoute('test', function () { - echo 'vous êtes à l\'adresse /test'; -}); -``` - -Vous pouvez également définir des paramètres dans le masque, qui seront automatiquement transmis à votre callback : - -```php -$router->addRoute('', function (string $lang) { - echo match ($lang) { - 'fr' => 'Bienvenue sur la version française de notre site !', - 'en' => 'Welcome to the English version of our website!', - }; -}); -``` - -Outre les paramètres du masque, le callback peut aussi recevoir des services du conteneur DI. Ils lui sont passés d'après le type du paramètre. De plus, le paramètre `$presenter` reçoit une instance de [MicroPresenter |api:NetteModule\MicroPresenter], qui traite la route : - -```php -$router->addRoute('', function (string $lang, Nette\Http\Request $httpRequest, NetteModule\MicroPresenter $presenter) { - // ... -}); -``` - - -Modules -------- - -Si nous avons plusieurs routes qui appartiennent à un [module |directory-structure#Presenters et templates] commun, nous utilisons `withModule()`. Le module donné est automatiquement préfixé au presenter de chaque route du groupe, et il disparaît complètement de l'URL : - -```php -$router = new RouteList; -$router->withModule('Forum') // les routes suivantes font partie du module Forum - ->addRoute('rss', 'Feed:rss') // le presenter sera Forum:Feed - ->addRoute('/') - - ->withModule('Admin') // les routes suivantes font partie du module Forum:Admin - ->addRoute('sign:in', 'Sign:in'); -``` - -Une alternative est le paramètre `module`, qui fixe lui aussi un module donné et le garde hors de l'URL : - -```php -// L'URL manage/dashboard/default est mappée sur le presenter Admin:Dashboard -$router->addRoute('manage//', [ - 'module' => 'Admin', -]); -``` - -Tout nom de presenter n'est complet qu'accompagné de son module, par ex. `Front:Admin:ProductList`. Chaque fois qu'un tel nom complet atterrit dans un paramètre d'URL, le routeur l'encode selon deux règles simples : chaque deux-points `:` (le séparateur de modules) devient un **point**, et chaque frontière de mot d'un nom en PascalCase devient un **tiret**. Ainsi, `Front:Admin:ProductList` apparaît dans l'URL comme `front.admin.product-list` et se décode de la même façon en sens inverse. C'est pourquoi une application modulaire produit, sans aucun des outils ci-dessus, des URL pleines de points. - -`withModule()` comme le paramètre `module` évitent cela précisément parce qu'ils retirent un préfixe de module connu du nom du presenter avant qu'il n'atteigne l'URL : le module étant constant, il n'a pas besoin d'être encodé du tout. - -Il arrive que nous voulions faire varier le module lui-même et le voir apparaître dans l'URL ; nous employons alors `` directement dans le masque. Attention à un détail capital : **`` capture tout le chemin du module**, c'est-à-dire tout ce qui précède le dernier deux-points du nom du presenter. Pour le presenter `Shop:Admin:Product`, cela signifie le module `Shop:Admin` et le presenter `Product` ; et comme les deux-points deviennent des points, nous obtenons : - - -Sous-domaines -------------- - -Nous pouvons diviser les collections de routes par sous-domaines : - -```php -$router = new RouteList; -$router->withDomain('example.com') - ->addRoute('rss', 'Feed:rss') - ->addRoute('/'); -``` - -Dans le nom de domaine, on peut également utiliser des [#Caractères génériques] : - -```php -$router = new RouteList; -$router->withDomain('example.%tld%') - // ... -``` - - -Préfixe de chemin ------------------ - -Nous pouvons diviser les collections de routes par chemin dans l'URL : - -```php -$router = new RouteList; -$router->withPath('eshop') - ->addRoute('rss', 'Feed:rss') // attrape l'URL /eshop/rss - ->addRoute('/'); // attrape l'URL /eshop// -``` - - -Combinaisons ------------- - -Nous pouvons combiner les divisions ci-dessus : - -```php -$router = (new RouteList) - ->withDomain('admin.example.com') - ->withModule('Admin') - ->addRoute(/* ... */) - ->addRoute(/* ... */) - ->end() - ->withModule('Images') - ->addRoute(/* ... */) - ->end() - ->end() - ->withDomain('example.com') - ->withPath('export') - ->addRoute(/* ... */) - // ... -``` - - -Paramètres de requête ---------------------- - -Les masques peuvent également contenir des paramètres de requête (paramètres après le point d'interrogation dans l'URL). On ne peut pas leur définir d'expression de validation, mais on peut changer le nom sous lequel ils sont transmis au presenter : - -```php -// le paramètre de requête 'cat' que nous voulons utiliser dans l'application sous le nom 'categoryId' -$router->addRoute('product ? id= & cat=', /* ... */); -``` - - -Paramètres Foo --------------- - -Maintenant, nous allons plus en profondeur. Les paramètres Foo sont essentiellement des paramètres sans nom qui permettent de faire correspondre une expression régulière. Un exemple est une route acceptant `/index`, `/index.html`, `/index.htm` et `/index.php` : - -```php -$router->addRoute('index', /* ... */); -``` - -Il est également possible de définir explicitement la chaîne qui sera utilisée lors de la génération de l'URL. La chaîne doit être placée directement après le point d'interrogation. La route suivante est similaire à la précédente, mais génère `/index.html` au lieu de `/index`, car la chaîne `.html` est définie comme valeur de génération : - -```php -$router->addRoute('index', /* ... */); -``` - - -Intégration dans l'application -============================== - -Pour intégrer le routeur créé dans l'application, nous devons en informer le conteneur DI. Le moyen le plus simple est de préparer une factory qui fabriquera l'objet routeur et d'indiquer dans la configuration du conteneur qu'elle doit être utilisée. Supposons que nous écrivions à cet effet la méthode `App\Core\RouterFactory::createRouter()` : - -```php -namespace App\Core; - -use Nette\Application\Routers\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute(/* ... */); - return $router; - } -} -``` - -Dans la [configuration |dependency-injection:services], nous écrirons alors : - -```neon -services: - - App\Core\RouterFactory::createRouter -``` - -Toutes les dépendances, par exemple envers la base de données, etc., sont transmises à la méthode factory comme ses paramètres via [l'autowiring|dependency-injection:autowiring] : - -```php -public static function createRouter(Nette\Database\Connection $db): RouteList -{ - // ... -} -``` - - -SimpleRouter -============ - -Un routeur beaucoup plus simple que la collection de routes est [SimpleRouter |api:Nette\Application\Routers\SimpleRouter]. Nous l'utilisons lorsque nous n'avons pas d'exigences particulières sur la forme de l'URL, si `mod_rewrite` (ou ses alternatives) n'est pas disponible ou si nous ne voulons pas encore nous préoccuper des URL conviviales. - -Il génère des adresses à peu près sous cette forme : - -``` -http://example.com/?presenter=Product&action=detail&id=123 -``` - -Le paramètre du constructeur de `SimpleRouter` est le presenter & action par défaut, c'est-à-dire l'action à exécuter si nous ouvrons par exemple `http://example.com/` sans autres paramètres. - -```php -// le presenter par défaut sera 'Home' et l'action 'default' -$router = new Nette\Application\Routers\SimpleRouter('Home:default'); -``` - -Nous recommandons de définir SimpleRouter directement dans la [configuration |dependency-injection:services] : - -```neon -services: - - Nette\Application\Routers\SimpleRouter('Home:default') -``` - - -SEO et canonisation -=================== - -Le framework contribue au SEO (optimisation pour les moteurs de recherche) en empêchant la duplication de contenu sur différentes URL. Si plusieurs adresses mènent à une certaine cible, par ex. `/index` et `/index.html`, le framework détermine la première comme étant la principale (canonique) et redirige les autres vers elle à l'aide du code HTTP 301. Grâce à cela, les moteurs de recherche n'indexeront pas vos pages deux fois et ne dilueront pas leur page rank. - -Ce processus est appelé canonisation. L'URL canonique est celle générée par le routeur, c'est-à-dire la première route correspondante dans la collection sans l'indicateur OneWay. C'est pourquoi dans la collection, nous listons les **routes primaires en premier**. - -La canonisation est effectuée par le presenter, plus d'informations dans le chapitre [canonisation |presenters#Canonisation]. - - -HTTPS -===== - -Pour pouvoir utiliser le protocole HTTPS, il est nécessaire de l'activer sur l'hébergement et de configurer correctement le serveur. - -La redirection de l'ensemble du site vers HTTPS doit être configurée au niveau du serveur, par exemple à l'aide du fichier `.htaccess` dans le répertoire racine de notre application, et ce avec le code HTTP 301. La configuration peut varier en fonction de l'hébergement et ressemble à peu près à ceci : - -``` - - RewriteEngine On - ... - RewriteCond %{HTTPS} off - RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] - ... - -``` - -Le routeur génère des URL avec le même protocole que celui avec lequel la page a été chargée, il n'y a donc rien de plus à configurer. - -Cependant, si exceptionnellement nous avons besoin que différentes routes fonctionnent sous différents protocoles, nous l'indiquons dans le masque de la route : - -```php -// Générera une adresse avec HTTP -$router->addRoute('http://%host%//', /* ... */); - -// Générera une adresse avec HTTPS -$router->addRoute('https://%host%//', /* ... */); -``` - - -Débogage du routeur -=================== - -Le panneau de routage affiché dans la [barre Tracy |tracy:] est un outil utile qui affiche la liste des routes ainsi que les paramètres que le routeur a obtenus de l'URL. - -La barre verte avec le symbole ✓ représente la route qui a traité l'URL actuelle, la couleur bleue et le symbole ≈ indiquent les routes qui auraient également traité l'URL si la verte ne les avait pas devancées. Ensuite, nous voyons le presenter & action actuel. - -[* routing-debugger.webp *] - -En même temps, si une redirection inattendue se produit en raison de la [canonisation |#SEO et canonisation], il est utile de regarder le panneau dans la barre *redirect*, où vous découvrirez comment le routeur a initialement compris l'URL et pourquoi il a redirigé. - -.[note] -Lors du débogage du routeur, nous recommandons d'ouvrir les Outils de développement dans le navigateur (Ctrl+Shift+I ou Cmd+Option+I) et de désactiver le cache dans le panneau Réseau, afin que les redirections n'y soient pas enregistrées. - - -Performance -=========== - -Le nombre de routes a une influence sur la vitesse du routeur. Leur nombre ne devrait certainement pas dépasser quelques dizaines. Si votre site a une structure d'URL trop compliquée, vous pouvez écrire un [#Routeur personnalisé] sur mesure. - -Si le routeur n'a pas de dépendances, par exemple envers la base de données, et que sa factory n'accepte aucun argument, nous pouvons sérialiser sa forme compilée directement dans le conteneur DI et ainsi accélérer légèrement l'application. - -```neon -routing: - cache: true -``` - - -Routeur personnalisé -==================== - -Les lignes suivantes sont destinées aux utilisateurs très avancés. Vous pouvez créer votre propre routeur et l'intégrer tout naturellement dans la collection de routes. Le routeur est une implémentation de l'interface [api:Nette\Routing\Router] avec deux méthodes : - -```php -use Nette\Http\IRequest as HttpRequest; -use Nette\Http\UrlScript; - -class MyRouter implements Nette\Routing\Router -{ - public function match(HttpRequest $httpRequest): ?array - { - // ... - } - - public function constructUrl(array $params, UrlScript $refUrl): ?string - { - // ... - } -} -``` - -La méthode `match` traite la requête actuelle [$httpRequest |http:request], à partir de laquelle on peut obtenir non seulement l'URL, mais aussi les en-têtes, etc., en un tableau contenant le nom du presenter et ses paramètres. Si elle ne peut pas traiter la requête, elle retourne null. Lors du traitement de la requête, nous devons renvoyer au minimum le presenter ; l'action est facultative et vaut `default` par défaut si elle n'est pas indiquée. Le nom du presenter est complet et contient également d'éventuels modules : - -```php -[ - 'presenter' => 'Front:Home', - 'action' => 'default', -] -``` - -La méthode `constructUrl` assemble au contraire l'URL absolue résultante à partir du tableau de paramètres. Pour cela, elle peut utiliser les informations du paramètre [`$refUrl`|api:Nette\Http\UrlScript], qui est l'URL actuelle. - -Vous l'ajoutez à la collection de routes à l'aide de `add()` : - -```php -$router = new Nette\Application\Routers\RouteList; -$router->add($myRouter); -$router->addRoute(/* ... */); -// ... -``` - - -Utilisation autonome -==================== - -Par utilisation autonome, nous entendons l'utilisation des capacités du routeur dans une application qui n'utilise pas Nette Application et les presenters. Presque tout ce que nous avons montré dans ce chapitre s'applique, avec ces différences : - -- pour les collections de routes, nous utilisons la classe [api:Nette\Routing\RouteList] -- comme routeur simple, la classe [api:Nette\Routing\SimpleRouter] -- comme il n'existe pas de paire `Presenter:action`, nous utilisons la [#Notation étendue] - -Donc, nous créons à nouveau une méthode qui nous assemblera le routeur, par ex. : - -```php -namespace App\Core; - -use Nette\Routing\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute('rss.xml', [ - 'controller' => 'RssFeedController', - ]); - $router->addRoute('article/', [ - 'controller' => 'ArticleController', - ]); - // ... - return $router; - } -} -``` - -Si vous utilisez un conteneur DI, ce que nous recommandons, nous ajoutons à nouveau la méthode à la configuration et obtenons ensuite le routeur avec la requête HTTP du conteneur : - -```php -$router = $container->getByType(Nette\Routing\Router::class); -$httpRequest = $container->getByType(Nette\Http\IRequest::class); -``` - -Ou nous créons directement les objets : - -```php -$router = App\Core\RouterFactory::createRouter(); -$httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); -``` - -Maintenant, il ne reste plus qu'à laisser le routeur travailler : - -```php -$params = $router->match($httpRequest); -if ($params === null) { - // aucune route correspondante n'a été trouvée, nous envoyons une erreur 404 - exit; -} - -// nous traitons les paramètres obtenus -$controller = $params['controller']; -// ... -``` - -Et inversement, nous utilisons le routeur pour assembler un lien : - -```php -$params = ['controller' => 'ArticleController', 'id' => 123]; -$url = $router->constructUrl($params, $httpRequest->getUrl()); -``` - - -{{composer: nette/routing}} -{{repo: nette/routing}} -{{api: https://api.nette.org/routing/}} diff --git a/application/fr/templates.texy b/application/fr/templates.texy deleted file mode 100644 index 2b1931c088..0000000000 --- a/application/fr/templates.texy +++ /dev/null @@ -1,419 +0,0 @@ -Templates -********* - -.[perex] -Nette utilise le système de templates [Latte |latte:]. D'une part parce que c'est le système de templates le plus sécurisé pour PHP, et d'autre part parce que c'est aussi le système le plus intuitif. Vous n'avez pas besoin d'apprendre beaucoup de nouveautés, la connaissance de PHP et de quelques balises suffit. - -Il est courant qu'une page soit composée d'un template de layout + du template de l'action donnée. Voici à quoi peut ressembler un template de layout, remarquez les blocs `{block}` et la balise `{include}` : - -```latte - - - - {block title}Mon App{/block} - - -
    ...
    - {include content} -
    ...
    - - -``` - -Et voici ce que sera le template de l'action : - -```latte -{block title}Page d'accueil{/block} - -{block content} -

    Page d'accueil

    -... -{/block} -``` - -Il définit le bloc `content`, qui sera inséré à la place de `{include content}` dans le layout, et redéfinit également le bloc `title`, qui écrasera `{block title}` dans le layout. Essayez d'imaginer le résultat. - - -Recherche de templates ----------------------- - -Vous n'avez pas besoin de spécifier dans les presenters quel template doit être rendu, le framework déduit le chemin lui-même et vous évite d'écrire. - -Si vous utilisez une structure de répertoires où chaque presenter a son propre répertoire, placez simplement le template dans ce répertoire sous le nom de l'action (ou de la vue), c'est-à-dire pour l'action `default`, utilisez le template `default.latte` : - -/--pre -app/ -└── Presentation/ - └── Home/ - ├── HomePresenter.php - └── default.latte -\-- - -Si vous utilisez une structure où les presenters sont regroupés dans un seul répertoire et les templates dans un dossier `templates`, enregistrez-le soit dans le fichier `..latte` soit `/.latte` : - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── Home.default.latte ← 1ère variante - └── Home/ - └── default.latte ← 2ème variante -\-- - -Le répertoire `templates` peut également être placé un niveau plus haut, c'est-à-dire au même niveau que le répertoire contenant les classes des presenters. - -Si le template n'est pas trouvé, le presenter répondra par une [erreur 404 - page non trouvée |presenters#Erreur 404 et autres]. - -Vous pouvez changer la vue en utilisant `$this->setView('autreVue')`. Il est également possible de spécifier directement le fichier de template en utilisant `$this->template->setFile('/chemin/vers/template.latte')`. - -.[note] -Les fichiers où les templates sont recherchés peuvent être modifiés en surchargeant la méthode [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], qui retourne un tableau de noms de fichiers possibles. - - -Recherche du template de layout -------------------------------- - -Nette recherche également automatiquement le fichier de layout. - -Si vous utilisez une structure de répertoires où chaque presenter a son propre répertoire, placez le layout soit dans le dossier du presenter s'il lui est spécifique, soit un niveau plus haut s'il est commun à plusieurs presenters : - -/--pre -app/ -└── Presentation/ - ├── @layout.latte ← layout commun - └── Home/ - ├── @layout.latte ← uniquement pour le presenter Home - ├── HomePresenter.php - └── default.latte -\-- - -Si vous utilisez une structure où les presenters sont regroupés dans un seul répertoire et les templates dans un dossier `templates`, le layout sera attendu à ces endroits : - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── @layout.latte ← layout commun - ├── Home.@layout.latte ← uniquement pour Home, 1ère variante - └── Home/ - └── @layout.latte ← uniquement pour Home, 2ème variante -\-- - -Si le presenter se trouve dans un module, la recherche s'effectuera également aux niveaux de répertoires supérieurs, en fonction de l'imbrication du module. - -Le nom du layout peut être modifié à l'aide de `$this->setLayout('layoutAdmin')` et il sera alors attendu dans le fichier `@layoutAdmin.latte`. Il est également possible de spécifier directement le fichier de template de layout à l'aide de `$this->setLayout('/chemin/vers/template.latte')`. - -En utilisant `$this->setLayout(false)` ou la balise `{layout none}` à l'intérieur du template, la recherche de layout est désactivée. - -.[note] -Les fichiers où les templates de layout sont recherchés peuvent être modifiés en surchargeant la méthode [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], qui retourne un tableau de noms de fichiers possibles. - - -Variables dans le template --------------------------- - -Les variables sont passées aux templates en les écrivant dans `$this->template`. Elles deviennent alors disponibles dans le template comme variables locales : - -```php -$this->template->article = $this->articles->getById($id); -``` - -Pour passer automatiquement la valeur d'une propriété au template sous forme de variable, marquez-la de l'attribut `#[TemplateVariable]` et rendez-la publique : .{data-version:3.2.9} - -```php -use Nette\Application\Attributes\TemplateVariable; - -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - #[TemplateVariable] - public string $siteName = 'My blog'; -} -``` - -Si vous passez au template une variable du même nom, `#[TemplateVariable]` ne l'écrasera pas. - - -Variables par défaut --------------------- - -Les presenters et les composants transmettent automatiquement plusieurs variables utiles aux templates : - -- `$basePath` est le chemin URL absolu vers le répertoire racine (par ex. `/eshop`) -- `$baseUrl` est l'URL absolue vers le répertoire racine (par ex. `http://localhost/eshop`) -- `$user` est l'objet [représentant l'utilisateur |security:authentication] -- `$presenter` est le presenter actuel -- `$control` est le composant ou presenter actuel -- `$flashes` tableau des [messages |presenters#Messages Flash] envoyés par la fonction `flashMessage()` - -Si vous utilisez votre propre classe de template, ces variables seront transmises si vous créez une propriété pour elles. - - -Templates typés ---------------- - -Quand vous développez des applications robustes, il est utile de définir explicitement quelles variables le template attend et de quels types. Cela apporte le contrôle de types de PHP, des suggestions pertinentes dans l'IDE et permet à l'analyse statique de détecter les erreurs. - -Comment définir une telle liste ? Simplement sous forme de classe dont les propriétés représentent les variables du template. Nommez-la comme le presenter, en ajoutant simplement `Template` à la fin : - -```php -/** - * @property-read ArticleTemplate $template - */ -class ArticlePresenter extends Nette\Application\UI\Presenter -{ -} - -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - public Model\Article $article; - public Nette\Security\User $user; - - // et d'autres variables -} -``` - -L'objet `$this->template` du presenter sera désormais une instance de la classe `ArticleTemplate`. PHP contrôlera donc les types déclarés lors de l'écriture. - -Nette choisit la classe de template automatiquement. Il cherche d'abord une classe nommée `Template`, par ex. `ArticleEditTemplate` pour l'action `edit`, et ne se rabat sur `Template` que si elle n'existe pas. - -L'annotation `@property-read` est destinée à l'IDE et à l'analyse statique : elle active la complétion, voyez "PhpStorm et la complétion pour $this⁠-⁠>⁠template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template. - -[* phpstorm-completion.webp *] - -Vous pouvez aussi utiliser la complétion directement dans les templates. Il suffit d'installer le plugin Latte pour PhpStorm et d'indiquer au début du template le nom de la classe de paramètres, plus de détails dans le chapitre [Latte : système de types |latte:type-system] : - -```latte -{templateType App\Presentation\Article\ArticleTemplate} -... -``` - -Cela vaut également pour les composants. Il suffit de suivre la convention de nommage et de créer une classe de paramètres `FifteenTemplate` pour un composant comme `FifteenControl`. - -Si vous avez besoin d'une autre classe de paramètres, utilisez la méthode `createTemplate()` : - -```php -public function renderDefault(): void -{ - $template = $this->createTemplate(SpecialTemplate::class); - $template->foo = 123; - // ... - $this->sendTemplate($template); -} -``` - -.{data-version:3.3.0} -Si vous avez besoin d'influencer la façon dont le template est finalisé avant le rendu - par exemple pour ajouter des variables partagées par toutes les actions - vous pouvez redéfinir la méthode `completeTemplate()` dans le presenter. Elle est appelée juste avant le rendu du template : - -```php -protected function completeTemplate(Nette\Application\UI\Template $template): void -{ - parent::completeTemplate($template); - $template->siteName = 'My blog'; -} -``` - - -Création de liens ------------------ - -Dans le template, les liens vers d'autres presenters & actions sont créés de cette manière : - -```latte -détail du produit -``` - -L'attribut `n:href` est très pratique pour les balises HTML ``. Si nous voulons afficher le lien ailleurs, par exemple dans du texte, nous utilisons `{link}` : - -```latte -L'adresse est : {link Home:default} -``` - -Plus d'informations peuvent être trouvées dans le chapitre [Création de liens URL|creating-links]. - - -Filtres personnalisés, balises, etc. ------------------------------------- - -Le système de templates Latte peut être étendu avec des filtres, des fonctions, des balises et d'autres éléments personnalisés. Trois approches sont possibles, de la solution ad hoc rapide aux motifs architecturaux valables pour toute l'application. - -**Ad hoc, dans les méthodes du presenter** - -L'approche la plus rapide consiste à ajouter les filtres ou les fonctions directement dans le code du presenter ou du composant. Dans les presenters, les méthodes `beforeRender()` ou `render()` s'y prêtent bien : - -```php -protected function beforeRender(): void -{ - // ajout d'un filtre - $this->template->addFilter('money', fn($val) => '$' . number_format($val, 2)); - - // ajout d'une fonction - $this->template->addFunction('isWeekend', fn($date) => $date->format('N') >= 6); -} -``` - -Dans le template : - -```latte -

    Price: {$price|money}

    - -{if isWeekend($now)} ... {/if} -``` - -Pour une logique plus complexe, vous pouvez configurer directement l'objet `Latte\Engine` : - -```php -protected function beforeRender(): void -{ - $latte = $this->template->getLatte(); - $latte->setFeature(Latte\Feature::MigrationWarnings); -} -``` - -**À l'aide d'attributs** - -Une approche plus élégante consiste à définir les filtres et les fonctions comme méthodes directement dans la [classe de paramètres de template|#Templates typés] du presenter ou du composant, marquées par des attributs : - -```php -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - #[Latte\Attributes\TemplateFilter] - public function money(float $val): string - { - return '$' . number_format($val, 2); - } - - #[Latte\Attributes\TemplateFunction] - public function isWeekend(DateTimeInterface $date): bool - { - return $date->format('N') >= 6; - } -} -``` - -Latte découvre et enregistre automatiquement les méthodes marquées par ces attributs. Le nom du filtre ou de la fonction dans les templates correspond au nom de la méthode. Ces méthodes doivent être publiques. - -**Globalement, à l'aide d'extensions** - -Les approches précédentes conviennent aux filtres et fonctions dont vous n'avez besoin que dans certains presenters ou composants, pas dans toute l'application. Pour l'application entière, le mieux est de créer une [extension |latte:extending-latte#Latte Extension]. Cette classe centralise toutes les extensions de Latte de votre projet. Exemple succinct : - -```php -namespace App\Presentation\Accessory; - -final class LatteExtension extends Latte\Extension -{ - public function __construct( - private App\Model\Facade $facade, - private Nette\Security\User $user, - // ... - ) { - } - - public function getFilters(): array - { - return [ - 'timeAgoInWords' => $this->filterTimeAgoInWords(...), - 'money' => $this->filterMoney(...), - // ... - ]; - } - - public function getFunctions(): array - { - return [ - 'canEditArticle' => - fn($article) => $this->facade->canEditArticle($article, $this->user->getId()), - // ... - ]; - } - - private function filterTimeAgoInWords(DateTimeInterface $time): string - { - // ... - } - - // ... -} -``` - -Enregistrez l'extension via la [configuration |configuration#Templates Latte] : - -```neon -latte: - extensions: - - App\Presentation\Accessory\LatteExtension -``` - -Les extensions offrent plusieurs avantages : la prise en charge de l'injection de dépendances, l'accès à la couche modèle de votre application et la gestion centralisée de toutes les extensions. Elles prennent aussi en charge les balises personnalisées, les providers, les passes de compilation et bien d'autres choses. - - -Configurer tous les templates ------------------------------ - -Le service `TemplateFactory`, qui crée tous les templates, expose un tableau public de callbacks `$onCreate`. Ils sont appelés à chaque création d'un template : depuis un seul endroit, vous pouvez donc définir des filtres, des fonctions ou des variables pour tous les templates de l'application. Chaque callback reçoit le template fraîchement créé. Faites-vous [injecter |dependency-injection:passing-dependencies] le service `TemplateFactory` et enregistrez les callbacks, par exemple au démarrage de l'application : - -```php -$templateFactory->onCreate[] = function (Nette\Bridges\ApplicationLatte\Template $template): void { - $template->addFilter('money', fn($val) => '$' . number_format($val, 2)); -}; -``` - - -Traduction ----------- - -Si vous programmez une application multilingue, vous aurez probablement besoin d'afficher certains textes dans le template dans différentes langues. Nette Framework définit à cet effet une interface pour la traduction [api:Nette\Localization\Translator], qui a une seule méthode `translate()`. Elle accepte un message `$message`, qui est généralement une chaîne, et tout autre paramètre. La tâche est de retourner la chaîne traduite. Il n'y a pas d'implémentation par défaut dans Nette, vous pouvez choisir parmi plusieurs solutions prêtes à l'emploi selon vos besoins, que vous trouverez sur [Componette |https://componette.org/search/localization]. Dans leur documentation, vous apprendrez comment configurer le traducteur. - -Il est possible de définir un traducteur pour les templates, que nous [recevrons via injection |dependency-injection:passing-dependencies], avec la méthode `setTranslator()` : - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator); -} -``` - -Le traducteur peut alternativement être défini via la [configuration |configuration#Templates Latte] : - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -Ensuite, le traducteur peut être utilisé par exemple comme filtre `|translate`, y compris avec des paramètres supplémentaires qui sont passés à la méthode `translate()` (voir `foo, bar`) : - -```latte -
    {='Panier'|translate} -{$item|translate} -{$item|translate, foo, bar} -``` - -Ou comme balise soulignée : - -```latte -{_'Panier'} -{_$item} -{_$item, foo, bar} -``` - -Pour traduire une section du template, il existe une balise paire `{translate}` (depuis Latte 2.11, auparavant la balise `{_}` était utilisée) : - -```latte -{translate}Commande{/translate} -{translate foo, bar}Commande{/translate} -``` - -Le traducteur est appelé par défaut à l'exécution lors du rendu du template. Cependant, Latte version 3 peut traduire tous les textes statiques déjà pendant la compilation du template. Cela économise des performances, car chaque chaîne n'est traduite qu'une seule fois et la traduction résultante est écrite dans la forme compilée. Ainsi, plusieurs versions compilées du template sont créées dans le répertoire cache, une pour chaque langue. Pour cela, il suffit d'indiquer la langue comme deuxième paramètre : - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator, $lang); -} -``` - -Par texte statique, on entend par exemple `{_'hello'}` ou `{translate}hello{/translate}`. Les textes non statiques, comme par exemple `{_$foo}`, continueront d'être traduits à l'exécution. diff --git a/application/fr/upgrading.texy b/application/fr/upgrading.texy deleted file mode 100644 index 0df1a3b5ea..0000000000 --- a/application/fr/upgrading.texy +++ /dev/null @@ -1,47 +0,0 @@ -Mise à niveau -************* - - -Mise à niveau vers la version 3.0 -================================= - -Nette 3.0 ajoute des déclarations de type aux paramètres et aux valeurs de retour des méthodes. Si vous redéfinissez une telle méthode dans une classe héritant de Nette (dans un presenter ou un composant, par exemple), vous devez y ajouter les mêmes déclarations de type, sans quoi PHP lève une erreur "Declaration must be compatible". - -L'interface `Nette\Application\IRouter` a changé. La méthode `match()` retourne désormais un tableau de paramètres, et `constructUrl()` en accepte un, au lieu d'un objet `Nette\Application\Request`. - -Nette vérifie désormais que chaque signal est envoyé depuis la même origine (c'est-à-dire le même domaine et le même sous-domaine). Cette politique de même origine est un mécanisme de sécurité essentiel, qui aide à réduire les vecteurs d'attaque possibles. Si vous voulez autoriser d'autres origines, ajoutez l'annotation `@crossOrigin` à la méthode de traitement : - -```php -/** - * @crossOrigin - */ -public function handleXy(): void -{ -} -``` - -Cela vaut aussi pour l'envoi des formulaires. Si vous voulez autoriser l'envoi depuis d'autres origines, procédez ainsi : - -```php -$form = new Nette\Application\UI\Form; -$form->allowCrossOrigin(); -``` - -Le constructeur de `Nette\ComponentModel\Component` n'était plus utilisé depuis des années et a été supprimé en version 3.0. C'est une rupture de compatibilité : si vous appelez le constructeur parent dans un composant ou un presenter héritant de `Nette\Application\UI\Presenter`, vous devez retirer cet appel. - - -Mise à niveau vers la version 2.4 -================================= - -- `Route` et `SimpleRouter` génèrent désormais le même schéma HTTP/HTTPS que celui par lequel le site a été atteint. Une route qui exige un protocole précis peut être définie avec le schéma, par ex. `Route('http://domain.cz/')`. -- Pour les paramètres de type bool des méthodes render/action (c'est-à-dire ceux dont la valeur par défaut est true ou false) et pour les paramètres persistants, `false` et `null` sont désormais distingués. Si le paramètre est absent de l'URL, sa valeur est maintenant `null` (auparavant `false`). -- La classe retournée par `Presenter::getReflection()` n'est plus un descendant de `Nette\Reflection\ClassType`, et `getReflection()->getMethod()` n'est plus un descendant de `Nette\Reflection\Method`. -- Le drapeau `SECURED` et `Route::$defaultFlags` sont dépréciés. - - -Mise à niveau vers la version 2.3 -================================= - -- les routes et les noms de presenters sont **sensibles à la casse**. Nette vous avertit si vous employez une mauvaise casse dans un nom de presenter ; pour des raisons de performance, le masque de Route n'est pas vérifié, contrôlez-le donc vous-même. -- `Route::addStyle()` et `Route::setStyleProperty()` sont dépréciées et déclenchent désormais `E_USER_DEPRECATED`. -- l'extension de template `.phtml` et l'ancienne syntaxe des liens ne sont plus prises en charge. diff --git a/application/it/@home.texy b/application/it/@home.texy deleted file mode 100644 index 3a4b81e569..0000000000 --- a/application/it/@home.texy +++ /dev/null @@ -1,85 +0,0 @@ -Nette Application -***************** - -.[perex] -Nette Application è il cuore del framework Nette e offre strumenti potenti per creare applicazioni web moderne. Mette a disposizione una serie di funzionalità eccezionali, che semplificano notevolmente lo sviluppo e migliorano la sicurezza e la manutenibilità del codice. - - -Installazione -------------- - -Scaricate e installate la libreria con [Composer|best-practices:composer]: - -```shell -composer require nette/application -``` - - -Perché scegliere Nette Application? ------------------------------------ - -Nette è sempre stata pioniera nelle tecnologie web. - -**Router bidirezionale:** Nette dispone di un sistema di routing avanzato, unico nella sua bidirezionalità: non solo traduce gli URL in azioni dell'applicazione, ma sa anche generare gli URL in senso inverso. Questo significa che: -- potete cambiare in qualsiasi momento la struttura degli URL dell'intera applicazione senza dover modificare i template -- gli URL vengono canonizzati automaticamente, il che migliora la SEO -- il routing è definito in un unico punto, non sparso nelle annotazioni - -**Componenti e segnali:** il sistema di componenti integrato, ispirato a Delphi e a React.js, è unico tra i framework PHP: -- permette di creare elementi di interfaccia riutilizzabili -- supporta la composizione gerarchica dei componenti -- offre una gestione elegante delle richieste AJAX tramite i segnali -- ricca libreria di componenti già pronti su [Componette](https://componette.org) - -**AJAX e snippet:** nel 2009 Nette ha introdotto un modo rivoluzionario di lavorare con AJAX, molto prima di soluzioni simili come Hotwire per Ruby on Rails o Symfony UX Turbo: -- gli snippet permettono di aggiornare solo parti della pagina senza dover scrivere JavaScript -- integrazione automatica con il sistema di componenti -- invalidazione intelligente delle sezioni della pagina -- trasferimento minimo di dati - -**Template [Latte|latte:] intuitivi:** il sistema di template più sicuro per PHP, con funzionalità avanzate: -- protezione automatica contro l'XSS con escaping sensibile al contesto -- estensibile con filtri, funzioni e tag personalizzati -- ereditarietà dei template e snippet per AJAX -- ottimo supporto di PHP 8.x e del sistema di tipi - -**Dependency Injection:** Nette sfrutta pienamente la dependency injection: -- passaggio automatico delle dipendenze (autowiring) -- configurazione nel chiaro formato NEON -- supporto per le factory dei componenti - - -Vantaggi principali -------------------- - -- **Sicurezza**: protezione automatica contro le [vulnerabilità|nette:vulnerability-protection] come XSS, CSRF e simili -- **Produttività**: meno codice da scrivere e più funzionalità, grazie a una progettazione intelligente -- **Debugging**: il [debugger Tracy|tracy:] con il pannello del routing -- **Prestazioni**: cache intelligente, caricamento pigro dei componenti -- **Flessibilità**: modifica semplice degli URL anche a lavoro finito -- **Componenti**: un sistema unico di elementi di interfaccia riutilizzabili -- **Modernità**: pieno supporto di PHP 8.3+ e del sistema di tipi - - -Primi passi ------------ - -1. [Come funzionano le applicazioni? |how-it-works] - comprendere l'architettura di base -2. [Presenter |presenters] - lavorare con i presenter e le azioni -3. [Template |templates] - creare template in Latte -4. [Routing |routing] - configurare gli indirizzi URL -5. [Componenti interattivi |components] - usare il sistema di componenti - - -Compatibilità con PHP ---------------------- - -| versione | compatibile con PHP -|-----------------------|------------------- -| Nette Application 3.3 | PHP 8.3 - 8.5 -| Nette Application 3.2 | PHP 8.1 - 8.5 -| 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 - -Vale per le ultime versioni patch. diff --git a/application/it/@left-menu.texy b/application/it/@left-menu.texy deleted file mode 100644 index 9db6fbd67b..0000000000 --- a/application/it/@left-menu.texy +++ /dev/null @@ -1,24 +0,0 @@ -Nette Application -***************** -- [Panoramica |@home] -- [Come funzionano le applicazioni? |how-it-works] -- [Bootstrapping|bootstrapping] -- [Presenter|presenters] -- [Template|templates] -- [Struttura delle directory |directory-structure] -- [Routing|routing] -- [Creazione di link URL |creating-links] -- [Componenti interattivi |components] -- [AJAX e snippet |ajax] -- [Multiplier |multiplier] -- [Configurazione|configuration] -- [Aggiornamento|upgrading] - - -Letture consigliate -******************* -- [Perché usare Nette?|www:10-reasons-why-nette] -- [Installazione |nette:installation] -- [Crea la tua prima applicazione! |quickstart:] -- [Best practice |best-practices:] -- [Risoluzione dei problemi |nette:troubleshooting] diff --git a/application/it/@meta.texy b/application/it/@meta.texy deleted file mode 100644 index 4647d0c8a2..0000000000 --- a/application/it/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentazione Nette}} diff --git a/application/it/ajax.texy b/application/it/ajax.texy deleted file mode 100644 index 624bfe0181..0000000000 --- a/application/it/ajax.texy +++ /dev/null @@ -1,265 +0,0 @@ -AJAX e snippet -************** - -
    - -Nell'era delle applicazioni web moderne, in cui le funzionalità sono spesso ripartite tra il server e il browser, AJAX è l'elemento di collegamento indispensabile. Quali possibilità offre Nette Framework in questo campo? -- l'invio di parti del template, i cosiddetti snippet -- il passaggio di variabili tra PHP e JavaScript -- strumenti per il debugging delle richieste AJAX - -
    - - -Richiesta AJAX -============== - -Una richiesta AJAX non differisce sostanzialmente da una classica richiesta HTTP. Viene chiamato un presenter con determinati parametri. Sta al presenter decidere come rispondere alla richiesta: può restituire dati in formato JSON, inviare una parte di codice HTML, un documento XML e così via. - -Sul lato browser avviamo una richiesta AJAX con la funzione `fetch()`: - -```js -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -.then(response => response.json()) -.then(payload => { - // elabora la risposta -}); -``` - -Sul lato server una richiesta AJAX si riconosce con il metodo `$httpRequest->isAjax()` del servizio che [incapsula la richiesta HTTP |http:request]. Per il rilevamento usa l'header HTTP `X-Requested-With`, quindi è essenziale inviarlo. Dentro il presenter potete usare il metodo `$this->isAjax()`. - -Se volete inviare dati in formato JSON, usate il metodo [`sendJson()` |presenters#Inviare una risposta]. Il metodo termina anche l'attività del presenter. - -```php -public function actionExport(): void -{ - $this->sendJson($this->model->getData()); -} -``` - -Se avete in programma di rispondere con un template speciale pensato per AJAX, potete farlo così: - -```php -public function handleClick($param): void -{ - if ($this->isAjax()) { - $this->template->setFile('path/to/ajax.latte'); - } - // ... -} -``` - - -Snippet -======= - -Lo strumento più potente che Nette offre per collegare il server al client sono gli snippet. Con essi potete trasformare una normale applicazione in una applicazione AJAX con il minimo sforzo e poche righe di codice. L'esempio Fifteen mostra come funziona il tutto e il suo codice si trova su [GitHub |https://github.com/nette-examples/fifteen]. - -Gli snippet permettono di aggiornare solo parti della pagina, invece di ricaricarla per intero. Non è solo più veloce ed efficiente, ma offre anche un'esperienza d'uso più comoda. Gli snippet vi ricorderanno forse Hotwire per Ruby on Rails o Symfony UX Turbo. È curioso che Nette abbia introdotto gli snippet 14 anni prima. - -Come funzionano gli snippet? Al primo caricamento della pagina (una richiesta non AJAX) viene caricata l'intera pagina, snippet compresi. Quando l'utente interagisce con la pagina (per esempio clicca un pulsante, invia un form ecc.), invece di ricaricare l'intera pagina viene avviata una richiesta AJAX. Il codice del presenter esegue l'azione e decide quali snippet vanno aggiornati. Nette disegna questi snippet e li invia come payload JSON contenente un array con gli snippet. Il codice che li gestisce nel browser inserisce poi gli snippet ricevuti nella pagina. Viene quindi trasferito solo il codice degli snippet cambiati, il che risparmia banda e accelera il caricamento rispetto al trasferimento dell'intero contenuto della pagina. Se nessuno snippet viene invalidato con `redrawControl()`, Nette restituisce l'intera pagina anche per una richiesta AJAX: gli snippet vengono inviati solo quando qualcosa è stato invalidato. - - -Naja ----- - -Per gestire gli snippet sul lato browser si usa la [libreria Naja |https://naja.js.org]. [Installatela |https://naja.js.org/#/guide/01-install-setup-naja] come pacchetto Node.js (per l'uso con bundler come Webpack, Rollup, Vite, Parcel e altri): - -```shell -npm install naja -``` - -…oppure inseritela direttamente nel template della pagina: - -```latte - -``` - -Per prima cosa dovete [inizializzare |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization] la libreria: - -```js -naja.initialize(); -``` - -Per trasformare un normale link (segnale) o l'invio di un form in una richiesta AJAX, basta contrassegnare il link, il form o il pulsante con la classe `ajax`: - -```latte -Vai - -
    - -
    - -oppure - -
    - -
    -``` - - -Ridisegnare gli snippet ------------------------ - -Ogni oggetto della classe [Control |components] (compreso il presenter stesso) tiene traccia del fatto che siano avvenuti cambiamenti che ne richiedono il ridisegno. A questo scopo si usa il metodo `redrawControl()`: - -```php -public function handleLogin(string $user): void -{ - // dopo il login è necessario ridisegnare la parte interessata - $this->redrawControl(); - // ... -} -``` - -Nette permette un controllo ancora più fine su ciò che va ridisegnato. Il metodo può ricevere come argomento il nome dello snippet. È quindi possibile invalidare (cioè forzare il ridisegno) a livello di singole parti del template. Se viene invalidato l'intero componente, verranno ridisegnati anche tutti i suoi snippet: - -```php -// invalida lo snippet 'header' -$this->redrawControl('header'); -``` - -Potete anche annullare un'invalidazione in sospeso con il secondo parametro `$redraw`: chiamando `$this->redrawControl('header', redraw: false)` contrassegnate lo snippet come non bisognoso di ridisegno. La firma completa è `redrawControl(?string $snippet = null, bool $redraw = true)`. - - -Snippet in Latte ----------------- - -Usare gli snippet in Latte è estremamente semplice. Per definire una parte del template come snippet basta racchiuderla tra i tag `{snippet}` e `{/snippet}`: - -```latte -{snippet header} -

    Ciao ...

    -{/snippet} -``` - -Lo snippet crea nella pagina HTML un elemento `
    ` con un `id` speciale generato. Quando lo snippet viene ridisegnato, il contenuto di questo elemento viene aggiornato. È quindi necessario che al primo rendering della pagina vengano disegnati anche tutti gli snippet, anche se all'inizio potrebbero essere vuoti. - -Potete creare uno snippet anche con un elemento diverso da `
    `, usando un n:attributo: - -```latte -
    -

    Ciao ...

    -
    -``` - - -Aree snippet ------------- - -I nomi degli snippet possono essere anche espressioni: - -```latte -{foreach $items as $id => $item} -
  • {$item}
  • -{/foreach} -``` - -Di per sé è un passaggio intermedio non funzionante: disegnato fuori da uno `{snippet}` o `{snippetArea}` statico, uno snippet dinamico emette un `E_USER_WARNING` con il messaggio *Dynamic snippets are allowed only inside static snippet/snippetArea.* Lo sistemiamo qui sotto. - -Così vengono creati diversi snippet come `item-0`, `item-1` ecc. Se invalidassimo direttamente uno snippet dinamico (per esempio `item-1`), non verrebbe ridisegnato nulla. Il motivo è che gli snippet funzionano davvero come estratti e vengono disegnati direttamente solo loro stessi. Nel template, però, tecnicamente non esiste alcuno snippet chiamato `item-1`: esso nasce solo quando viene eseguito il codice che circonda lo snippet, cioè il ciclo foreach. Contrassegniamo quindi con il tag `{snippetArea}` la parte del template che deve essere eseguita: - -```latte -
      - {foreach $items as $id => $item} -
    • {$item}
    • - {/foreach} -
    -``` - -E chiediamo il ridisegno sia del singolo snippet sia dell'intera area genitore: - -```php -$this->redrawControl('itemsContainer'); -$this->redrawControl('item-1'); -``` - -Allo stesso tempo è opportuno assicurarsi che l'array `$items` contenga solo gli elementi da ridisegnare. - -Se nel template principale includiamo con il tag `{include}` un altro template che contiene snippet, è necessario racchiudere di nuovo l'inclusione del template in uno `snippetArea` e invalidarlo insieme allo snippet: - -```latte -{snippetArea include} - {include 'included.latte'} -{/snippetArea} -``` - -```latte -{* included.latte *} -{snippet item} - ... -{/snippet} -``` - -```php -$this->redrawControl('include'); -$this->redrawControl('item'); -``` - - -Snippet nei componenti ----------------------- - -Potete creare snippet dentro i [componenti|components] e Nette li ridisegnerà automaticamente. C'è però un limite: per ridisegnare gli snippet Nette chiama il metodo `render()` senza alcun parametro. Passare parametri nel template non funzionerà quindi: - -```latte -OK -{control productGrid} - -non funzionerà: -{control productGrid, $arg, $arg} -{control productGrid:paginator} -``` - - -Inviare dati personalizzati ---------------------------- - -Insieme agli snippet potete inviare al client qualsiasi dato aggiuntivo. Basta scriverlo nell'oggetto `payload`: - -```php -public function actionDelete(int $id): void -{ - // ... - if ($this->isAjax()) { - $this->payload->message = 'Success'; - } -} -``` - - -Redirect --------- - -Durante una richiesta AJAX i metodi `redirect()` e `redirectUrl()` non inviano un redirect HTTP. Scrivono invece l'URL di destinazione nel payload (l'oggetto dati inviato nella risposta AJAX), precisamente nella sua proprietà `payload.redirect`, e lo inviano; il redirect vero e proprio viene poi eseguito dalla libreria lato client (Naja). - - -Passare i parametri -=================== - -Quando inviamo dei parametri a un componente tramite una richiesta AJAX, siano essi parametri di segnale o parametri persistenti, dobbiamo indicarne nella richiesta il nome globale, che comprende il nome del componente. Il metodo `getParameterId()` restituisce il nome completo del parametro. - -```js -let url = new URL({link //foo!}); -url.searchParams.set({$control->getParameterId('bar')}, bar); - -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -``` - -E il metodo handle con i parametri corrispondenti nel componente: - -```php -public function handleFoo(int $bar): void -{ -} -``` - - -Letture consigliate -=================== - -- [Snippet dinamici |best-practices:dynamic-snippets] diff --git a/application/it/bootstrapping.texy b/application/it/bootstrapping.texy deleted file mode 100644 index ab11b76910..0000000000 --- a/application/it/bootstrapping.texy +++ /dev/null @@ -1,312 +0,0 @@ -Bootstrapping -************* - -
    - -Il bootstrapping è il processo di inizializzazione dell'ambiente dell'applicazione, di creazione del container di dependency injection (DI) e di avvio dell'applicazione. Parleremo di: - -- come la classe Bootstrap inizializza l'ambiente -- come si configurano le applicazioni con i file NEON -- come distinguere tra modalità di produzione e di sviluppo -- come creare e configurare il container DI - -
    - - -Le applicazioni, siano esse web o script eseguiti dalla riga di comando, iniziano la propria esecuzione con una qualche forma di inizializzazione dell'ambiente. In passato se ne occupava un file chiamato magari `include.inc.php`, incluso dal file iniziale. Nelle moderne applicazioni Nette è stato sostituito dalla classe `Bootstrap`, che, in quanto parte dell'applicazione, si trova nel file `app/Bootstrap.php`. Potrebbe avere per esempio questo aspetto: - -```php -namespace App; - -use Nette; -use Nette\Bootstrap\Configurator; - -class Bootstrap -{ - private Configurator $configurator; - private string $rootDir; - - public function __construct() - { - $this->rootDir = dirname(__DIR__); - // il configurator si occupa di impostare l'ambiente dell'applicazione e i servizi. - $this->configurator = new Configurator; - // imposta la directory dei file temporanei generati da Nette (per esempio i template 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 - { - // Nette è furbo e la modalità di sviluppo si attiva da sola, - // oppure potete attivarla per un determinato indirizzo IP togliendo il commento alla riga seguente: - // $this->configurator->setDebugMode('secret@23.75.345.200'); - - // attiva Tracy: il "coltellino svizzero" definitivo per il debugging. - $this->configurator->enableTracy($this->rootDir . '/log'); - - // RobotLoader: carica automaticamente tutte le classi della directory scelta - $this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); - } - - private function setupContainer(): void - { - // carica i file di configurazione - $this->configurator->addConfig($this->rootDir . '/config/common.neon'); - } -} -``` - - -index.php -========= - -Nel caso delle applicazioni web il file iniziale è `index.php`, che si trova nella [directory pubblica |directory-structure#Directory pubblica www/] `www/`. Chiede alla classe Bootstrap di inizializzare l'ambiente e di creare il container DI. Poi ottiene dal container il servizio `Application`, che esegue l'applicazione web: - -```php -$bootstrap = new App\Bootstrap; -// inizializza l'ambiente e crea il container DI -$container = $bootstrap->bootWebApplication(); -// il container DI crea un oggetto Nette\Application\Application -$application = $container->getByType(Nette\Application\Application::class); -// avvia l'applicazione Nette ed elabora la richiesta in arrivo -$application->run(); -``` - -.[note] -Elaborando la richiesta, l'oggetto `$application` emette [eventi |nette:glossary#Eventi]: `onStartup`, `onRequest`, `onPresenter`, `onResponse`, `onShutdown` e `onError` (in caso di eccezione non gestita). Potete agganciarvi dei gestori, il che torna comodo per il logging o per il monitoraggio dell'intera applicazione. - -Come vedete, la classe [api:Nette\Bootstrap\Configurator] aiuta a impostare l'ambiente e a creare il container di dependency injection (DI). Ve la presentiamo ora più in dettaglio. - - -Modalità di sviluppo e di produzione -==================================== - -Nette si comporta in modo diverso a seconda che giri su un server di sviluppo o di produzione: - -🛠️ Modalità di sviluppo: - - mostra la barra di debug di Tracy con informazioni utili (query SQL, tempo di esecuzione, memoria usata) - - in caso di errore mostra una pagina di errore dettagliata, con le chiamate di funzione e il contenuto delle variabili - - aggiorna automaticamente la cache quando cambiano i template Latte, i file di configurazione ecc. - - -🚀 Modalità di produzione: - - non mostra alcuna informazione di debug; tutti gli errori vengono scritti nel log - - in caso di errore mostra un ErrorPresenter oppure una pagina generica "Server Error" - - la cache non viene mai aggiornata automaticamente! - - ottimizzata per velocità e sicurezza - - -La scelta della modalità avviene per rilevamento automatico, quindi di norma non c'è bisogno di configurare nulla né di cambiare modalità manualmente: - -- modalità di sviluppo: su localhost (indirizzo IP `127.0.0.1` o `::1`) se non è presente un proxy (cioè se non viene rilevato il suo header HTTP) -- modalità di produzione: ovunque altrove - -Se vogliamo attivare la modalità di sviluppo anche in altri casi, per esempio per i programmatori che accedono da un determinato indirizzo IP, usiamo `setDebugMode()`: - -```php -$this->configurator->setDebugMode('23.75.345.200'); // si può indicare anche un array di indirizzi IP -``` - -Consigliamo vivamente di combinare l'indirizzo IP con un cookie. Salvate un token segreto, per esempio `secret1234`, nel cookie `nette-debug` e attivate così la modalità di sviluppo per i programmatori che accedono da un determinato indirizzo IP e che hanno anche il token indicato nel cookie: - -```php -$this->configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Possiamo anche disattivare del tutto la modalità di sviluppo, perfino su localhost: - -```php -$this->configurator->setDebugMode(false); -``` - -Attenzione: il valore `true` forza l'attivazione della modalità di sviluppo, cosa che su un server di produzione non deve **mai** accadere. - -Del rilevamento automatico si occupa internamente il metodo statico `Configurator::detectDebugMode()`, che potete chiamare anche voi, per esempio per rilevare la modalità di sviluppo fuori dal configurator. Accetta un elenco facoltativo di indirizzi IP o di nomi di computer autorizzati e restituisce se la richiesta corrente deve girare in modalità di sviluppo: - -```php -$debug = Nette\Bootstrap\Configurator::detectDebugMode('23.75.345.200'); -``` - - -Lo strumento di debug Tracy -=========================== - -Per un debugging semplice attiviamo l'eccellente strumento [Tracy |tracy:]. In modalità di sviluppo visualizza gli errori, in modalità di produzione li registra nella directory indicata: - -```php -$this->configurator->enableTracy($this->rootDir . '/log'); -``` - - -File temporanei -=============== - -Nette usa la cache per il container DI, per RobotLoader, per i template ecc. È quindi necessario impostare il percorso della directory in cui la cache verrà salvata: - -```php -$this->configurator->setTempDirectory($this->rootDir . '/temp'); -``` - -Su Linux o macOS impostate i [permessi di scrittura |nette:troubleshooting#Impostazione dei permessi delle directory] per le directory `log/` e `temp/`. - - -RobotLoader -=========== - -Di solito vorremo caricare automaticamente le classi con [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 -$this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); -``` - -Un approccio alternativo è caricare le classi esclusivamente tramite [Composer |best-practices:composer], secondo PSR-4. - - -Fuso orario -=========== - -Tramite il configurator potete impostare il fuso orario predefinito. - -```php -$this->configurator->setTimeZone('Europe/Prague'); -``` - - -Configurazione del container DI -=============================== - -Parte del processo di avvio è la creazione del container DI, cioè della factory di oggetti, che è il cuore dell'intera applicazione. È in realtà una classe PHP generata da Nette e salvata nella directory della cache. La factory produce gli oggetti chiave dell'applicazione e con i file di configurazione le diciamo come crearli e impostarli, influenzando così il comportamento dell'intera applicazione. - -I file di configurazione si scrivono di norma nel [formato NEON |neon:format]. In un capitolo a parte potete leggere [cosa si può configurare |nette:configuring]. - -.[tip] -In modalità di sviluppo il container viene aggiornato automaticamente ogni volta che cambiano il codice o i file di configurazione. In modalità di produzione viene generato una sola volta e le modifiche non vengono controllate, per massimizzare le prestazioni. - -Mentre `createContainer()` costruisce il container e ne restituisce l'istanza, il metodo `loadContainer()` restituisce solo il nome della classe del container generata, che potete poi istanziare voi. È utile negli scenari avanzati. - -I file di configurazione si caricano con `addConfig()`: - -```php -$this->configurator->addConfig($this->rootDir . '/config/common.neon'); -``` - -Se vogliamo aggiungere altri file di configurazione, possiamo chiamare la funzione `addConfig()` più volte. - -```php -$configDir = $this->rootDir . '/config'; -$this->configurator->addConfig($configDir . '/common.neon'); -$this->configurator->addConfig($configDir . '/services.neon'); -if (PHP_SAPI === 'cli') { - $this->configurator->addConfig($configDir . '/cli.php'); -} -``` - -Il nome `cli.php` non è un errore di battitura: la configurazione si può scrivere anche in un file PHP che la restituisce come array. - -Possiamo aggiungere altri file di configurazione anche nella [sezione `includes` |dependency-injection:configuration#Inclusione di file]. - -Se nei file di configurazione compaiono elementi con le stesse chiavi, essi verranno sovrascritti oppure, nel caso degli [array, uniti |dependency-injection:configuration#Unione]. Un file incluso più tardi ha priorità maggiore rispetto al precedente. Il file in cui è indicata la sezione `includes` ha priorità maggiore rispetto ai file inclusi al suo interno. - - -Parametri statici ------------------ - -I parametri usati nei file di configurazione si possono definire [nella sezione `parameters` |dependency-injection:configuration#Parametri] e anche passare (o sovrascrivere) con il metodo `addStaticParameters()` (il cui vecchio alias, ora deprecato, è `addParameters()`). È importante sapere che valori diversi dei parametri provocano la generazione di altri container DI, cioè di altre classi. - -```php -$this->configurator->addStaticParameters([ - 'projectId' => 23, -]); -``` - -Nella configurazione si può fare riferimento al parametro `projectId` con la notazione consueta `%projectId%`. - - -Parametri dinamici ------------------- - -Al container possiamo aggiungere anche parametri dinamici, i cui valori diversi, a differenza dei parametri statici, non provocano la generazione di nuovi container DI. - -```php -$this->configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Così possiamo aggiungere facilmente, per esempio, le variabili d'ambiente, alle quali si può poi fare riferimento nella configurazione con la notazione `%env.variabile%`. - -```php -$this->configurator->addDynamicParameters([ - 'env' => getenv(), -]); -``` - - -Parametri predefiniti ---------------------- - -Nei file di configurazione potete usare questi parametri: - -- `%appDir%` è il percorso assoluto della directory che contiene il file `Bootstrap.php` -- `%wwwDir%` è il percorso assoluto della directory che contiene il file d'ingresso `index.php` -- `%tempDir%` è il percorso assoluto della directory dei file temporanei -- `%vendorDir%` è il percorso assoluto della directory in cui Composer installa le librerie -- `%rootDir%` è il percorso assoluto della directory radice del progetto -- `%baseUrl%` è l'URL assoluto della directory radice (un parametro dinamico, risolto in fase di esecuzione) -- `%debugMode%` indica se l'applicazione è in modalità debug -- `%consoleMode%` indica se la richiesta è arrivata dalla riga di comando - - -Servizi importati ------------------ - -Ora andiamo più a fondo. Benché lo scopo del container DI sia creare oggetti, di tanto in tanto può nascere l'esigenza di inserire nel container un oggetto già esistente. Lo facciamo definendo il servizio con il flag `imported: true`. - -```neon -services: - myservice: - type: App\Model\MyCustomService - imported: true -``` - -E nel bootstrap inseriamo l'oggetto nel container: - -```php -$this->configurator->addServices([ - 'myservice' => new App\Model\MyCustomService('foobar'), -]); -``` - - -Ambienti diversi -================ - -Modificate pure la classe `Bootstrap` secondo le vostre esigenze. Potete aggiungere parametri al metodo `bootWebApplication()` per distinguere tra progetti web. Oppure possiamo aggiungere altri metodi, come `bootTestEnvironment()`, che inizializza l'ambiente per i test unitari, `bootConsoleApplication()` per gli script chiamati dalla riga di comando ecc. - -```php -public function bootTestEnvironment(): Nette\DI\Container -{ - Tester\Environment::setup(); // inizializzazione di Nette Tester - $this->setupContainer(); - return $this->configurator->createContainer(); -} - -public function bootConsoleApplication(): Nette\DI\Container -{ - $this->configurator->setDebugMode(false); - $this->initializeEnvironment(); - $this->setupContainer(); - return $this->configurator->createContainer(); -} -``` diff --git a/application/it/components.texy b/application/it/components.texy deleted file mode 100644 index 37413b7604..0000000000 --- a/application/it/components.texy +++ /dev/null @@ -1,504 +0,0 @@ -Componenti interattivi -********************** - -
    - -I componenti sono oggetti riutilizzabili autonomi che incorporiamo nelle pagine. Possono essere form, datagrid, sondaggi, in pratica tutto ciò che ha senso usare più volte. Vi mostreremo: - -- come usare i componenti? -- come scriverli? -- cosa sono i segnali? - -
    - -Nette ha un sistema di componenti integrato. Qualcosa di simile sarà familiare ai veterani di Delphi o di ASP.NET Web Forms; React o Vue.js si basano su qualcosa di vagamente affine. Nel mondo dei framework PHP, però, è una funzionalità unica. - -Allo stesso tempo i componenti influenzano profondamente il modo di sviluppare le applicazioni. Potete comporre le pagine a partire da unità già pronte. Vi serve una datagrid nella vostra amministrazione? Cercatela su [Componette |https://componette.org/search/component], un repository di estensioni open source (non solo componenti) per Nette, e inseritela semplicemente nel presenter. - -Nel presenter potete incorporare quanti componenti volete. E dentro alcuni componenti potete incorporarne altri. Nasce così un albero di componenti, con il presenter come radice. - - -Metodi factory -============== - -Come si inseriscono i componenti nel presenter e come li si usa poi? Di solito tramite metodi factory. - -Una factory di componenti è un modo elegante di creare i componenti solo quando servono davvero (lazy / on demand). Tutta la magia sta nell'implementare un metodo chiamato `createComponent()`, dove `` è il nome del componente da creare, che crea e restituisce il componente. - -```php .{file:DefaultPresenter.php} -class DefaultPresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentPoll(): PollControl - { - $poll = new PollControl; - $poll->items = $this->items; - return $poll; - } -} -``` - -Poiché tutti i componenti vengono creati in metodi separati, il codice risulta più chiaro. - -.[note] -I nomi dei componenti iniziano sempre con una lettera minuscola, anche se nel nome del metodo sono scritti con la maiuscola. - -Non chiamiamo mai le factory direttamente: vengono chiamate automaticamente la prima volta che usiamo il componente. Grazie a questo il componente viene creato al momento giusto e solo se serve davvero. Se non usiamo il componente (per esempio durante una richiesta AJAX in cui viene trasferita solo una parte della pagina, oppure quando il template è in cache), non verrà creato affatto, risparmiando prestazioni del server. - -```php .{file:DefaultPresenter.php} -// accediamo al componente e, se è la prima volta, -// viene chiamato createComponentPoll(), che lo crea -$poll = $this->getComponent('poll'); -// sintassi alternativa: $poll = $this['poll']; -``` - -Nel template è possibile disegnare un componente con il tag [{control} |#Rendering]. Non c'è quindi bisogno di passare manualmente i componenti al template. - -```latte -

    Vota

    - -{control poll} -``` - -.[tip] -Per creare dinamicamente un numero variabile di componenti usate [Multiplier |multiplier]. - -I metodi factory `createComponent()` non funzionano solo nei presenter. Allo stesso modo potete annidare un componente dentro un altro componente, componendoli in un albero: comodo per esempio per un form disegnato separatamente dentro un componente. - - -Stile hollywoodiano -=================== - -I componenti usano abitualmente una tecnica fresca che ci piace chiamare stile hollywoodiano. Conoscete di sicuro il cliché che sentono spesso i partecipanti ai provini cinematografici: "Non chiamateci, vi chiameremo noi." Ed è esattamente di questo che si tratta. - -In Nette, invece di dover porre continuamente domande ("il form è stato inviato?", "era valido?", "l'utente ha premuto questo pulsante?"), dite al framework "quando succede questo, chiama questo metodo" e gli lasciate il resto del lavoro. Se programmate in JavaScript conoscete benissimo questo stile di programmazione: scrivete funzioni che vengono chiamate quando si verifica un certo evento. E il linguaggio passa loro i parametri appropriati. - -Questo cambia completamente la prospettiva sullo scrivere applicazioni. Più compiti riuscite a lasciare al framework, meno lavoro avete. E meno cose potete trascurare. - - -Scrivere un componente -====================== - -Con il termine componente intendiamo di norma un discendente della classe [api:Nette\Application\UI\Control]. (Sarebbe più preciso usare il termine "control", ma in alcune lingue ha un significato diverso e "componente" si è affermato di più.) Anche il presenter [api:Nette\Application\UI\Presenter] è un discendente della classe `Control`. - -```php .{file:PollControl.php} -use Nette\Application\UI\Control; - -class PollControl extends Control -{ -} -``` - - -Rendering -========= - -Sappiamo già che per disegnare un componente si usa il tag `{control nomeComponente}`. In realtà esso chiama il metodo `render()` del componente, nel quale ci occupiamo del rendering. Abbiamo a disposizione, come nel presenter, un [template Latte|templates] nella variabile `$this->template`, alla quale passiamo i parametri. A differenza del presenter, dobbiamo indicare il file del template e farlo disegnare: - -```php .{file:PollControl.php} -public function render(): void -{ - // inserisce alcuni parametri nel template - $this->template->param = $value; - // e lo disegna - $this->template->render(__DIR__ . '/poll.latte'); -} -``` - -Il tag `{control}` permette di passare parametri al metodo `render()`: - -```latte -{control poll, $id, $message} -``` - -```php .{file:PollControl.php} -public function render(int $id, string $message): void -{ - // ... -} -``` - -A volte un componente può essere composto da più parti che vogliamo disegnare separatamente. Per ognuna di esse creiamo un proprio metodo di rendering, qui nell'esempio `renderPaginator()`: - -```php .{file:PollControl.php} -public function renderPaginator(): void -{ - // ... -} -``` - -E nel template lo richiamiamo poi così: - -```latte -{control poll:paginator} -``` - -Per capire meglio è utile sapere come questo tag si traduce in codice PHP. - -```latte -{control poll} -{control poll:paginator, 123, 'hello'} -``` - -si traduce in: - -```php -$control->getComponent('poll')->render(); -$control->getComponent('poll')->renderPaginator(123, 'hello'); -``` - -Il metodo `getComponent()` restituisce il componente `poll` e su questo componente viene chiamato il metodo `render()`, oppure `renderPaginator()` se nel tag, dopo i due punti, è indicato un metodo di rendering diverso. - -.[caution] -Attenzione: se nei parametri compare **`=>`** fuori dalle parentesi quadre, tutti i parametri verranno racchiusi in un array e passati come primo argomento: - -```latte -{control poll, id: 123, message: 'hello'} -``` - -si traduce in: - -```php -$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']); -``` - -Rendering di un sottocomponente: - -```latte -{control cartControl-someForm} -``` - -si traduce in: - -```php -$control->getComponent("cartControl-someForm")->render(); -``` - -I componenti, come i presenter, passano automaticamente ai template alcune variabili utili: - -- `$basePath` è il percorso URL assoluto della directory radice (per esempio `/eshop`) -- `$baseUrl` è l'URL assoluto della directory radice (per esempio `http://localhost/eshop`) -- `$user` è un oggetto che [rappresenta l'utente |security:authentication] -- `$presenter` è il presenter corrente -- `$control` è il componente corrente -- `$flashes` è un array dei [messaggi |#Messaggi flash] inviati dalla funzione `flashMessage()` - - -Segnale -======= - -Sappiamo già che la navigazione in un'applicazione Nette consiste nel collegare o nel reindirizzare verso coppie `Presenter:azione`. Ma cosa succede se vogliamo solo eseguire un'azione sulla **pagina corrente**? Per esempio cambiare l'ordinamento delle colonne di una tabella, eliminare un elemento, passare dalla modalità chiara a quella scura, inviare un form, votare in un sondaggio e così via. - -Questo tipo di richiesta si chiama segnale. E come le azioni richiamano i metodi `action()` o `render()`, i segnali chiamano i metodi `handle()`. Mentre il concetto di azione (o vista) riguarda esclusivamente i presenter, i segnali riguardano tutti i componenti. E quindi anche i presenter, perché `UI\Presenter` è un discendente di `UI\Control`. - -```php -public function handleClick(int $x, int $y): void -{ - // ... elaborazione del segnale ... -} -``` - -Un link che chiama un segnale si crea nel modo consueto, cioè nel template con l'attributo `n:href` o con il tag `{link}`, nel codice con il metodo `link()`. Maggiori informazioni nel capitolo [Creazione di link URL |creating-links#Link a un segnale]. - -```latte -clicca qui -``` - -Un segnale viene sempre chiamato sul presenter e sull'azione correnti; non è possibile richiamarlo su un altro presenter o su un'altra azione. - -Un segnale provoca quindi il ricaricamento della pagina esattamente come la richiesta originale, ma in più chiama il metodo di gestione del segnale con i parametri appropriati. Se il metodo non esiste, viene sollevata l'eccezione [api:Nette\Application\UI\BadSignalException], che viene mostrata all'utente come pagina di errore 403 Forbidden. - - -Snippet e AJAX -============== - -I segnali vi ricorderanno un po' AJAX: handler richiamati sulla pagina corrente. E avete ragione, i segnali vengono davvero chiamati spesso tramite AJAX e, di conseguenza, al browser vengono trasferite solo le parti della pagina che sono cambiate. Sono i cosiddetti snippet. Maggiori informazioni si trovano nella [pagina dedicata ad AJAX |ajax]. - - -Messaggi flash -============== - -Un componente ha un proprio archivio di messaggi flash, indipendente da quello del presenter. Sono messaggi che, per esempio, informano sull'esito di un'operazione. Una caratteristica importante dei messaggi flash è che sono disponibili nel template anche dopo un redirect. Anche dopo essere stati mostrati restano attivi per altri 30 secondi, per esempio nel caso in cui l'utente ricarichi la pagina per un errore di trasmissione: il messaggio non sparisce subito. - -Dell'invio si occupa il metodo [flashMessage |api:Nette\Application\UI\Control::flashMessage()]. Il primo parametro è il testo del messaggio (`string`, `Stringable`) oppure un oggetto `stdClass` che rappresenta il messaggio. Il secondo parametro facoltativo è il suo tipo (error, warning, info ecc.). Il metodo `flashMessage()` restituisce un'istanza del messaggio flash come oggetto `stdClass`, al quale si possono aggiungere altre informazioni. - -```php -$this->flashMessage('Item was deleted.'); -$this->redirect(/* ... */); // e reindirizza -``` - -Questi messaggi sono disponibili nel template nella variabile `$flashes` come oggetti `stdClass`, che contengono le proprietà `message` (testo del messaggio), `type` (tipo del messaggio) e possono contenere le informazioni aggiunte dall'utente. Li disegniamo per esempio così: - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -Redirect dopo l'elaborazione di un segnale -========================================== - -L'elaborazione del segnale di un componente è spesso seguita da un redirect. È come per i form: dopo il loro invio reindirizziamo, per impedire il reinvio dei dati se la pagina viene ricaricata nel browser. - -```php -$this->redirect('this'); // reindirizza al presenter e all'azione correnti -``` - -Poiché un componente è un elemento riutilizzabile e di norma non dovrebbe avere un legame diretto con presenter specifici, i metodi `redirect()` e `link()` interpretano automaticamente il parametro come un segnale del componente: - -```php -$this->redirect('click'); // reindirizza al segnale 'click' dello stesso componente -``` - -Se avete bisogno di reindirizzare a un altro presenter o a un'altra azione, potete farlo tramite il presenter: - -```php -$this->getPresenter()->redirect('Product:show'); // reindirizza a un altro presenter/azione -``` - - -Parametri persistenti -===================== - -I parametri persistenti servono a mantenere lo stato nei componenti tra richieste diverse. Il loro valore resta lo stesso anche dopo aver cliccato un link. A differenza dei dati di sessione, vengono trasferiti nell'URL. E questo avviene in modo completamente automatico, compresi i link creati in altri componenti della stessa pagina. - -Avete per esempio un componente per la paginazione dei contenuti. Su una pagina possono essercene diversi. E vogliamo che tutti i componenti restino sulla propria pagina corrente dopo aver cliccato un link. Rendiamo quindi il numero di pagina (`page`) un parametro persistente. - -Creare un parametro persistente in Nette è estremamente semplice. Basta creare una proprietà pubblica e contrassegnarla con l'attributo: (in precedenza si usava `/** @persistent */`) - -```php -use Nette\Application\Attributes\Persistent; // questa riga è importante - -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; // deve essere public -} -``` - -Consigliamo di indicare il tipo di dato della proprietà (per esempio `int`) e potete anche fornire un valore predefinito. I valori dei parametri si possono [validare |#Validazione dei parametri persistenti]. - -Quando si crea un link, il valore di un parametro persistente si può cambiare: - -```latte -successiva -``` - -Oppure lo si può *azzerare*, cioè rimuovere dall'URL. Assumerà allora il proprio valore predefinito: - -```latte -azzera -``` - - -Componenti persistenti -====================== - -Non solo i parametri, ma anche i componenti possono essere persistenti. I loro parametri persistenti vengono allora trasferiti anche tra azioni diverse del presenter o tra più presenter. Contrassegniamo i componenti persistenti con un attributo sulla classe del presenter. Contrassegniamo per esempio così i componenti `calendar` e `poll`: - -```php -use Nette\Application\Attributes\Persistent; - -#[Persistent('calendar', 'poll')] -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -I sottocomponenti di questi componenti non devono essere contrassegnati: diventano persistenti anch'essi. - -La vecchia annotazione `@persistent` funziona ancora, ma è deprecata ed emette un avviso: - -```php -/** - * @persistent(calendar, poll) - */ -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Componenti con dipendenze -========================= - -Come creare componenti con dipendenze senza "ingombrare" i presenter che li useranno? Grazie alle funzionalità intelligenti del container DI in Nette, come per i servizi classici, gran parte del lavoro si può lasciare al framework. - -Prendiamo l'esempio di un componente che ha una dipendenza dal servizio `PollFacade`: - -```php -class PollControl extends Control -{ - public function __construct( - private int $id, // ID del sondaggio per cui creiamo il componente - private PollFacade $facade, - ) { - } - - public function handleVote(int $voteId): void - { - $this->facade->vote($this->id, $voteId); - // ... - } -} -``` - -Se stessimo scrivendo un servizio classico non ci sarebbe nulla da discutere: il container DI si occuperebbe invisibilmente di passare tutte le dipendenze. Con i componenti, però, di norma li gestiamo creando una nuova istanza direttamente nel presenter, nei [metodi factory |#Metodi factory] `createComponent…()`. Ma passare al presenter tutte le dipendenze di tutti i componenti solo per girarle ai componenti è scomodo. E quanto codice da scrivere... - -La domanda logica è: perché non registriamo semplicemente il componente come servizio classico, non lo passiamo al presenter e non lo restituiamo nel metodo `createComponent…()`? Questo approccio è però inadatto, perché vogliamo poter creare il componente più volte, se serve. - -La soluzione corretta è scrivere una factory per il componente, cioè una classe che crei il componente al posto nostro: - -```php -class PollControlFactory -{ - public function __construct( - private PollFacade $facade, - ) { - } - - public function create(int $id): PollControl - { - return new PollControl($id, $this->facade); - } -} -``` - -Registriamo questa factory nel nostro container nella configurazione: - -```neon -services: - - PollControlFactory -``` - -e infine la usiamo nel nostro presenter: - -```php -class PollPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private PollControlFactory $pollControlFactory, - ) { - } - - protected function createComponentPollControl(): PollControl - { - $pollId = 1; // possiamo passare il nostro parametro - return $this->pollControlFactory->create($pollId); - } -} -``` - -La cosa bella è che Nette DI sa [generare |dependency-injection:factory] factory semplici come questa, quindi invece di scriverne tutto il codice vi basta scriverne l'interfaccia: - -```php -interface PollControlFactory -{ - public function create(int $id): PollControl; -} -``` - -E questo è tutto. Nette implementa internamente questa interfaccia e la inietta nel presenter, dove possiamo usarla. Aggiunge come per magia al nostro componente il parametro `$id` e un'istanza della classe `PollFacade`. - - -I componenti in profondità -========================== - -I componenti in Nette Application rappresentano parti riutilizzabili di un'applicazione web che incorporiamo nelle pagine, ed è a essi che è dedicato tutto questo capitolo. Quali sono esattamente le capacità di un componente del genere? - -1) È disegnabile in un template -2) Sa [quale parte di sé |ajax#Snippet] disegnare durante una richiesta AJAX (snippet) -3) Ha la capacità di salvare il proprio stato nell'URL (parametri persistenti) -4) Ha la capacità di reagire alle azioni dell'utente (segnali) -5) Crea una struttura gerarchica (la cui radice è il presenter) - -Di ognuna di queste funzioni si occupa una delle classi della catena di ereditarietà. Del rendering (1 + 2) si occupa [api:Nette\Application\UI\Control], dell'integrazione nel [ciclo di vita |presenters#Ciclo di vita del presenter] (3, 4) la classe [api:Nette\Application\UI\Component] e della creazione della struttura gerarchica (5) le classi [Container e Component |component-model:]. - -``` -Nette\ComponentModel\Component { IComponent } -| -+- Nette\ComponentModel\Container { IContainer } - | - +- Nette\Application\UI\Component { SignalReceiver, StatePersistent } - | - +- Nette\Application\UI\Control { Renderable } - | - +- Nette\Application\UI\Presenter { IPresenter } -``` - - -Ciclo di vita del componente ----------------------------- - -[* lifecycle-component.svg *] *** *Ciclo di vita del componente* .<> - - -Validazione dei parametri persistenti -------------------------------------- - -I valori dei [parametri persistenti |#Parametri persistenti] ricevuti dagli URL vengono scritti nelle proprietà dal metodo `loadState()`. Esso controlla anche che il tipo di dato indicato per la proprietà corrisponda; in caso contrario risponde con un errore 404 e la pagina non viene mostrata. - -Non fidatevi mai ciecamente dei parametri persistenti, perché l'utente può facilmente sovrascriverli nell'URL. Ecco come controlliamo, per esempio, che il numero di pagina `$this->page` sia maggiore di 0. Un modo adatto è sovrascrivere il metodo `loadState()` già menzionato: - -```php -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; - - public function loadState(array $params): void - { - parent::loadState($params); // qui viene impostato $this->page - // segue il controllo personalizzato del valore: - if ($this->page < 1) { - $this->error(); - } - } -} -``` - -Del processo inverso, cioè della raccolta dei valori dalle proprietà persistenti, si occupa il metodo `saveState()`. - - -Collegamento al presenter -------------------------- - -Nel momento in cui un componente entra a far parte della gerarchia del presenter, vengono richiamate le sue callback salvate nell'array `$onAnchor`. Da quel momento in poi il componente ha a disposizione il presenter, può creare link in sicurezza, leggere i parametri persistenti e così via. - -```php -$control->onAnchor[] = function ($control): void { - // il componente ha ora a disposizione il presenter -}; -``` - - -I segnali in profondità ------------------------ - -Un segnale provoca il ricaricamento della pagina esattamente come la richiesta originale (tranne quando viene chiamato via AJAX) e richiama il metodo `signalReceived($signal)`, la cui implementazione predefinita nella classe `Nette\Application\UI\Component` prova a chiamare un metodo composto dalle parole `handle`. L'ulteriore elaborazione spetta all'oggetto in questione. Gli oggetti che ereditano da `Component` (cioè `Control` e `Presenter`) reagiscono provando a chiamare il metodo `handle` con i parametri appropriati. - -In altre parole: si prende la definizione della funzione `handle`, insieme a tutti i parametri arrivati con la richiesta, si assegnano per nome agli argomenti i parametri provenienti dall'URL e si prova a chiamare il metodo. Per esempio il valore del parametro `id` nell'URL viene passato come argomento `$id`, `something` dall'URL viene passato come `$something` e così via. E se il metodo non esiste, il metodo `signalReceived` solleva un'[eccezione |api:Nette\Application\UI\BadSignalException]. - -Oltre ai parametri provenienti dall'URL, un segnale legge anche i parametri inviati nel **corpo POST della richiesta**. Questo torna comodo, perché i segnali vengono spesso richiamati via JavaScript, dove è naturale inviare i dati con il metodo POST. Se però un parametro con lo stesso nome arriva sia dall'URL sia dal corpo POST, ha la precedenza il valore **proveniente dall'URL**. Evitate quindi di dare a un campo POST lo stesso nome di un parametro dell'URL o della route, altrimenti il valore dell'URL lo sovrascriverebbe in silenzio. I parametri dei segnali condividono lo spazio con i parametri delle azioni e con quelli persistenti, vedi [Spazio condiviso dei parametri |presenters#Spazio condiviso dei parametri]. - -Un segnale può essere ricevuto da qualsiasi componente, presenter o oggetto che implementi l'interfaccia `SignalReceiver` e sia collegato all'albero dei componenti. - -I principali destinatari dei segnali saranno i `Presenter` e i componenti visivi che ereditano da `Control`. Un segnale è pensato per fare da segnale a un oggetto affinché faccia qualcosa: un sondaggio deve conteggiare un voto dell'utente, un blocco di notizie deve espandersi e mostrare il doppio delle notizie, un form è stato inviato e deve elaborare i dati e così via. - -L'URL di un segnale si crea con il metodo [Component::link() |api:Nette\Application\UI\Component::link()]. Come parametro `$destination` passiamo la stringa `{segnale}!` e come `$args` un array di argomenti che vogliamo passare al segnale. Il segnale viene sempre chiamato sul presenter e sull'azione correnti, con i parametri correnti; i parametri del segnale si limitano ad aggiungersi. In più viene aggiunto il **parametro `?do`, che indica il segnale**. - -Il suo formato è `{segnale}` oppure `{destinatarioDelSegnale}-{segnale}`. `{destinatarioDelSegnale}` è il nome del componente nel presenter. Nel nome del componente non si può quindi usare il trattino: serve a separare il nome del componente dal segnale, anche se è possibile annidare così più componenti. - -Il metodo [isSignalReceiver()|api:Nette\Application\UI\Presenter::isSignalReceiver()] controlla se il componente (primo argomento) è il destinatario del segnale (secondo argomento). Il secondo argomento si può omettere: in tal caso controlla se il componente è destinatario di un qualsiasi segnale. Se il secondo parametro è impostato a `true`, verifica se il componente indicato o uno qualsiasi dei suoi discendenti è il destinatario. - -In qualsiasi fase precedente a `handle` possiamo eseguire manualmente il segnale chiamando il metodo [processSignal()|api:Nette\Application\UI\Presenter::processSignal()], che si occupa della gestione del segnale: prende il componente individuato come destinatario del segnale (se non è indicato alcun destinatario, è il presenter stesso) e gli invia il segnale. - -Esempio: - -```php -if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) { - $this->processSignal(); -} -``` - -Così il segnale viene eseguito in anticipo e non verrà chiamato di nuovo. diff --git a/application/it/configuration.texy b/application/it/configuration.texy deleted file mode 100644 index 10520edde4..0000000000 --- a/application/it/configuration.texy +++ /dev/null @@ -1,186 +0,0 @@ -Configurazione dell'applicazione -******************************** - -.[perex] -Panoramica delle opzioni di configurazione di Nette Application. - - -Application -=========== - -```neon -application: - # mostrare il pannello "Nette Application" nella BlueScreen di Tracy? - debugger: ... # (bool) attivo se Tracy è disponibile - - # in produzione le eccezioni sono sempre gestite dall'error-presenter; - # questa opzione attiva quel comportamento anche in modalità di sviluppo - catchExceptions: ... # (bool) di norma false, cioè disattivo in dev, sempre attivo in produzione - - # nome dell'error-presenter - errorPresenter: Error # (string|array) di norma 'Nette:Error' - - # definisce gli alias per i presenter e le azioni - aliases: ... - - # definisce le regole per tradurre il nome del presenter in una classe - mapping: ... - - # silenziare gli avvisi sui link non validi? - # ha effetto solo in modalità di sviluppo - silentLinks: ... # (bool) di norma false -``` - -Da `nette/application` versione 3.2 è possibile definire una coppia di error presenter: - -```neon -application: - errorPresenter: - 4xx: Error4xx # per Nette\Application\BadRequestException - 5xx: Error5xx # per le altre eccezioni -``` - -Separarli è utile, perché le due situazioni sono radicalmente diverse. Una `BadRequestException` (codici 4xx) significa che l'applicazione sta bene e che semplicemente il visitatore ha chiesto qualcosa che non esiste. Potete quindi usare un presenter a pieno regime, che mostri un messaggio cordiale nel layout del vostro sito. Un errore 5xx significa invece che qualcosa nell'applicazione si è rotto e non sapete cosa. Tenete il presenter 5xx il più minimale possibile, così che durante il suo rendering non possa rompersi nient'altro: idealmente non dovrebbe toccare il database, il layout o l'utente connesso. - -L'opzione `silentLinks` stabilisce come si comporta Nette in modalità di sviluppo quando la generazione di un link fallisce (per esempio perché il presenter non esiste ecc.). Il valore predefinito `false` significa che Nette emette un errore `E_USER_WARNING`. Impostandola a `true` questo messaggio di errore viene silenziato. In un ambiente di produzione `E_USER_WARNING` viene sempre emesso. Su questo comportamento si può influire anche impostando la variabile del presenter [$invalidLinkMode |creating-links#Link non validi]. - -Gli [alias semplificano il riferimento |creating-links#Alias] ai presenter usati più di frequente. - -Il [mapping definisce le regole |directory-structure#Mapping dei presenter] con cui il nome della classe viene ricavato dal nome del presenter. - - -Registrazione automatica dei presenter --------------------------------------- - -Nette aggiunge automaticamente i presenter come servizi al container DI, il che ne accelera notevolmente la creazione. Il modo in cui Nette individua i presenter si può configurare: - -```neon -application: - # cercare i presenter nella class map di Composer? - scanComposer: ... # (bool) di norma true - - # maschera a cui devono corrispondere il nome della classe e quello del file - scanFilter: ... # (string) di norma '*Presenter' - - # in quali directory cercare i presenter? - scanDirs: # (string[]|false) di norma '%appDir%' - - %vendorDir%/mymodule -``` - -Le directory elencate in `scanDirs` non sostituiscono il valore predefinito `%appDir%`, ma lo integrano, quindi `scanDirs` conterrà entrambi i percorsi, `%appDir%` e `%vendorDir%/mymodule`. Se vogliamo escludere la directory predefinita, usiamo un [punto esclamativo |dependency-injection:configuration#Unione]: - -```neon -application: - scanDirs!: - - %vendorDir%/mymodule -``` - -La scansione delle directory si può disattivare impostando il valore a `false`. I presenter non vengono allora più registrati come servizi, quindi non si possono modificare tramite la sezione [decorator |dependency-injection:configuration#Decorator] e la loro creazione è più lenta. Non consigliamo quindi di sopprimere del tutto la registrazione automatica, perché riduce le prestazioni dell'applicazione. - - -Template Latte -============== - -Questa impostazione influisce globalmente sul comportamento di Latte nei componenti e nei presenter. - -```neon -latte: - # mostrare il pannello di Latte nella Tracy Bar per il template principale (true) o per tutti i componenti (all)? - debugger: ... # (true|false|'all') attivo se Tracy è disponibile (solo in modalità debug) - - # genera i template con l'intestazione declare(strict_types=1) - strictTypes: ... # (bool) di norma false - - # attiva la [modalità di analisi rigorosa |latte:develop#strict mode] - strictParsing: ... # (bool) di norma false - - # limita l'ambito delle variabili al corpo del ciclo - scopedLoopVariables: ... # (bool) di norma false - - # rimuove l'indentazione dovuta all'annidamento nei tag di tipo pari - dedent: ... # (bool) di norma false - - # attiva il [controllo del codice generato |latte:develop#Checking Generated Code] - phpLinter: ... # (string) di norma null - - # imposta il locale - locale: cs_CZ # (string) di norma null - - # classe dell'oggetto $this->template - templateClass: App\MyTemplateClass # di norma Nette\Bridges\ApplicationLatte\DefaultTemplate -``` - -Potete aggiungere nuove [estensioni |latte:extending-latte#Estensione di Latte] così: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - - -Routing -======= - -Impostazioni di base: - -```neon -routing: - # mostrare il pannello del routing nella Tracy Bar? - debugger: ... # (bool) attivo se Tracy è disponibile (solo in modalità debug) - - # serializza il router nel container DI - cache: ... # (bool) di norma false -``` - -Il routing si definisce di solito nella classe [RouterFactory |routing#Collezione di route]. In alternativa le route si possono definire anche nella configurazione, con coppie `maschera: azione`, ma questo metodo non offre molta flessibilità: - -```neon -routing: - routes: - 'detail/': Admin:Home:default - '/': Front:Home:default -``` - - -Costanti -======== - -Creazione di costanti PHP. - -```neon -constants: - Foobar: 'baz' -``` - -La costante `Foobar` verrà creata dopo l'avvio dell'applicazione. - -.[note] -Le costanti non devono servire da variabili globalmente accessibili. Per passare valori agli oggetti usate la [dependency injection |dependency-injection:passing-dependencies]. - - -PHP -=== - -Impostazione delle direttive di PHP. Una panoramica di tutte le direttive si trova su [php.net |https://www.php.net/manual/en/ini.list.php]. - -```neon -php: - date.timezone: Europe/Prague -``` - - -Servizi DI -========== - -Questi servizi vengono aggiunti al container DI: - -| Nome | Tipo | Descrizione -|----------------------------|---------------------------------------------------|----------------------------------------- -| `application.application` | [api:Nette\Application\Application] | l'[esecutore dell'applicazione |how-it-works#Nette Application] -| `application.linkGenerator` | [api:Nette\Application\LinkGenerator] | [LinkGenerator |creating-links#LinkGenerator] -| `application.presenterFactory` | [api:Nette\Application\IPresenterFactory] | factory dei presenter -| `application.###` | [api:Nette\Application\UI\Presenter] | i singoli presenter -| `routing.router` | [api:Nette\Routing\Router] | router -| `latte.latteFactory` | [api:Nette\Bridges\ApplicationLatte\LatteFactory] | factory dell'oggetto `Latte\Engine` -| `latte.templateFactory` | [api:Nette\Application\UI\TemplateFactory] | factory di [`$this->template` |templates] diff --git a/application/it/creating-links.texy b/application/it/creating-links.texy deleted file mode 100644 index b641e316d7..0000000000 --- a/application/it/creating-links.texy +++ /dev/null @@ -1,319 +0,0 @@ -Creazione di link URL -********************* - -
    - -Creare link in Nette è semplice come puntare il dito. Basta mirare e il framework farà tutto il lavoro per voi. Vi mostreremo: - -- come creare link nei template e altrove -- come riconoscere un link alla pagina corrente -- cosa fare con i link non validi - -
    - - -Grazie al [routing bidirezionale |routing] non dovrete mai scrivere a mano nei template o nel codice gli URL della vostra applicazione, che potrebbero cambiare in seguito o essere complicati da comporre. Nel link basta indicare il presenter e l'azione, passare eventuali parametri, e il framework genererà l'URL da sé. In realtà è molto simile a chiamare una funzione. Vi piacerà. - - -Nel template del presenter -========================== - -Il più delle volte creiamo i link nei template, e l'attributo `n:href` è un ottimo aiuto: - -```latte -dettaglio -``` - -Notate che al posto dell'attributo HTML `href` abbiamo usato l'[n:attributo |latte:syntax#n:attributi] `n:href`. Il suo valore non è un URL, come sarebbe per l'attributo `href`, ma il nome del presenter e dell'azione. - -Cliccare su un link è, detta semplicemente, un po' come chiamare il metodo `ProductPresenter::renderShow()`. E se questo ha dei parametri nella propria firma, possiamo chiamarlo con degli argomenti: - -```latte -dettaglio del prodotto -``` - -È possibile passare anche parametri nominali. Il link seguente passa il parametro `lang` con il valore `en`: - -```latte -dettaglio del prodotto -``` - -Se il metodo `ProductPresenter::renderShow()` non ha `$lang` nella propria firma, può ottenere il valore del parametro con `$lang = $this->getParameter('lang')` oppure da una [proprietà |presenters#Parametri della richiesta]. - -Se i parametri sono salvati in un array, si possono espandere con l'operatore `...`: - -```latte -{var $args = [$product->id, lang => en]} -dettaglio del prodotto -``` - -Nei link vengono passati automaticamente anche i cosiddetti [parametri persistenti |presenters#Parametri persistenti]. - -L'attributo `n:href` è molto comodo per i tag HTML ``. Se vogliamo stampare il link altrove, per esempio nel testo, usiamo `{link}`: - -```latte -L'URL è: {link Home:default} -``` - - -Nel codice -========== - -Per creare un link nel presenter si usa il metodo `link()`: - -```php -$url = $this->link('Product:show', $product->id); -``` - -I parametri si possono passare anche come array, in cui si possono indicare anche parametri nominali: - -```php -$url = $this->link('Product:show', [$product->id, 'lang' => 'en']); -``` - -I link si possono creare anche senza un presenter, con il [#LinkGenerator] e il suo metodo `link()`. - -A volte vi serve creare un link subito, ma generare l'URL vero e proprio solo più tardi. A questo serve il metodo `lazyLink()`, che restituisce un oggetto `Nette\Application\UI\Link`. Il vantaggio è che potete passare questo oggetto, per esempio a un template, e prima che venga disegnato potete ancora modificarne i parametri con il metodo `setParameter()`. L'URL viene composto solo quando l'oggetto viene convertito in stringa: - -```php -$link = $this->lazyLink('Product:show', $id); -// ... -echo $link; // l'URL viene generato solo qui -``` - - -Link a un presenter -=================== - -Se la destinazione del link è un presenter con un'azione, la sintassi è questa: - -``` -[//] [[[[:]module:]presenter:]action | this] [#fragment] -``` - -Questo formato è supportato da tutti i tag di Latte e da tutti i metodi del presenter che lavorano con i link, cioè `n:href`, `{link}`, `{plink}`, `link()`, `lazyLink()`, `isLinkCurrent()`, `redirect()`, `redirectPermanent()`, `forward()`, `canonicalize()` e anche dal [#LinkGenerator]. Quindi, anche se negli esempi si usa `n:href`, al suo posto potrebbe esserci una qualsiasi di queste funzioni. - -La forma di base è dunque `Presenter:azione`: - -```latte -home page -``` - -Se colleghiamo a un'azione del presenter corrente, possiamo ometterne il nome: - -```latte -home page -``` - -Se l'azione di destinazione è `default`, possiamo ometterla, ma i due punti devono restare: - -```latte -home page -``` - -I link possono puntare anche ad altri [moduli |directory-structure#Presenter e template]. Qui si distingue tra link relativi a un sottomodulo annidato e link assoluti. Il principio è analogo a quello dei percorsi su disco, solo che al posto delle barre si usano i due punti. Supponendo che il presenter corrente faccia parte del modulo `Front`, scriveremmo: - -```latte -link a Front:Shop:Product:show -link a Admin:Product:show -``` - -Un caso particolare è il link [a sé stessi |#Link alla pagina corrente], dove indichiamo come destinazione `this`. - -```latte -aggiorna -``` - -Possiamo collegare a una parte precisa della pagina tramite il cosiddetto frammento dopo il cancelletto `#`: - -```latte -link a Home:default e al frammento #main -``` - -.{data-version:3.3.0} -Il frammento si può impostare anche dinamicamente, come argomento con la chiave `#`. Il suo valore viene codificato automaticamente e ha la precedenza sul frammento indicato nella destinazione: - -```php -$this->link('Home:default', ['#' => $fragment]); -``` - - -Percorsi assoluti -================= - -I link generati con `link()` o `n:href` sono sempre percorsi assoluti (cioè iniziano con `/`), ma non URL assoluti con protocollo e dominio, come `https://domain`. - -Per generare un URL assoluto aggiungete due barre all'inizio (per esempio `n:href="//Home:"`). In alternativa potete far generare al presenter solo link assoluti impostando `$this->absoluteUrls = true`. - -Nel template si può usare anche il filtro `|absoluteUrl` per convertire un percorso relativo in uno assoluto. - - -Link alla pagina corrente -========================= - -La destinazione `this` crea un link alla pagina corrente: - -```latte -aggiorna -``` - -Allo stesso tempo vengono trasferiti tutti i parametri indicati nella firma del metodo `action()` o `render()` (se `action()` non è definito). Se quindi ci troviamo sulla pagina `Product:show` con `id: 123`, anche il link a `this` passerà questo parametro. - -Naturalmente è possibile indicare i parametri direttamente: - -```latte -aggiorna -``` - -La funzione `isLinkCurrent()` controlla se la destinazione del link coincide con la pagina corrente. Si può usare per esempio in un template per distinguere i link e simili. - -I parametri sono gli stessi del metodo `link()`, ma al posto di un'azione specifica si può usare anche il carattere jolly `*`, che indica una qualsiasi azione del presenter indicato. - -```latte -{if !isLinkCurrent('Admin:login')} - Accedi -{/if} - -
  • - ... -
  • -``` - -In combinazione con `n:href` su un unico elemento si può usare una forma abbreviata: - -```latte -... -``` - -Il carattere jolly `*` si può usare solo al posto dell'azione, non del presenter. - -Per stabilire se ci troviamo in un determinato modulo o in un suo sottomodulo, usate il metodo `isModuleCurrent(moduleName)`. - -```latte -
  • - ... -
  • -``` - - -Cambiare la base dei link .{data-version:3.2.7} -=============================================== - -Per impostazione predefinita i link relativi sono derivati dal presenter corrente. Lo si può cambiare con `{linkBase}`: - -```latte -{linkBase Admin:Dashboard} -dettaglio del prodotto -``` - -Il link porterà a `Admin:Dashboard:Product:show`. Ne sono interessati solo i link relativi: i link assoluti che iniziano con i due punti e i link al presenter corrente (`this`, `show`) restano invariati. - -`{linkBase}` vale per l'intero template ed è particolarmente utile nei template di layout, dove garantisce link coerenti indipendentemente dal presenter chiamante. -Il tag va collocato all'inizio del template, altrimenti solleva una `CompileException`. - - -Link a un segnale -================= - -La destinazione di un link non deve essere per forza un presenter con un'azione: può essere anche un [segnale |components#Segnale] (che chiama il metodo `handle()`). La sintassi è allora questa: - -``` -[//] [sotto-componente:]segnale! [#fragment] -``` - -Il segnale si distingue quindi per il punto esclamativo: - -```latte -segnale -``` - -Potete creare anche un link a un segnale di un sottocomponente (o di un sotto-sottocomponente): - -```latte -segnale -``` - - -Link in un componente -===================== - -Poiché i [componenti|components] sono unità riutilizzabili autonome, che non dovrebbero avere alcun legame con i presenter circostanti, qui i link funzionano in modo un po' diverso. L'attributo Latte `n:href` e il tag `{link}`, così come i metodi del componente come `link()` e altri, **considerano sempre la destinazione del link come il nome di un segnale**. Non è quindi nemmeno necessario indicare il punto esclamativo: - -```latte -segnale, non un'azione -``` - -Se volessimo collegare ai presenter nel template di un componente, useremmo il tag `{plink}`: - -```latte -home -``` - -oppure nel codice - -```php -$this->getPresenter()->link('Home:default') -``` - - -Alias .{data-version:3.2.3} -=========================== - -A volte può essere utile assegnare a una coppia Presenter:azione un alias facile da ricordare. Per esempio chiamare la home page `Front:Home:default` semplicemente `home`, oppure `Admin:Dashboard:default` come `admin`. - -Gli alias si definiscono nella [configurazione|configuration], sotto la chiave `application › aliases`: - -```neon -application: - aliases: - home: Front:Home:default - admin: Admin:Dashboard:default - sign: Front:Sign:in -``` - -Nei link si scrivono poi con la chiocciola, per esempio: - -```latte -amministrazione -``` - -Sono supportati anche in tutti i metodi che lavorano con i link, come `redirect()` e simili. - - -Link non validi -=============== - -Può capitare di creare un link non valido: perché porta a un presenter inesistente, perché passa più parametri di quanti ne accetti il metodo di destinazione nella propria firma, oppure perché per l'azione di destinazione non è possibile generare un URL. Come gestire i link non validi si imposta nel presenter con `$this->invalidLinkMode`. Può assumere una combinazione di questi valori (costanti): - -- `Presenter::InvalidLinkSilent` - modalità silenziosa, restituisce come URL il carattere # -- `Presenter::InvalidLinkWarning` - viene emesso un avviso E_USER_WARNING, che in modalità di produzione verrà registrato nel log ma non interromperà l'esecuzione dello script -- `Presenter::InvalidLinkTextual` - avviso visivo, stampa l'errore direttamente nel link -- `Presenter::InvalidLinkException` - solleva InvalidLinkException - -L'impostazione predefinita è `InvalidLinkWarning` in modalità di produzione e `InvalidLinkWarning | InvalidLinkTextual` in modalità di sviluppo. In ambiente di produzione `InvalidLinkWarning` non provoca l'interruzione dello script, ma l'avviso verrà registrato nel log. In ambiente di sviluppo lo intercetta [Tracy |tracy:] e mostra una schermata blu. `InvalidLinkTextual` funziona restituendo come URL un messaggio di errore che inizia con i caratteri `#error:`. Perché link del genere si notino a colpo d'occhio, aggiungete al vostro CSS: - -```css -a[href^="#error:"] { - background: red; - color: white; -} -``` - -Se non vogliamo che in ambiente di sviluppo vengano emessi avvisi, possiamo silenziarli direttamente nella [configurazione|configuration]. - -```neon -application: - silentLinks: true -``` - - -LinkGenerator -============= - -Come creare link con la stessa comodità del metodo `link()`, ma senza la presenza di un presenter? A questo serve [api:Nette\Application\LinkGenerator]. - -LinkGenerator è un servizio che potete farvi passare tramite il costruttore e con cui potete poi creare link usandone il metodo `link()`. - -C'è una differenza rispetto ai presenter. LinkGenerator crea tutti i link direttamente come URL assoluti. Inoltre non esiste un "presenter corrente", quindi non è possibile indicare come destinazione solo il nome dell'azione, `link('default')`, né usare percorsi relativi ai moduli. - -I link non validi sollevano sempre `Nette\Application\UI\InvalidLinkException`. diff --git a/application/it/directory-structure.texy b/application/it/directory-structure.texy deleted file mode 100644 index f4a1ae9c48..0000000000 --- a/application/it/directory-structure.texy +++ /dev/null @@ -1,526 +0,0 @@ -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 codice. Imparerete: - -- come strutturare **logicamente** l'applicazione in directory -- come progettare la struttura perché **scali bene** con la crescita del progetto -- quali sono le **alternative possibili** e i loro pregi o difetti - -
    - - -È importante dire che Nette Framework stesso non impone alcuna struttura specifica. È progettato per adattarsi facilmente a qualsiasi esigenza e preferenza. - - -Struttura di base del progetto -============================== - -Benché Nette Framework non imponga alcuna struttura fissa di directory, esiste una disposizione predefinita 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 per la riga di comando -├── config/ ← configurazione -├── log/ ← errori registrati -├── temp/ ← file temporanei, cache -├── tests/ ← test -├── vendor/ ← librerie installate da Composer -└── www/ ← directory pubblica (document-root) -\-- - -Potete modificare liberamente questa struttura secondo le vostre esigenze, rinominando o spostando le cartelle. Dovete poi solo sistemare i percorsi relativi delle directory in `Bootstrap.php` ed eventualmente in `composer.json`. Non serve nient'altro: nessuna riconfigurazione complicata, nessuna modifica alle costanti. Nette dispone di un rilevamento automatico intelligente e riconosce da sé la posizione dell'applicazione, compresa la base del suo URL. - - -Principi di organizzazione del codice -===================================== - -Quando esplorate per la prima volta un nuovo progetto, dovreste riuscire a orientarvi rapidamente. Immaginate di cliccare sulla directory `app/Model/` e di vedere questa struttura: - -/--pre -app/Model/ -├── Services/ -├── Repositories/ -└── Entities/ -\-- - -Da qui imparate solo che il progetto usa dei servizi, dei repository e delle entità. Non imparate nulla sullo scopo reale dell'applicazione. - -Guardiamo un approccio diverso, l'**organizzazione per domini**: - -/--pre -app/Model/ -├── Cart/ -├── Payment/ -├── Order/ -└── Product/ -\-- - -Qui è diverso: a colpo d'occhio è chiaro che si tratta di un e-shop. I nomi stessi delle directory rivelano cosa sa fare l'applicazione: lavora con pagamenti, ordini e prodotti. - -Il primo approccio (l'organizzazione per tipo di classe) porta nella pratica diversi problemi: il codice logicamente collegato è frammentato in cartelle diverse e dovete saltare tra di esse. Organizzeremo quindi per domini. - - -Namespace ---------- - -È consuetudine che la struttura delle directory corrisponda ai namespace dell'applicazione. Questo significa che la posizione fisica dei file coincide con il loro namespace. Per esempio, una classe che si trova in `app/Model/Product/ProductRepository.php` dovrebbe avere il namespace `App\Model\Product`. Questo principio aiuta a orientarsi nel codice e semplifica l'autoloading. - - -Singolare o plurale nei nomi ----------------------------- - -Notate che per le directory principali dell'applicazione usiamo il singolare: `app`, `config`, `log`, `temp`, `www`. Lo stesso vale dentro l'applicazione: `Model`, `Core`, `Presentation`. Questo perché ciascuna rappresenta un unico concetto coerente. - -Allo stesso modo `app/Model/Product` rappresenta tutto ciò che riguarda i prodotti. Non la chiamiamo `Products` perché non è una cartella piena di prodotti (conterrebbe file come `nokia.php`, `samsung.php`). È un namespace che contiene le classi per lavorare con i prodotti: `ProductRepository.php`, `ProductService.php`. - -La cartella `app/Tasks` è al plurale perché contiene un insieme di script eseguibili separati: `CleanupTask.php`, `ImportTask.php`. Ognuno di essi è un'unità indipendente. - -Per coerenza consigliamo di usare: -- il singolare per i namespace che rappresentano un'unità funzionale (anche se lavorano con più entità) -- il plurale per le raccolte di unità indipendenti -- in caso di incertezza, o se non volete pensarci, scegliete il singolare - - -Directory pubblica `www/` -========================= - -Questa directory è l'unica accessibile dal web (il document-root). Spesso potreste incontrare il nome `public/` al posto di `www/`: è solo una questione di convenzione e non influisce sul funzionamento dell'applicazione. La directory contiene: -- il [punto d'ingresso |bootstrapping#index.php] dell'applicazione `index.php` -- il file `.htaccess` con le regole di mod_rewrite (per Apache) -- i file statici (CSS, JavaScript, immagini) -- i file caricati - -Per una corretta sicurezza dell'applicazione è essenziale avere il [document-root configurato correttamente |nette:troubleshooting#Come cambiare o rimuovere la directory www dall'URL?]. - -.[note] -Non collocate mai la cartella `node_modules/` in questa directory: contiene migliaia di file che potrebbero essere eseguibili e non dovrebbero essere accessibili pubblicamente. - - -Directory dell'applicazione `app/` -================================== - -È la directory principale, che contiene il codice dell'applicazione. Struttura di base: - -/--pre -app/ -├── Core/ ← questioni infrastrutturali -├── Model/ ← logica di business -├── Presentation/ ← presenter e template -├── Tasks/ ← script da riga di comando -└── Bootstrap.php ← classe di avvio dell'applicazione -\-- - -`Bootstrap.php` è la [classe di avvio dell'applicazione|bootstrapping], che inizializza l'ambiente, carica la configurazione e crea il container DI. - -Vediamo ora più in dettaglio le singole sottodirectory. - - -Presenter e template -==================== - -La parte di presentazione dell'applicazione si trova nella directory `app/Presentation`. Un'alternativa è la più breve `app/UI`. È il posto di tutti i presenter, dei loro template e delle eventuali classi di supporto collegate. - -Organizziamo questo strato per domini. In un progetto complesso che unisce un e-shop, un blog e un'API, la struttura avrebbe questo aspetto: - -/--pre -app/Presentation/ -├── Shop/ ← frontend dell'e-shop -│ ├── Product/ -│ ├── Cart/ -│ └── Order/ -├── Blog/ ← blog -│ ├── Home/ -│ └── Post/ -├── Admin/ ← amministrazione -│ ├── Dashboard/ -│ └── Products/ -└── Api/ ← endpoint dell'API - └── V1/ -\-- - -Al contrario, per un semplice blog useremmo questa struttura: - -/--pre -app/Presentation/ -├── Front/ ← frontend del sito -│ ├── Home/ -│ └── Post/ -├── Admin/ ← amministrazione -│ ├── Dashboard/ -│ └── Posts/ -├── Error/ -└── Export/ ← RSS, sitemap ecc. -\-- - -Cartelle come `Home/` o `Dashboard/` contengono presenter e template. Cartelle come `Front/`, `Admin/` o `Api/` si chiamano **moduli**. Tecnicamente sono normali directory usate per la suddivisione logica dell'applicazione. - -Ogni cartella che contiene un presenter comprende il file del presenter stesso e i suoi template. Per esempio la cartella `Dashboard/` contiene: - -/--pre -Dashboard/ -├── DashboardPresenter.php ← presenter -└── default.latte ← template -\-- - -Questa struttura di directory si riflette nei namespace delle classi. Per esempio `DashboardPresenter` si trova nel namespace `App\Presentation\Admin\Dashboard` (vedi [#Mapping dei presenter]): - -```php -namespace App\Presentation\Admin\Dashboard; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -Nell'applicazione ci riferiamo al presenter `Dashboard` del modulo `Admin` con la notazione a due punti, come `Admin:Dashboard`. La sua azione `default` si indica poi come `Admin:Dashboard:default`. Per i moduli annidati usiamo più volte i due punti, per esempio `Shop:Order:Detail:default`. - - -Sviluppo flessibile della struttura ------------------------------------ - -Uno dei grandi vantaggi di questa struttura è la sua eleganza nell'adattarsi alle esigenze crescenti del progetto. Prendiamo come esempio la parte che genera i feed XML. All'inizio abbiamo una forma semplice: - -/--pre -Export/ -├── ExportPresenter.php ← un presenter per tutte le esportazioni -├── sitemap.latte ← template della sitemap -└── feed.latte ← template del feed RSS -\-- - -Col tempo si aggiungono altri tipi di feed e ci serve più logica per gestirli... 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 indolore: basta creare le nuove sottocartelle, dividervi il codice e aggiornare i link (per esempio da `Export:feed` a `Export:Feed:amazon`). Grazie a questo possiamo ampliare gradualmente la struttura secondo necessità, e il livello di annidamento non è limitato in alcun modo. - -Se per esempio nell'amministrazione avete molti presenter legati alla gestione degli ordini, come `OrderDetail`, `OrderEdit`, `OrderDispatch` ecc., per una migliore organizzazione potete creare un modulo (una cartella) chiamato `Order`, che conterrà (le cartelle dei) presenter `Detail`, `Edit`, `Dispatch` e altri. - - -Posizione dei template ----------------------- - -Negli esempi precedenti abbiamo visto che i template si trovano direttamente nella cartella del presenter: - -/--pre -Dashboard/ -├── DashboardPresenter.php ← presenter -├── DashboardTemplate.php ← classe del template, facoltativa -└── default.latte ← template -\-- - -Nella pratica questa collocazione si rivela la più comoda: avete tutti i file collegati subito a portata di mano. - -In alternativa potete collocare i template in una sottocartella `templates/`. Nette supporta entrambe le varianti. Potete perfino collocare i template completamente fuori dalla cartella `Presentation/`. Tutto ciò che riguarda le possibilità di collocazione dei template si trova nel capitolo [Ricerca dei template |templates#Ricerca dei template]. - - -Classi di supporto e componenti -------------------------------- - -Ai presenter e ai template si accompagnano spesso altri file di supporto. Li collochiamo logicamente in base al loro ambito: - -1. **Direttamente con il presenter**, nel caso di componenti specifici di quel presenter: - -/--pre -Product/ -├── ProductPresenter.php -├── ProductGrid.php ← componente per l'elenco dei prodotti -└── FilterForm.php ← form per il filtraggio -\-- - -2. **Per il modulo**: consigliamo di usare la cartella `Accessory`, che in ordine alfabetico si colloca comodamente all'inizio: - -/--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/ -\-- - -In alternativa potete collocare le classi di supporto come `LatteExtension.php` o `TemplateFilters.php` nella cartella infrastrutturale `app/Core/Latte/`. E i componenti in `app/Components`. La scelta dipende dalle convenzioni del team. - - -Model, il cuore dell'applicazione -================================= - -Il model contiene tutta la logica di business dell'applicazione. La regola per organizzarlo è di nuovo: struttura per domini. - -/--pre -app/Model/ -├── Payment/ ← tutto ciò che riguarda i pagamenti -│ ├── PaymentFacade.php ← punto d'ingresso principale -│ ├── PaymentRepository.php -│ ├── Payment.php ← entità -├── Order/ ← tutto ciò che riguarda gli ordini -│ ├── OrderFacade.php -│ ├── OrderRepository.php -│ ├── Order.php -└── Shipping/ ← tutto ciò che riguarda le spedizioni -\-- - -Nel model incontrate di norma questi tipi di classi: - -**Facade**: rappresentano il punto d'ingresso principale in un determinato dominio dell'applicazione. Fanno da orchestratore e coordinano la collaborazione tra i vari servizi per realizzare interi casi d'uso (come "crea ordine" o "elabora pagamento"). Sotto il proprio strato di orchestrazione la facade nasconde i dettagli implementativi al resto dell'applicazione, offrendo così un'interfaccia pulita per lavorare con quel dominio. - -```php -class OrderFacade -{ - public function createOrder(Cart $cart): Order - { - // validazione - // creazione dell'ordine - // invio dell'e-mail - // scrittura nelle statistiche - } -} -``` - -**Servizi**: si concentrano su operazioni di business specifiche all'interno di un dominio. A differenza delle facade, che orchestrano interi casi d'uso, un servizio implementa una logica di business precisa (come il calcolo dei prezzi o l'elaborazione dei pagamenti). I servizi sono di norma privi di stato e possono essere usati sia dalle facade come mattoni di 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 tutta la comunicazione con l'archivio dei dati, di norma un database. Il loro compito è caricare e salvare le entità e implementare i metodi per cercarle. Un repository protegge il resto dell'applicazione dai dettagli implementativi del database e offre 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 propria identità e cambiano nel tempo. Di norma sono classi mappate sulle tabelle del database tramite un ORM (come Nette Database Explorer o Doctrine). Le entità possono contenere regole di business relative ai propri dati e logica di validazione. - -```php -// entità mappata sulla tabella di database '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 object**: oggetti immutabili che rappresentano valori privi di identità propria, per esempio un importo monetario o un indirizzo e-mail. Due istanze di un value object con gli stessi valori sono considerate identiche. - - -Codice infrastrutturale -======================= - -La cartella `Core/` (o, in alternativa, `Infrastructure/`) ospita le fondamenta tecniche dell'applicazione. Il codice infrastrutturale comprende di norma: - -/--pre -app/Core/ -├── Router/ ← routing e gestione degli URL -│ └── RouterFactory.php -├── Security/ ← autenticazione e autorizzazione -│ ├── Authenticator.php -│ └── Authorizator.php -├── Logging/ ← logging e monitoraggio -│ ├── SentryLogger.php -│ └── FileLogger.php -├── Cache/ ← strato di caching -│ └── FullPageCache.php -└── Integration/ ← integrazione con servizi esterni - ├── Slack/ - └── Stripe/ -\-- - -Per i progetti più piccoli basta naturalmente una struttura piatta: - -/--pre -Core/ -├── RouterFactory.php -├── Authenticator.php -└── QueueMailer.php -\-- - -È il codice che: - -- si occupa dell'infrastruttura tecnica (routing, logging, caching) -- integra servizi esterni (Sentry, Elasticsearch, Redis) -- offre servizi di base all'intera applicazione (posta, database) -- è per lo più indipendente da un dominio specifico: la cache o il logger funzionano allo stesso modo per un e-shop o per un blog. - -Vi state chiedendo se una certa classe appartenga a questa cartella o al model? La differenza fondamentale è che il codice in `Core/`: - -- non sa nulla del dominio (prodotti, ordini, articoli) -- di norma si può trasferire in un altro progetto -- risolve il "come funziona" (come inviare un'e-mail), non il "cosa fa" (quale e-mail inviare) - -Un esempio per capire meglio: - -- `App\Core\MailerFactory` - crea istanze della classe per inviare e-mail, gestisce le impostazioni SMTP -- `App\Model\OrderMailer` - usa `MailerFactory` per inviare le e-mail relative agli ordini, ne conosce i template e sa quando vanno inviate - - -Script da riga di comando -========================= - -Le applicazioni hanno spesso bisogno di svolgere attività fuori dalle normali richieste HTTP: che si tratti di elaborazione dati in background, di manutenzione o di attività periodiche. Per l'esecuzione si usano semplici script nella directory `bin/`, mentre la logica implementativa vera e propria si colloca in `app/Tasks/` (o in `app/Commands/`). - -Esempio: - -/--pre -app/Tasks/ -├── Maintenance/ ← script di manutenzione -│ ├── CleanupCommand.php ← eliminazione dei dati vecchi -│ └── DbOptimizeCommand.php ← ottimizzazione del database -├── Integration/ ← integrazione con sistemi esterni -│ ├── ImportProducts.php ← importazione dal sistema del fornitore -│ └── SyncOrders.php ← sincronizzazione degli ordini -└── Scheduled/ ← attività periodiche - ├── NewsletterCommand.php ← invio delle newsletter - └── ReminderCommand.php ← notifiche ai clienti -\-- - -Cosa appartiene al model e cosa agli script da riga di comando? Per esempio la logica per inviare una singola e-mail fa parte del model, mentre l'invio massivo di migliaia di e-mail appartiene a `Tasks/`. - -I task si eseguono di solito dalla riga di comando o via cron: lo script in `bin/` crea il container DI con il metodo [bootConsoleApplication() |bootstrapping#Ambienti diversi] e ne estrae il servizio necessario. Si possono eseguire anche tramite una richiesta HTTP, ma bisogna pensare alla sicurezza. Il presenter che esegue il task va protetto, per esempio consentendolo solo agli utenti connessi oppure con un token robusto e l'accesso da indirizzi IP autorizzati. Per i task di lunga durata è necessario aumentare il limite di tempo dello script e usare `session_write_close()` per non bloccare la sessione. - - -Altre directory possibili -========================= - -Oltre alle directory di base già menzionate, potete aggiungere altre cartelle specializzate secondo le esigenze del progetto. Vediamo le più comuni e il loro uso: - -/--pre -app/ -├── Api/ ← logica dell'API, indipendente dallo strato di presentazione -├── Database/ ← script di migrazione e seeder per i dati di test -├── Components/ ← componenti visivi condivisi in tutta l'applicazione -├── Event/ ← utile se usate un'architettura a eventi -├── Mail/ ← template delle e-mail e logica collegata -└── Utils/ ← classi di supporto -\-- - -Per i componenti visivi condivisi, usati nei presenter di tutta l'applicazione, potete usare la cartella `app/Components` o `app/Controls`: - -/--pre -app/Components/ -├── Form/ ← componenti di form condivisi -│ ├── SignInForm.php -│ └── UserForm.php -├── Grid/ ← componenti per gli elenchi di dati -│ └── DataGrid.php -└── Navigation/ ← elementi di navigazione - ├── Breadcrumbs.php - └── Menu.php -\-- - -È qui che appartengono i componenti con logica più complessa. Se volete condividere i componenti tra più progetti, conviene estrarli in un pacchetto Composer separato. - -Nella directory `app/Mail` potete collocare la gestione della comunicazione via e-mail: - -/--pre -app/Mail/ -├── templates/ ← template delle e-mail -│ ├── order-confirmation.latte -│ └── welcome.latte -└── OrderMailer.php -\-- - - -Mapping dei presenter -===================== - -Il mapping definisce le regole per ricavare il nome della classe dal nome del presenter. Le indichiamo nella [configurazione|configuration], sotto la chiave `application › mapping`. - -In questa pagina abbiamo mostrato che collochiamo i presenter nella cartella `app/Presentation` (o `app/UI`). Da Nette Application 3.3 questa è la convenzione predefinita, che non serve configurare. Se usate una struttura diversa o volete indicare il mapping esplicitamente, l'impostazione predefinita corrisponde a questa riga: - -```neon -application: - mapping: App\Presentation\*\**Presenter -``` - -Come funziona il mapping? Per capire meglio, immaginiamo prima un'applicazione senza moduli. Vogliamo che le classi dei presenter ricadano nel namespace `App\Presentation`, così che il presenter `Home` sia mappato sulla classe `App\Presentation\HomePresenter`. Lo si ottiene con questa configurazione: - -```neon -application: - mapping: App\Presentation\*Presenter -``` - -Il mapping funziona sostituendo l'asterisco nella maschera `App\Presentation\*Presenter` con il nome del presenter `Home`, ottenendo il nome finale della classe `App\Presentation\HomePresenter`. Semplice! - -Come vedete negli esempi di questo e di altri capitoli, però, collochiamo le classi dei presenter in sottodirectory omonime: per esempio il presenter `Home` è mappato sulla classe `App\Presentation\Home\HomePresenter`. Lo otteniamo usando il doppio asterisco `**` (richiede Nette Application 3.2.3): - -```neon -application: - mapping: App\Presentation\**Presenter -``` - -Passiamo ora al mapping dei presenter nei moduli. Possiamo definire un mapping specifico per ogni modulo: - -```neon -application: - mapping: - Front: App\Presentation\Front\**Presenter - Admin: App\Presentation\Admin\**Presenter - Api: App\Api\*Presenter -``` - -Secondo questa configurazione il presenter `Front:Home` è mappato sulla classe `App\Presentation\Front\Home\HomePresenter`, mentre il presenter `Api:OAuth` è mappato sulla classe `App\Api\OAuthPresenter`. - -Poiché i moduli `Front` e `Admin` hanno uno schema di mapping simile, e di moduli così ce ne saranno probabilmente altri, è possibile creare una regola generale che li sostituisca. Nella maschera della classe si aggiunge un nuovo asterisco per il modulo: - -```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 di modulo, dando come risultato la classe `App\Presentation\Admin\User\Edit\EditPresenter`. - -Una notazione alternativa è usare, al posto di una stringa, un array composto da tre segmenti. Per gli esempi mostrati sopra questa notazione equivale 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 deleted file mode 100644 index 5539de6df3..0000000000 --- a/application/it/how-it-works.texy +++ /dev/null @@ -1,200 +0,0 @@ -Come funzionano le applicazioni? -******************************** - -
    - -State leggendo il capitolo fondamentale della documentazione di Nette. Imparerete tutti i principi del funzionamento delle applicazioni web, dalla A alla Z, dal momento in cui nasce una richiesta fino a quando lo script PHP termina l'esecuzione. Dopo la lettura capirete: - -- come funziona il tutto -- cosa sono Bootstrap, il presenter e il container DI -- che aspetto ha la struttura delle directory - -
    - - -Struttura delle directory -========================= - -Aprite lo scheletro di esempio di un'applicazione web, chiamato [WebProject|https://github.com/nette/web-project]. Mentre leggete potete consultare i file di cui si parla. - -La struttura delle directory ha più o meno questo aspetto: - -/--pre -web-project/ -├── app/ ← directory dell'applicazione -│ ├── Core/ ← classi fondamentali necessarie al funzionamento -│ │ └── RouterFactory.php ← configurazione degli indirizzi URL -│ ├── Presentation/ ← presenter, template & co. -│ │ ├── @layout.latte ← template del layout -│ │ └── Home/ ← directory del presenter Home -│ │ ├── HomePresenter.php ← classe del presenter Home -│ │ └── default.latte ← template dell'azione default -│ └── Bootstrap.php ← classe di avvio Bootstrap -├── assets/ ← risorse (SCSS, TypeScript, immagini sorgente) -├── bin/ ← script eseguiti dalla riga di comando -├── config/ ← file di configurazione -│ ├── common.neon -│ └── services.neon -├── log/ ← errori registrati -├── temp/ ← file temporanei, cache, … -├── vendor/ ← librerie installate da Composer -│ ├── ... -│ └── autoload.php ← autoloading di tutti i pacchetti installati -├── www/ ← directory pubblica, document root del progetto -│ ├── assets/ ← file statici compilati (CSS, JS, immagini, ...) -│ ├── .htaccess ← regole di mod_rewrite -│ └── index.php ← file iniziale che avvia l'applicazione -└── .htaccess ← vieta l'accesso a tutte le directory tranne www -\-- - -Potete cambiare la struttura delle directory in qualsiasi modo, rinominare o spostare le cartelle: è del tutto flessibile. Nette dispone inoltre di un rilevamento automatico intelligente e riconosce da sé la posizione dell'applicazione, compresa la base del suo URL. - -Nelle applicazioni un po' più grandi possiamo organizzare le cartelle dei presenter e dei template in [sottodirectory |directory-structure#Presenter e template] e raggruppare le classi in namespace, che chiamiamo moduli. - -La directory `www/` rappresenta la directory pubblica, il document-root del progetto. Potete rinominarla senza dover configurare nient'altro sul lato applicativo. Basta [configurare l'hosting |nette:troubleshooting#Come cambiare o rimuovere la directory www dall'URL?] in modo che il document-root punti a questa directory. - -Potete anche scaricare WebProject direttamente, Nette compreso, con [Composer |best-practices:composer]: - -```shell -composer create-project nette/web-project -``` - -Su Linux o macOS impostate i [permessi di scrittura |nette:troubleshooting#Impostazione dei permessi delle directory] per le directory `log/` e `temp/`. - -L'applicazione WebProject è pronta all'uso: non c'è proprio nulla da configurare e potete vederla subito nel browser accedendo alla cartella `www/`. - - -Richiesta HTTP -============== - -Tutto inizia quando un utente apre una pagina nel proprio browser. Il browser invia una richiesta HTTP al server. Questa richiesta punta a un unico file PHP che si trova nella directory pubblica `www/`, cioè `index.php`. Supponiamo che la richiesta sia per l'indirizzo `https://example.com/product/123`. Grazie a un'opportuna [configurazione del server |nette:troubleshooting#Come configurare il server per gli URL leggibili?], anche questo URL viene mappato sul file `index.php`, che viene poi eseguito. - -Il suo compito è: - -1) inizializzare l'ambiente -2) ottenere la factory -3) avviare l'applicazione Nette, che gestisce la richiesta - -Quale factory? Non stiamo mica producendo trattori, stiamo costruendo siti web! Un attimo di pazienza, verrà spiegato tra poco. - -Per "inizializzazione dell'ambiente" intendiamo, per esempio, l'attivazione di [Tracy|tracy:], un fantastico strumento per registrare o visualizzare gli errori. Su un server di produzione registra gli errori, in ambiente di sviluppo li mostra direttamente. L'inizializzazione comprende quindi anche stabilire se il sito gira in modalità produzione o sviluppo. Nette usa a questo scopo un [rilevamento automatico intelligente |bootstrapping#Modalità di sviluppo e di produzione]: se eseguite il sito su localhost, lavora in modalità di sviluppo. Non dovete configurare nulla e l'applicazione è subito pronta sia per lo sviluppo sia per la messa online. Questi passaggi vengono eseguiti e descritti in dettaglio nel capitolo sulla [classe Bootstrap|bootstrapping]. - -Il terzo punto (sì, abbiamo saltato il secondo, ma ci torneremo) è l'avvio dell'applicazione. In Nette la gestione delle richieste HTTP è compito della classe `Nette\Application\Application` (d'ora in poi `Application`). Quando quindi diciamo "avviare l'applicazione", intendiamo concretamente chiamare il metodo, dal nome azzeccato, `run()` su un oggetto di questa classe. - -Nette fa da mentore e vi guida a scrivere applicazioni pulite secondo metodologie collaudate. Una delle più consolidate è la **dependency injection**, in breve DI. Non vogliamo appesantirvi ora con la spiegazione della DI: c'è un [capitolo dedicato|dependency-injection:introduction]. La conseguenza essenziale è che gli oggetti chiave vengono di norma creati da una factory di oggetti nota come **container DI** (o DIC). Sì, è la factory di cui si parlava prima. È lei a produrre per noi anche l'oggetto `Application`, ed è per questo che ci serve prima il container. Lo otteniamo con la classe `Configurator`, gli facciamo creare l'oggetto `Application`, ne chiamiamo il metodo `run()` e così l'applicazione Nette parte. È esattamente quello che avviene nel file [index.php |bootstrapping#index.php]. - - -Nette Application -================= - -La classe `Application` ha un solo compito: rispondere alla richiesta HTTP. - -Le applicazioni scritte in Nette sono divise in tanti cosiddetti presenter (in altri framework potreste incontrare il termine "controller", che è in sostanza la stessa cosa). Sono classi che rappresentano ciascuna una determinata pagina del sito: per esempio la home page, un prodotto di un e-shop, un form di login, un feed sitemap e così via. Un'applicazione può avere da uno a migliaia di presenter. - -`Application` comincia chiedendo al cosiddetto router di decidere quale presenter deve gestire la richiesta corrente. È il router a stabilire la competenza. Esamina l'URL in ingresso `https://example.com/product/123` e, in base alla propria configurazione, decide che questo compito spetta per esempio al **presenter** `Product`, che deve eseguire l'**azione** `show` per il prodotto con `id: 123`. È buona pratica scrivere la coppia presenter + azione separata dai due punti, come `Product:show`. - -Il router ha quindi trasformato l'URL nella coppia `Presenter:azione` + parametri, nel nostro caso `Product:show` + `id: 123`. Che aspetto abbia un router del genere lo vedete nel file `app/Core/RouterFactory.php` e lo descriviamo in dettaglio nel capitolo [Routing |Routing]. - -Andiamo avanti. `Application` conosce ora il nome del presenter e può proseguire. Lo fa creando un'istanza della classe `ProductPresenter`, che contiene il codice del presenter `Product`. Più precisamente, chiede al container DI di creare il presenter, perché la creazione degli oggetti è compito suo. - -Il presenter potrebbe avere questo aspetto: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ProductRepository $repository, - ) { - } - - public function renderShow(int $id): void - { - // ottiene i dati dal modello e li passa al template - $this->template->product = $this->repository->getProduct($id); - } -} -``` - -Il presenter prende in carico la gestione della richiesta. Il compito è chiaro: eseguire l'azione `show` con `id: 123`. Nella terminologia dei presenter questo significa che viene chiamato il metodo `renderShow()`, che riceve `123` nel parametro `$id`. - -Un presenter può gestire più azioni, può cioè avere più metodi `render()`. Consigliamo però di progettare i presenter con una sola azione o comunque con il minor numero possibile di azioni. - -È stato quindi chiamato il metodo `renderShow(123)`. Il suo codice è un esempio inventato, ma mostra come i dati vengono passati al template, cioè scrivendoli in `$this->template`. - -Il presenter restituisce poi una risposta. Può essere una pagina HTML, un'immagine, un documento XML, l'invio di un file dal disco, un JSON oppure un redirect a un'altra pagina. Attenzione: se non indichiamo esplicitamente come rispondere (come nel caso di `ProductPresenter`), la risposta sarà il rendering di un template in una pagina HTML. Perché? Perché nel 99% dei casi vogliamo disegnare un template. Il presenter adotta quindi questo comportamento come predefinito, per semplificarci il lavoro. È l'essenza di Nette. - -Non dobbiamo nemmeno indicare quale template disegnare: il framework ne deduce automaticamente il percorso. Nel caso dell'azione `show` prova semplicemente a caricare il template `show.latte` che si trova nella stessa directory della classe `ProductPresenter`. Cerca inoltre il layout nel file `@layout.latte` (maggiori dettagli sulla [ricerca dei template |templates#Ricerca dei template]). - -Poi i template vengono disegnati. Così si conclude il compito del presenter e dell'intera applicazione. Se il template non esiste, viene restituita una pagina di errore 404. Potete saperne di più sui presenter nella pagina [Presenter|presenters]. - -[* request-flow.svg *] - -Per sicurezza ricapitoliamo l'intero processo con un URL leggermente diverso: - -1) L'URL è `https://example.com` -2) L'applicazione si avvia, viene creato il container DI e viene eseguito `Application::run()`. -3) Il router decodifica l'URL nella coppia `Home:default`. -4) Viene creata un'istanza della classe `HomePresenter`. -5) Viene chiamato il metodo `renderDefault()` (se esiste). -6) Viene disegnato il template, per esempio `default.latte`, insieme al layout, per esempio `@layout.latte`. - - -Avrete forse incontrato ora molti concetti nuovi, ma crediamo che abbiano senso. Sviluppare applicazioni in Nette è straordinariamente semplice. - - -Template -======== - -A proposito di template, Nette usa il sistema di template [Latte |latte:]. Ecco perché i file dei template hanno estensione `.latte`. Latte viene usato soprattutto perché è il sistema di template più sicuro per PHP, e anche il più intuitivo. Non dovete imparare molto di nuovo: bastano la conoscenza di PHP e qualche tag. Trovate tutto ciò che vi serve [nella documentazione |templates]. - -Nel template [create i link |creating-links] verso altri presenter e azioni così: - -```latte -dettaglio del prodotto -``` - -Basta scrivere la familiare coppia `Presenter:azione` al posto dell'URL vero e proprio e aggiungere gli eventuali parametri necessari. Il trucco sta in `n:href`, che dice a Nette di elaborare questo attributo. Verrà allora generato: - -```latte -dettaglio del prodotto -``` - -Della generazione dell'URL si occupa il router già menzionato. I router in Nette sono eccezionali perché sanno eseguire non solo la trasformazione da URL a coppia `Presenter:azione`, ma anche quella inversa: generare un URL a partire dal nome del presenter, dall'azione e dai parametri. Grazie a questo, in Nette potete cambiare completamente il formato degli URL in tutta l'applicazione già finita senza modificare un solo carattere nei template o nei presenter, semplicemente modificando il router. Ciò rende possibile anche la cosiddetta canonizzazione, un'altra funzionalità unica di Nette che migliora la SEO (Search Engine Optimization) impedendo automaticamente che lo stesso contenuto esista su URL diversi. Molti programmatori trovano questa possibilità stupefacente. - - -Componenti interattivi -====================== - -Dobbiamo dirvi ancora una cosa sui presenter: hanno un sistema di componenti integrato. Chi ha più esperienza ricorderà forse qualcosa di simile in Delphi o in ASP.NET Web Forms; React o Vue.js si basano su concetti in qualche modo affini. Nel mondo dei framework PHP è una funzionalità del tutto unica. - -I componenti sono unità indipendenti e riutilizzabili che incorporiamo nelle pagine (cioè nei presenter). Possono essere [form |forms:in-presenter], [datagrid |https://componette.org/contributte/datagrid/], menu, sondaggi: in pratica tutto ciò che ha senso riutilizzare. Possiamo creare componenti nostri oppure attingere all'[ampia scelta |https://componette.org] di componenti open source. - -I componenti influenzano profondamente il modo di sviluppare le applicazioni. Aprono nuove possibilità di comporre le pagine a partire da unità già pronte. E hanno anche qualcosa in comune con [Hollywood |components#Stile hollywoodiano]. - - -Container DI e configurazione -============================= - -Il container DI, cioè la factory di oggetti, è il cuore dell'intera applicazione. - -Niente paura, non è una scatola nera magica, come potrebbero suggerire le righe precedenti. In realtà è una classe PHP piuttosto banale, generata da Nette e salvata nella directory della cache. Contiene molti metodi con nomi come `createServiceAbcd()`, ognuno capace di creare e restituire un determinato oggetto. Sì, c'è anche un metodo `createServiceApplication__application()`, che produce l'istanza di `Nette\Application\Application` che ci serviva in `index.php` per avviare l'applicazione. Ci sono anche i metodi per creare i singoli presenter e così via. - -Gli oggetti creati dal container DI si chiamano, per qualche motivo, servizi. - -La cosa davvero particolare di questa classe è che non la programmate voi: la genera il framework. Genera davvero il codice PHP e lo salva su disco. Voi vi limitate a fornire le istruzioni su quali oggetti il container deve saper creare, e come esattamente. Queste istruzioni si scrivono nei [file di configurazione |bootstrapping#Configurazione del container DI], che usano il formato [NEON|neon:format] e hanno quindi estensione `.neon`. - -I file di configurazione servono esclusivamente a istruire il container DI. Se quindi indicate, per esempio, l'opzione `expiration: 14 days` nella sezione [session |http:configuration#Sessione], il container DI, creando l'oggetto `Nette\Http\Session` che rappresenta la sessione, ne chiamerà il metodo `setExpiration('14 days')`, rendendo così reale la configurazione. - -C'è un intero capitolo pronto per voi che descrive cosa si può [configurare |nette:configuring] e come [definire i propri servizi |dependency-injection:services]. - -Appena vi addentrerete un po' nella creazione dei servizi, incontrerete il termine [autowiring |dependency-injection:autowiring]. È una funzionalità che vi semplificherà incredibilmente la vita: sa passare automaticamente gli oggetti dove vi servono (per esempio nei costruttori delle vostre classi), senza che dobbiate fare nulla. Scoprirete che il container DI di Nette è un piccolo miracolo. - - -E adesso? -========= - -Abbiamo trattato i principi fondamentali delle applicazioni Nette. Finora è stata una panoramica superficiale, ma presto approfondirete e col tempo creerete applicazioni web straordinarie. Dove andare adesso? Avete già provato il tutorial [Crea la tua prima applicazione|quickstart:]? - -Oltre a quanto descritto sopra, Nette offre tutto un arsenale di [classi utili|utils:], uno [strato per il database|database:] e altro ancora. Provate a curiosare nella documentazione. Oppure visitate il [blog|https://blog.nette.org]. Scoprirete molte cose interessanti. - -Che il framework vi porti molta gioia 💙 diff --git a/application/it/multiplier.texy b/application/it/multiplier.texy deleted file mode 100644 index f4c003c96d..0000000000 --- a/application/it/multiplier.texy +++ /dev/null @@ -1,63 +0,0 @@ -Multiplier: componenti dinamici -******************************* - -.[perex] -Uno strumento per creare dinamicamente componenti interattivi. - -Cominciamo con un esempio tipico: immaginate l'elenco dei prodotti di un e-shop, in cui per ogni articolo volete un form "Aggiungi al carrello". Un approccio possibile è racchiudere l'intero elenco in un unico form. Un metodo molto più comodo lo offre però [api:Nette\Application\UI\Multiplier]. - -Multiplier vi permette di definire comodamente una factory per più componenti. Funziona sul principio dei componenti annidati: qualsiasi componente che eredita da [api:Nette\ComponentModel\Container] può contenere altri componenti. - -.[tip] -Vedi nella documentazione il capitolo sul [modello a componenti |components#I componenti in profondità]. - -L'essenza di Multiplier sta nel fatto che agisce da genitore capace di creare dinamicamente i propri figli tramite una callback passata al costruttore. Ecco l'esempio: - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function () { - $form = new Nette\Application\UI\Form; - $form->addInteger('amount', 'Quantità:') - ->setRequired(); - $form->addSubmit('send', 'Aggiungi al carrello'); - return $form; - }); -} -``` - -Ora, nel template, possiamo semplicemente disegnare il form per ogni prodotto, e ognuno sarà davvero un componente a sé. - -```latte -{foreach $items as $item} -

    {$item->title}

    - {$item->description} - - {control "shopForm-$item->id"} -{/foreach} -``` - -L'argomento passato nel tag `{control}` segue un formato che significa: - -1. Ottieni il componente `shopForm`. -2. Da esso ottieni il figlio chiamato `$item->id`. - -Alla prima esecuzione del punto **1** il componente `shopForm` non esiste ancora, quindi viene chiamata la sua factory `createComponentShopForm`. Poi, sul componente ottenuto (un'istanza di Multiplier), viene chiamata la factory del form specifico, cioè la funzione anonima che abbiamo passato al costruttore di Multiplier. - -Nell'iterazione successiva del ciclo foreach il metodo `createComponentShopForm` non verrà chiamato di nuovo (perché il componente esiste già). Poiché però cerchiamo un figlio diverso (dato che `$item->id` sarà diverso a ogni iterazione), la funzione anonima verrà chiamata di nuovo e restituirà un nuovo form. - -Resta solo da garantire che il form aggiunga al carrello il prodotto giusto: al momento il form è identico per ogni prodotto. Qui ci aiuta una caratteristica di Multiplier (e in generale di qualsiasi factory di componenti in Nette Framework): ogni factory riceve come primo argomento il nome del componente che si sta creando. La factory di Multiplier riceve inoltre come secondo argomento l'istanza stessa di Multiplier. Nel nostro caso il primo argomento sarà `$item->id`, che è esattamente l'informazione che ci serve. Basta quindi modificare leggermente la creazione del form: - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function (string $itemId) { - $form = new Nette\Application\UI\Form; - $form->addInteger('amount', 'Quantità:') - ->setRequired(); - $form->addHidden('itemId', $itemId); - $form->addSubmit('send', 'Aggiungi al carrello'); - return $form; - }); -} -``` diff --git a/application/it/presenters.texy b/application/it/presenters.texy deleted file mode 100644 index b735eb3ca6..0000000000 --- a/application/it/presenters.texy +++ /dev/null @@ -1,595 +0,0 @@ -Presenter -********* - -
    - -Esamineremo come si scrivono i presenter e i template in Nette. Dopo la lettura capirete: - -- come funzionano i presenter -- cosa sono i parametri persistenti -- come vengono disegnati i template - -
    - -[Sappiamo già |how-it-works#Nette Application] che un presenter è una classe che rappresenta una determinata pagina di un'applicazione web, per esempio la home page, un prodotto di un e-shop, un form di login, un feed sitemap e così via. Un'applicazione può avere da uno a migliaia di presenter. In altri framework sono noti anche come controller. - -Di norma con il termine presenter si intende un discendente della classe [api:Nette\Application\UI\Presenter], adatta a generare interfacce web e alla quale sarà dedicato il resto di questo capitolo. In senso generale, un presenter è qualsiasi oggetto che implementi l'interfaccia [api:Nette\Application\IPresenter]. - - -Ciclo di vita del presenter -=========================== - -Il compito del presenter è gestire una richiesta e restituire una risposta (che può essere una pagina HTML, un'immagine, un redirect ecc.). - -All'inizio, quindi, gli viene passata una richiesta. Non è la richiesta HTTP diretta, ma un oggetto [api:Nette\Application\Request], nel quale la richiesta HTTP è stata trasformata con l'aiuto del router. Di norma non interagiamo direttamente con questo oggetto, perché il presenter delega abilmente l'elaborazione della richiesta ad altri metodi, che esamineremo ora. - -[* lifecycle.svg *] *** Ciclo di vita del presenter .<> - -Lo schema mostra l'elenco dei metodi che vengono chiamati in sequenza dall'alto verso il basso, se esistono. Nessuno di essi è obbligatorio: potete avere un presenter completamente vuoto, senza un solo metodo, e costruirci sopra un semplice sito statico. - - -`__construct()` ---------------- - -Il costruttore non appartiene in senso stretto al ciclo di vita del presenter, perché viene chiamato nel momento in cui l'oggetto viene creato. Lo menzioniamo però per la sua importanza. Il costruttore (insieme al [metodo inject|best-practices:inject-method-attribute]) serve a passare le dipendenze. - -Il presenter non deve occuparsi della logica di business dell'applicazione, non deve scrivere sul database o leggerne, non deve eseguire calcoli e così via. È compito delle classi dello strato che chiamiamo model. Per esempio, una classe `ArticleRepository` può occuparsi di caricare e salvare gli articoli. Perché il presenter possa lavorarci, deve farsela [passare tramite dependency injection |dependency-injection:passing-dependencies]: - - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articles, - ) { - } -} -``` - - -`startup()` ------------ - -Subito dopo aver ricevuto la richiesta viene richiamato il metodo `startup()`. Potete usarlo per inizializzare le proprietà, controllare i permessi dell'utente e così via. È obbligatorio che questo metodo chiami sempre il metodo genitore: `parent::startup()`. - - -`action(args...)` .{toc: action()} --------------------------------------------------- - -È simile al metodo `render()`. Mentre `render()` serve a preparare i dati per un determinato template che verrà poi disegnato, `action()` elabora una richiesta senza per forza disegnare poi un template. Per esempio può elaborare dei dati, autenticare o disconnettere un utente e così via, per poi [reindirizzare altrove |#Redirect]. - -È importante che `action()` venga chiamato *prima* di `render()`. Questo ci permette eventualmente di cambiare il corso della richiesta dentro il metodo action, per esempio cambiando il template che verrà disegnato o perfino il metodo `render()` che verrà chiamato, con `setView('otherView')`. - -.{data-version:3.2.3} -Potete perfino passare a un'azione completamente diversa con il metodo `switch('otherAction')`. Interrompe il metodo corrente ed esegue invece i metodi `action()` e `render()` della nuova azione (e disattiva la [canonizzazione|#Canonizzazione] automatica). La richiesta in sé prosegue; viene interrotto solo il metodo attualmente in esecuzione. - -Al metodo vengono passati i parametri della richiesta. È possibile, e consigliato, indicare i tipi di questi parametri, per esempio `actionShow(int $id, ?string $slug = null)`. Se il parametro `id` manca o non è un intero, il presenter restituisce un [errore 404 |#Errore 404 e simili] e termina. - - -`handle(args...)` .{toc: handle()} ---------------------------------------------------- - -Questo metodo elabora i cosiddetti segnali, di cui parleremo nel capitolo dedicato ai [componenti |components#Segnale]. È destinato soprattutto ai componenti e alla gestione delle richieste AJAX. - -Al metodo vengono passati i parametri della richiesta, come per `action()`, compreso il controllo dei tipi. - - -`beforeRender()` ----------------- - -Il metodo `beforeRender`, come suggerisce il nome, viene chiamato prima di ogni metodo `render()`. Serve per la configurazione comune del template, per passare variabili al layout e per compiti simili. - - -`render(args...)` .{toc: render()} ------------------------------------------------ - -È qui che prepariamo il template per il successivo rendering, gli passiamo i dati e così via. - -Al metodo vengono passati i parametri della richiesta, come per `action()`, compreso il controllo dei tipi. - -```php -public function renderShow(int $id): void -{ - // ottiene i dati dal modello e li passa al template - $this->template->article = $this->articles->getById($id); -} -``` - - -`afterRender()` ---------------- - -Il metodo `afterRender`, come suggerisce di nuovo il nome, viene chiamato dopo ogni metodo `render()`. Si usa piuttosto di rado. - - -`shutdown()` ------------- - -Viene chiamato alla fine del ciclo di vita del presenter. - - -Eventi ------- - -Oltre ai metodi `startup()`, `beforeRender()` e `shutdown()`, chiamati nell'ambito del ciclo di vita del presenter, si possono definire altre funzioni da chiamare automaticamente. Il presenter definisce i cosiddetti [eventi |nette:glossary#Eventi] e voi aggiungete i loro gestori negli array `$onStartup`, `$onRender` e `$onShutdown`. - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct() - { - $this->onStartup[] = function () { - // ... - }; - } -} -``` - -I gestori dell'array `$onStartup` vengono chiamati subito prima del metodo `startup()`, quelli di `$onRender` tra `beforeRender()` e `render()` e infine quelli di `$onShutdown` subito prima di `shutdown()`. - - -**Un consiglio prima di proseguire:** come vedete, un presenter può gestire più azioni/viste, può cioè avere più metodi `render()`. Consigliamo però di progettare i presenter con una sola azione o comunque con il minor numero possibile di azioni. - - -Inviare una risposta -==================== - -La risposta del presenter è di norma il [rendering di un template in una pagina HTML|templates], ma può essere anche l'invio di un file, di un JSON o perfino un redirect a un'altra pagina. - -In qualsiasi momento del ciclo di vita possiamo usare uno dei metodi seguenti per inviare una risposta e terminare contemporaneamente il presenter: - -- `redirect()`, `redirectPermanent()`, `redirectUrl()` e `forward()` eseguono un [redirect |#Redirect] -- `error()` termina il presenter [a causa di un errore |#Errore 404 e simili] -- `sendJson($data)` termina il presenter e [invia i dati |#Inviare JSON] in formato JSON -- `sendTemplate()` termina il presenter e [disegna subito il template |templates] -- `sendResponse($response)` termina il presenter e invia una [risposta personalizzata |#Risposte] -- `terminate()` termina il presenter senza alcuna risposta - -Ognuno di questi metodi termina immediatamente il presenter sollevando l'eccezione di terminazione silenziosa `Nette\Application\AbortException`. - -Se non chiamate nessuno di questi metodi, il presenter passa automaticamente al rendering del template. Perché? Perché nel 99% dei casi vogliamo disegnare un template, quindi il presenter adotta questo comportamento come predefinito per semplificarci il lavoro. - - -Creare i link -============= - -Il presenter ha un metodo `link()` che serve a creare link URL verso altri presenter. Il primo parametro è il presenter e l'azione di destinazione, seguiti dagli argomenti, che si possono passare come array: - -```php -$url = $this->link('Product:show', $id); - -$url = $this->link('Product:show', [$id, 'lang' => 'en']); -``` - -Nel template i link verso altri presenter e azioni si creano così: - -```latte -dettaglio del prodotto -``` - -Basta scrivere la familiare coppia `Presenter:azione` al posto dell'URL vero e proprio e aggiungere gli eventuali parametri necessari. Il trucco sta in `n:href`, che dice a Latte di elaborare questo attributo e di generare l'URL reale. In Nette non dovete pensare affatto agli URL, solo ai presenter e alle azioni. - -Maggiori informazioni si trovano nel capitolo [Creazione di link URL|creating-links]. - - -Redirect -======== - -Per passare a un altro presenter si usano i metodi `redirect()` e `forward()`. Hanno una sintassi molto simile a quella del metodo [link() |#Creare i link]. - -Il metodo `forward()` passa subito al nuovo presenter, senza un redirect HTTP: - -```php -$this->forward('Product:show'); -``` - -Esempio di redirect temporaneo con codice HTTP 302 (o 303 se il metodo della richiesta corrente è POST): - -```php -$this->redirect('Product:show', $id); -``` - -Per ottenere un redirect permanente con codice HTTP 301, usate questo: - -```php -$this->redirectPermanent('Product:show', $id); -``` - -Potete reindirizzare a un altro URL fuori dall'applicazione con il metodo `redirectUrl()`. Il codice HTTP si può indicare come secondo parametro; il valore predefinito è 302 (o 303 se il metodo della richiesta corrente è POST): - -```php -$this->redirectUrl('https://nette.org'); -``` - -Il redirect termina immediatamente l'attività del presenter sollevando la cosiddetta eccezione di terminazione silenziosa, `Nette\Application\AbortException`. - -Prima del redirect è possibile inviare [messaggi flash |#Messaggi flash], cioè messaggi che verranno mostrati nel template dopo il redirect. - - -Messaggi flash -============== - -Sono messaggi che di norma informano sull'esito di qualche operazione. Una caratteristica importante dei messaggi flash è che restano disponibili nel template anche dopo un redirect. Una volta mostrati restano attivi per altri 30 secondi: se per esempio l'utente ricarica la pagina per un errore di trasmissione, il messaggio non sparisce subito. - -Basta chiamare il metodo [flashMessage() |api:Nette\Application\UI\Control::flashMessage()] e il presenter si occupa di passarlo al template. Il primo parametro è il testo del messaggio, il secondo, facoltativo, è il suo tipo (per esempio error, warning, info). Il metodo `flashMessage()` restituisce un'istanza del messaggio flash, alla quale si possono aggiungere altre informazioni. - -```php -$this->flashMessage('L\'elemento è stato eliminato.'); -$this->redirect(/* ... */); // e reindirizza -``` - -Nel template questi messaggi sono disponibili nella variabile `$flashes` come oggetti `stdClass`, che contengono le proprietà `message` (il testo del messaggio), `type` (il tipo del messaggio) ed eventualmente le informazioni aggiunte dall'utente già menzionate. Li disegniamo così: - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -Errore 404 e simili -=================== - -Se la richiesta non si può soddisfare, per esempio perché l'articolo che vogliamo mostrare non esiste nel database, solleviamo un errore 404 con il metodo `error(string $message = '', int $httpCode = 404)`. - -```php -public function renderShow(int $id): void -{ - $article = $this->articles->getById($id); - if (!$article) { - $this->error(); - } - // ... -} -``` - -Il codice di errore HTTP si può passare come secondo parametro; il valore predefinito è 404. Il metodo funziona sollevando una `Nette\Application\BadRequestException`, dopo la quale `Application` passa il controllo all'error presenter. È un presenter il cui compito è mostrare una pagina che informa dell'errore avvenuto. L'error presenter si imposta nella [configurazione dell'applicazione|configuration]. - - -Inviare JSON -============ - -Il metodo `sendJson($data)` codifica i dati indicati in JSON, li invia come risposta HTTP e termina il presenter. Esempio: - -```php -public function actionData(): void -{ - $data = ['hello' => 'nette']; - $this->sendJson($data); -} -``` - - -Parametri della richiesta .{data-version:3.1.14} -================================================ - -Il presenter, e anche ogni componente, ottiene i propri parametri dalla richiesta HTTP. Potete leggerne i valori con i metodi `getParameter($name)` o `getParameters()`. I valori sono stringhe o array di stringhe, in sostanza dati grezzi ottenuti direttamente dall'URL. - -Per maggiore comodità consigliamo di accedere ai parametri tramite le proprietà. Basta contrassegnarle con l'attributo `#[Parameter]`: - -```php -use Nette\Application\Attributes\Parameter; // questa riga è importante - -class HomePresenter extends Nette\Application\UI\Presenter -{ - #[Parameter] - public string $theme; // deve essere public -} -``` - -Per la proprietà consigliamo di indicare il tipo di dato (per esempio `string`) e Nette convertirà automaticamente il valore di conseguenza. I valori dei parametri si possono anche [validare |#Validazione dei parametri]. - -Quando create un link potete impostare direttamente il valore del parametro: - -```latte -clicca -``` - - -Parametri persistenti -===================== - -I parametri persistenti servono a mantenere lo stato tra richieste diverse. Il loro valore resta lo stesso anche dopo aver cliccato un link. A differenza dei dati di sessione, vengono trasmessi nell'URL. E questo avviene in modo completamente automatico, quindi non c'è bisogno di indicarli esplicitamente in `link()` o in `n:href`. - -Un esempio d'uso? Immaginate di avere un'applicazione multilingue. La lingua corrente è un parametro che deve sempre far parte dell'URL. Ma sarebbe incredibilmente noioso indicarlo in ogni link. Lo rendete quindi un parametro persistente `lang` e verrà trascinato automaticamente. Comodo! - -Creare un parametro persistente in Nette è estremamente semplice. Basta creare una proprietà pubblica e contrassegnarla con l'attributo: (in precedenza si usava `/** @persistent */`) - -```php -use Nette\Application\Attributes\Persistent; // questa riga è importante - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; // deve essere public -} -``` - -Se `$this->lang` ha un valore come `'en'`, allora anche i link creati con `link()` o `n:href` conterranno il parametro `lang=en`. E dopo aver cliccato il link, `$this->lang` sarà di nuovo `'en'`. - -Per la proprietà consigliamo di indicare il tipo di dato (per esempio `string`) e potete anche fornire un valore predefinito. I valori dei parametri si possono [validare |#Validazione dei parametri]. - -I parametri persistenti vengono di norma trasferiti tra tutte le azioni di un determinato presenter. Per trasferirli anche tra più presenter, vanno definiti: - -- in un antenato comune dal quale i presenter ereditano -- oppure in un trait che i presenter usano: - -```php -trait LanguageAware -{ - #[Persistent] - public string $lang; -} - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - use LanguageAware; -} -``` - -Quando si crea un link, il valore di un parametro persistente si può cambiare: - -```latte -dettaglio in ceco -``` - -In alternativa lo si può *azzerare*, cioè rimuovere dall'URL. Assumerà allora il proprio valore predefinito: - -```latte -clicca -``` - - -Spazio condiviso dei parametri -============================== - -I parametri della richiesta, i [parametri persistenti |#Parametri persistenti] e i parametri dei metodi `action`, `render` e `handle` (segnale) condividono un unico spazio, in cui ognuno è identificato dal proprio nome. Se lo stesso nome compare in più di uno di essi, si riferiscono a uno stesso e identico valore. - -Spesso lo si sfrutta a proprio vantaggio. Per esempio, il parametro persistente `lang` e l'argomento `$lang` di un metodo di azione o di segnale sono la stessa cosa: potete leggere il valore corrente di un parametro persistente semplicemente elencandolo nella firma del metodo: - -```php -#[Persistent] -public string $lang; - -public function handleSearch(string $query, string $lang): void -{ - // $lang contiene il valore corrente del parametro persistente lang -} -``` - -Poiché questo spazio è condiviso, tenete unici i nomi dei parametri, a meno che non vogliate deliberatamente che condividano un valore. Questo vale anche per i segnali, che in più leggono i parametri dal corpo POST della richiesta, vedi [I segnali in profondità |components#I segnali in profondità]. - - -Componenti interattivi -====================== - -I presenter hanno un sistema di componenti integrato. I componenti sono unità riutilizzabili autonome che incorporiamo nei presenter. Possono essere [form |forms:in-presenter], datagrid, menu, in pratica tutto ciò che ha senso usare più volte. - -Come si incorporano i componenti nei presenter e come li si usa poi? Lo imparerete nel capitolo [Componenti |components]. Scoprirete perfino cosa hanno in comune con Hollywood. - -E dove trovo i componenti? Su [Componette |https://componette.org/search/component] trovate componenti open source e molte altre estensioni per Nette, offerte da volontari della comunità del framework. - - -Andiamo più a fondo -=================== - -.[tip] -Quello che abbiamo trattato finora in questo capitolo basterà probabilmente per la maggior parte degli usi. Le sezioni seguenti sono destinate a chi vuole approfondire i presenter e sapere assolutamente tutto. - - -Validazione dei parametri -------------------------- - -I valori dei [parametri della richiesta |#Parametri della richiesta] e dei [parametri persistenti |#Parametri persistenti] ricevuti dagli URL vengono scritti nelle proprietà dal metodo `loadState()`. Esso controlla anche che il tipo di dato indicato nella proprietà corrisponda; in caso contrario risponde con un errore 404 e la pagina non viene mostrata. - -Non fidatevi mai ciecamente dei parametri ricevuti dall'URL, perché l'utente può sovrascriverli facilmente. Ecco per esempio come verificheremmo che la lingua `$this->lang` sia tra quelle supportate. Un modo adatto è sovrascrivere il metodo `loadState()` già menzionato: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; - - public function loadState(array $params): void - { - parent::loadState($params); // qui viene impostato $this->lang - // segue il controllo personalizzato del valore: - if (!in_array($this->lang, ['en', 'cs'])) { - $this->error(); - } - } -} -``` - - -Salvare e ripristinare la richiesta ------------------------------------ - -La richiesta gestita dal presenter è un oggetto [api:Nette\Application\Request], restituito dal metodo `getRequest()` del presenter. - -La richiesta corrente si può salvare nella sessione oppure, al contrario, ripristinarla da essa e farla eseguire di nuovo al presenter. È utile, per esempio, quando un utente sta compilando un form e la sua sessione di login scade. Per non perdere i dati, prima di reindirizzare alla pagina di login salviamo la richiesta corrente nella sessione con `$reqId = $this->storeRequest()`. Questo restituisce il suo identificatore come stringa breve, che passiamo poi come parametro al presenter di login. - -Dopo il login chiamiamo il metodo `$this->restoreRequest($reqId)`, che recupera la richiesta dalla sessione. Le richieste POST le vengono inoltrate, mentre le altre (GET) vengono reindirizzate all'URL della richiesta. Il metodo verifica che la richiesta sia stata creata dallo stesso utente ora connesso. Se accede un utente diverso o se la chiave non è valida, non fa nulla e il programma prosegue come al solito. - -Vedi la guida [Come tornare a una pagina precedente |best-practices:restore-request]. - - -Canonizzazione --------------- - -I presenter hanno una funzionalità davvero eccellente, che contribuisce a una migliore SEO (Search Engine Optimization). Impediscono automaticamente l'esistenza di contenuto duplicato su URL diversi. Se più URL portano a una determinata destinazione, per esempio `/index` e `/index?page=1`, il framework ne designa uno come principale (canonico) e vi reindirizza gli altri con il codice HTTP 301. Grazie a questo i motori di ricerca non indicizzano le vostre pagine due volte e non ne diluiscono il page rank. - -Questo processo si chiama canonizzazione. L'URL canonico è quello generato dal [router|routing], di norma la prima route corrispondente della collezione. - -La canonizzazione è attiva per impostazione predefinita e si può disattivare con `$this->autoCanonicalize = false`. - -Il redirect non avviene durante le richieste AJAX o POST, perché potrebbe portare a una perdita di dati o non offrirebbe alcun valore aggiunto in termini di SEO. - -Potete anche avviare la canonizzazione manualmente con il metodo `canonicalize()`. Come per il metodo `link()`, gli passate il presenter, l'azione e i parametri. Genera un link e lo confronta con l'indirizzo URL corrente. Se differiscono, reindirizza al link generato. - -```php -public function actionShow(int $id, ?string $slug = null): void -{ - $realSlug = $this->facade->getSlugForId($id); - // reindirizza se $slug è diverso da $realSlug - $this->canonicalize('Product:show', [$id, $realSlug]); -} -``` - -Per uno schema completo che combina i filtri delle route con `canonicalize()` per produrre URL adatti alla SEO, vedi [URL leggibili con gli slug |best-practices:pretty-urls]. - - -Risposte --------- - -La risposta restituita dal presenter è un oggetto che implementa l'interfaccia [api:Nette\Application\Response]. Sono disponibili diverse risposte già pronte: - -- [api:Nette\Application\Responses\CallbackResponse] - invia una callback -- [api:Nette\Application\Responses\FileResponse] - invia il file -- [api:Nette\Application\Responses\ForwardResponse] - forward() -- [api:Nette\Application\Responses\JsonResponse] - invia JSON -- [api:Nette\Application\Responses\RedirectResponse] - redirect -- [api:Nette\Application\Responses\TextResponse] - invia testo -- [api:Nette\Application\Responses\VoidResponse] - risposta vuota - -Le risposte si inviano con il metodo `sendResponse()`: - -```php -use Nette\Application\Responses; - -// testo semplice -$this->sendResponse(new Responses\TextResponse('Hello Nette!')); - -// invia un file -$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf')); - -// invia una callback -$callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) { - if ($httpResponse->getHeader('Content-Type') === 'text/html') { - echo '

    Hello

    '; - } -}; -$this->sendResponse(new Responses\CallbackResponse($callback)); -``` - -Potete anche scrivere una vostra risposta. Basta implementare l'interfaccia `Nette\Application\Response`, che ha un unico metodo `send()`, il quale riceve la richiesta e la risposta HTTP. È utile, per esempio, quando si trasmettono in streaming dati che non volete tenere in memoria: - -```php -class CsvResponse implements Nette\Application\Response -{ - public function __construct( - private string $fileName, - private iterable $rows, - ) { - } - - public function send(Nette\Http\IRequest $request, Nette\Http\IResponse $response): void - { - $response->setContentType('text/csv', 'utf-8'); - $response->sendAsFile($this->fileName); - - $handle = fopen('php://output', 'w'); - foreach ($this->rows as $row) { - fputcsv($handle, $row); - } - - fclose($handle); - } -} -``` - -La inviate poi nel presenter come al solito: `$this->sendResponse(new CsvResponse('export.csv', $rows));` - - -Caching HTTP ------------- - -Il metodo `lastModified()` rende semplice sfruttare il caching HTTP. Gli passate la data e l'ora dell'ultima modifica del contenuto (come timestamp, stringa oppure oggetto `DateTimeInterface`) e, facoltativamente, un validatore ETag (una breve stringa che identifica la versione corrente del contenuto, per esempio un suo hash) e un tempo di scadenza. Se il browser possiede già una versione corrispondente, il presenter invia una risposta `304 Not Modified` e termina, così la pagina non viene disegnata né trasferita inutilmente: - -```php -public function renderArticle(int $id): void -{ - $article = $this->articles->getById($id); - $this->lastModified($article->updatedAt); - // ... -} -``` - - -Completare il template .{data-version:3.3.0} --------------------------------------------- - -Quando il presenter disegna un template, il metodo `sendTemplate()` chiama `completeTemplate()` subito prima del rendering. Questo metodo riempie le variabili contrassegnate con l'attributo `#[TemplateVariable]` e individua il file del template (le variabili predefinite sono già impostate dal `TemplateFactory` al momento della creazione del template). Potete sovrascrivere questo metodo protected per aggiungere variabili condivise da tutte le viste o per impostare un file diverso: - -```php -protected function completeTemplate(Nette\Application\UI\Template $template): void -{ - parent::completeTemplate($template); - $template->siteName = 'My App'; -} -``` - - -Limitare l'accesso con `#[Requires]` .{data-version:3.2.3} ----------------------------------------------------------- - -L'attributo `#[Requires]` offre possibilità avanzate per limitare l'accesso ai presenter e ai loro metodi. Si può usare per indicare i metodi HTTP, per richiedere una richiesta AJAX, per limitare alla stessa origine e per consentire l'accesso solo tramite forwarding. L'attributo si può applicare sia alle classi dei presenter sia ai singoli metodi come `action()`, `render()`, `handle()` e `createComponent()`. - -Potete indicare queste restrizioni: -- sui metodi HTTP: `#[Requires(methods: ['GET', 'POST'])]` -- richiedere una richiesta AJAX: `#[Requires(ajax: true)]` -- accesso solo dalla stessa origine: `#[Requires(sameOrigin: true)]` -- accesso solo tramite forwarding: `#[Requires(forward: true)]` -- restrizioni su azioni specifiche: `#[Requires(actions: 'default')]` - -.[note] -Dalla versione 3.3 la corrispondenza della stessa origine è verificata tramite l'header `Sec-Fetch-Site` del browser (prima tramite un cookie SameSite), il che è più affidabile e controlla la corrispondenza esatta di schema, dominio e porta. - -I dettagli si trovano nella guida [Come usare l'attributo Requires |best-practices:attribute-requires]. - - -Controllo del metodo HTTP -------------------------- - -I presenter in Nette verificano automaticamente il metodo HTTP di ogni richiesta in arrivo, soprattutto per motivi di sicurezza. Per impostazione predefinita sono ammessi i metodi `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH`. - -Se volete ammettere in più, per esempio, il metodo `OPTIONS`, usate l'attributo `#[Requires]` (da Nette Application v3.2.3): - -```php -#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])] -class MyPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Dalla versione 3.1.13 la verifica avviene in `checkHttpMethod()`, che controlla se il metodo indicato nella richiesta è presente nell'array `$presenter->allowedMethods`. Dalla versione 3.2.3 questo approccio è deprecato in favore di `#[Requires]`. Potete sovrascrivere il metodo così: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - protected function checkHttpMethod(): void - { - $this->allowedMethods[] = 'OPTIONS'; - parent::checkHttpMethod(); - } -} -``` - -È importante sottolineare che, se attivate il metodo `OPTIONS`, dovete poi gestirlo in modo appropriato nel vostro presenter. Questo metodo è spesso usato come cosiddetta richiesta preflight, che il browser invia automaticamente prima della richiesta vera e propria quando è necessario stabilire se la richiesta è ammissibile secondo la policy CORS (Cross-Origin Resource Sharing). Se attivate il metodo ma non implementate la risposta corretta, si possono creare incoerenze e potenziali problemi di sicurezza. - - -Contrassegnare le azioni deprecate .{data-version:3.2.3} --------------------------------------------------------- - -L'attributo `#[Deprecated]` contrassegna azioni, segnali o interi presenter come deprecati e destinati alla rimozione futura. Generando link verso parti deprecate dell'applicazione, Nette emette un avviso per allertare gli sviluppatori. - -Potete applicare l'attributo all'intera classe del presenter oppure ai singoli metodi `action()`, `render()` e `handle()`. - - -Letture consigliate -=================== - -- [Metodi e attributi inject |best-practices:inject-method-attribute] -- [Comporre i presenter con i trait |best-practices:presenter-traits] -- [Passare le impostazioni ai presenter |best-practices:passing-settings-to-presenters] -- [Come tornare a una pagina precedente |best-practices:restore-request] diff --git a/application/it/routing.texy b/application/it/routing.texy deleted file mode 100644 index 88b63e9400..0000000000 --- a/application/it/routing.texy +++ /dev/null @@ -1,749 +0,0 @@ -Routing -******* - -
    - -Il router si occupa di tutto ciò che riguarda gli indirizzi URL, così non dovete pensarci voi. Vi mostreremo: - -- come configurare il router perché gli URL appaiano come volete -- parleremo di SEO e di redirect -- e mostreremo come scrivere un router personalizzato - -
    - - -Gli URL più amichevoli (noti anche come cool o pretty URL) sono più usabili, più facili da ricordare e contribuiscono positivamente alla SEO. Nette ne tiene conto e va pienamente incontro alle esigenze degli sviluppatori. Potete progettare per la vostra applicazione esattamente la struttura di URL che volete. Potete perfino progettarla quando l'applicazione è già finita, perché non richiede alcuna modifica al codice o ai template. Si definisce elegantemente in [un unico punto |#Integrazione], il router, invece di essere sparsa come annotazioni in tutti i presenter. - -Il router in Nette è eccezionale perché è **bidirezionale**. Sa sia decodificare gli URL delle richieste HTTP sia creare link. Ha quindi un ruolo essenziale in [Nette Application |how-it-works#Nette Application], perché non solo decide quale presenter e quale azione eseguiranno la richiesta corrente, ma serve anche a [generare gli URL |creating-links] nei template e altrove. - -Il router non è però limitato a questo uso: potete usarlo in applicazioni in cui i presenter non vengono usati affatto, per API REST e così via. Maggiori dettagli nella sezione [#Uso autonomo]. - - -Collezione di route -=================== - -Il modo più piacevole di definire la struttura degli indirizzi URL di un'applicazione lo offre la classe [api:Nette\Application\Routers\RouteList]. La definizione consiste in un elenco di cosiddette route, cioè di maschere di indirizzi URL e dei presenter e delle azioni a esse associati, tramite un'API semplice. Non abbiamo bisogno di dare un nome alle route. - -```php -$router = new Nette\Application\Routers\RouteList; -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('article/', 'Article:view'); -// ... -``` - -L'esempio mostra che, se apriamo nel browser `https://domain.com/rss.xml`, verrà mostrato il presenter `Feed` con l'azione `rss`. Se apriamo `https://domain.com/article/12`, verrà mostrato il presenter `Article` con l'azione `view` e così via. Se non viene trovata alcuna route adatta, Nette Application risponde sollevando una [BadRequestException |api:Nette\Application\BadRequestException], che viene mostrata all'utente come pagina di errore 404 Not Found. - - -Ordine delle route ------------------- - -L'**ordine** in cui sono elencate le singole route è assolutamente **essenziale**, perché vengono valutate in sequenza dall'alto verso il basso. La regola è dichiarare le route **dalla più specifica alla più generica**: - -```php -// SBAGLIATO: 'rss.xml' viene catturato dalla prima route, che intende questa stringa come -$router->addRoute('', 'Article:view'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// GIUSTO -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('', 'Article:view'); -``` - -Anche nella generazione dei link le route vengono valutate dall'alto verso il basso: - -```php -// SBAGLIATO: il link a 'Feed:rss' viene generato come 'admin/feed/rss' -$router->addRoute('admin//', 'Admin:default'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// GIUSTO -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('admin//', 'Admin:default'); -``` - -Non vi nasconderemo che comporre correttamente le route richiede una certa abilità. Finché non l'avrete padroneggiata, il [pannello del routing |#Debugging del router] vi sarà uno strumento utile. - - -Maschera e parametri --------------------- - -La maschera descrive il percorso relativo alla directory radice del sito. La maschera più semplice è un URL statico: - -```php -$router->addRoute('products', 'Products:default'); -``` - -Spesso le maschere contengono i cosiddetti **parametri**. Sono racchiusi tra parentesi angolari (per esempio ``) e vengono passati al presenter di destinazione, per esempio al metodo `renderShow(int $year)` oppure al parametro persistente `$year`: - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -L'esempio mostra che, se apriamo nel browser `https://example.com/chronicle/2020`, verrà mostrato il presenter `History` con l'azione `show` e il parametro `year: 2020`. - -Possiamo indicare un valore predefinito per i parametri direttamente nella maschera, rendendoli così facoltativi: - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -La route accetterà ora anche l'URL `https://example.com/chronicle/`, che mostrerà di nuovo `History:show` con il parametro `year: 2020`. - -Naturalmente anche i nomi del presenter e dell'azione possono essere parametri. Per esempio: - -```php -$router->addRoute('/', 'Home:default'); -``` - -La route indicata accetta, per esempio, URL nella forma `/article/edit` o `/catalog/list` e li interpreta rispettivamente come i presenter e le azioni `Article:edit` e `Catalog:list`. - -Allo stesso tempo assegna ai parametri `presenter` e `action` i valori predefiniti `Home` e `default`, rendendoli così anch'essi facoltativi. La route accetta quindi anche un URL come `/article` e lo interpreta come `Article:default`. Oppure, al contrario, un link a `Product:default` genera il percorso `/product` e un link al `Home:default` predefinito genera il percorso `/`. - -La maschera può descrivere non solo il percorso relativo alla directory radice del sito, ma anche un percorso assoluto se inizia con una barra, oppure perfino l'intero URL assoluto se inizia con due barre: - -```php -// relativo al document root -$router->addRoute('/', /* ... */); - -// percorso assoluto (relativo al dominio) -$router->addRoute('//', /* ... */); - -// URL assoluto, dominio compreso (relativo allo schema) -$router->addRoute('//.example.com//', /* ... */); - -// URL assoluto, schema compreso -$router->addRoute('https://.example.com//', /* ... */); -``` - - -Espressioni di validazione --------------------------- - -Per ogni parametro si può indicare una condizione di validazione tramite un'[espressione regolare|https://www.php.net/manual/en/reference.pcre.pattern.syntax.php]. Per esempio, per il parametro `id` indichiamo che può contenere solo cifre, con la regex `\d+`: - -```php -$router->addRoute('/[/]', /* ... */); -``` - -L'espressione regolare predefinita di tutti i parametri è `[^/]+`, cioè tutto tranne la barra. Se un parametro deve accettare anche le barre, impostiamo l'espressione a `.+`: - -```php -// accetta https://example.com/a/b/c, il percorso sarà 'a/b/c' -$router->addRoute('', /* ... */); -``` - - -Sequenze facoltative --------------------- - -Nella maschera le parti facoltative si possono contrassegnare con le parentesi quadre. Qualsiasi parte della maschera può essere facoltativa e può contenere parametri: - -```php -$router->addRoute('[/]', /* ... */); - -// accetta i percorsi: -// /en/download => lang => en, name => download -// /download => lang => null, name => download -``` - -Quando un parametro fa parte di una sequenza facoltativa, diventa naturalmente facoltativo anch'esso. Se non ha un valore predefinito indicato, sarà null. - -Le parti facoltative possono trovarsi anche nel dominio: - -```php -$router->addRoute('//[.]example.com//', /* ... */); -``` - -Le sequenze si possono annidare e combinare a piacere: - -```php -$router->addRoute( - '[[-]/][/page-]', - 'Home:default', -); - -// accetta i percorsi: -// /en/hello -// /en-us/hello -// /hello -// /hello/page-12 -``` - -Nella generazione degli URL si preferisce la variante più breve, quindi tutto ciò che si può omettere viene omesso. Perciò, per esempio, la route `index[.html]` genera il percorso `/index`. Questo comportamento si può invertire mettendo un punto esclamativo dopo la parentesi quadra sinistra: - -```php -// accetta /hello e /hello.html, genera /hello -$router->addRoute('[.html]', /* ... */); - -// accetta /hello e /hello.html, genera /hello.html -$router->addRoute('[!.html]', /* ... */); -``` - -I parametri facoltativi (cioè i parametri con un valore predefinito) senza parentesi quadre si comportano in sostanza come se fossero racchiusi nel modo seguente: - -```php -$router->addRoute('//', /* ... */); - -// corrisponde a questo: -$router->addRoute('[/[/[]]]', /* ... */); -``` - -Se vogliamo influire sul comportamento della barra finale, in modo che per esempio venga generato `/home` invece di `/home/`, lo si ottiene così: - -```php -$router->addRoute('[[/[/]]]', /* ... */); -``` - - -Caratteri jolly ---------------- - -Nella maschera di un URL assoluto possiamo usare i caratteri jolly seguenti per evitare, per esempio, di dover scrivere nella maschera il dominio, che potrebbe differire tra l'ambiente di sviluppo e quello di produzione: - -- `%tld%` = dominio di primo livello, per esempio `com` o `org` -- `%sld%` = dominio di secondo livello, per esempio `example` -- `%domain%` = dominio senza sottodomini, per esempio `example.com` -- `%host%` = host intero, per esempio `www.example.com` -- `%basePath%` = percorso della directory radice - -```php -$router->addRoute('//www.%domain%/%basePath%//', /* ... */); -$router->addRoute('//www.%sld%.%tld%/%basePath%//', /* ... */); -``` - - -Notazione avanzata ------------------- - -La destinazione della route, di norma scritta nel formato `Presenter:azione`, si può scrivere anche con un array che definisce i singoli parametri e i loro valori predefiniti: - -```php -$router->addRoute('/[/]', [ - 'presenter' => 'Home', - 'action' => 'default', -]); -``` - -Per una specifica più dettagliata si può usare una forma ancora più estesa, in cui, oltre ai valori predefiniti, possiamo impostare altre proprietà del parametro, come un'espressione regolare di validazione (vedi il parametro `id`): - -```php -use Nette\Routing\Route; - -$router->addRoute('/[/]', [ - 'presenter' => [ - Route::Value => 'Home', - ], - 'action' => [ - Route::Value => 'default', - ], - 'id' => [ - Route::Pattern => '\d+', - ], -]); -``` - -È importante notare che, se i parametri definiti nell'array non sono elencati nella maschera del percorso, i loro valori non si possono cambiare, nemmeno con i parametri di query indicati dopo il punto interrogativo nell'URL. - -Questo è utile per i **parametri fissi**: dare a una pagina specifica un URL breve e facile da ricordare. Per esempio, per far sì che `/tos` apra sempre `Article:view` con `id: 123`: - -```php -$router->addRoute('tos', [ - 'presenter' => 'Article', - 'action' => 'view', - 'id' => 123, -]); -``` - - -Filtri e traduzioni -------------------- - -Il codice sorgente dell'applicazione lo scriviamo in inglese, ma se il sito deve avere URL in ceco, un routing semplice come: - -```php -$router->addRoute('/', 'Home:default'); -``` - -genererà URL inglesi, come `/product/123` o `/cart`. Se vogliamo che nell'URL i presenter e le azioni siano rappresentati da parole ceche (per esempio `/produkt/123` o `/kosik`), possiamo usare un dizionario di traduzione. Per scriverlo ci serve già la variante "più prolissa" del secondo parametro: - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterTable => [ - // stringa nell'URL => presenter - 'produkt' => 'Product', - 'kosik' => 'Cart', - 'katalog' => 'Catalog', - ], - ], - 'action' => [ - Route::Value => 'default', - Route::FilterTable => [ - 'seznam' => 'list', - ], - ], -]); -``` - -Più chiavi del dizionario di traduzione possono portare allo stesso presenter. Si creano così vari alias per esso. L'ultima chiave è considerata la variante canonica (cioè quella che comparirà nell'URL generato). - -La tabella di traduzione si può usare in questo modo per qualsiasi parametro. Se una traduzione non esiste, viene preso il valore originale. Possiamo cambiare questo comportamento aggiungendo `Route::FilterStrict => true`: la route rifiuterà allora l'URL se il valore non è nel dizionario. - -Oltre al dizionario di traduzione sotto forma di array, si possono usare funzioni di traduzione personalizzate. - -```php -use Nette\Routing\Route; - -$router->addRoute('//', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterIn => function (string $s): string { /* ... */ }, - Route::FilterOut => function (string $s): string { /* ... */ }, - ], - 'action' => 'default', - 'id' => null, -]); -``` - -La funzione `Route::FilterIn` converte tra il parametro presente nell'URL e la stringa che viene poi passata al presenter; la funzione `FilterOut` garantisce la conversione nella direzione opposta. - -I parametri `presenter`, `action` e `module` hanno già filtri predefiniti che convertono tra lo stile PascalCase o camelCase e il kebab-case usato negli URL. Il valore predefinito dei parametri si scrive nella forma in cui viene passato all'applicazione (PascalCase per presenter e module, camelCase per action), quindi per esempio, nel caso di un presenter, scriviamo ``, non ``. - - -Filtri generali ---------------- - -Oltre ai filtri destinati a parametri specifici, possiamo definire anche filtri generali, che ricevono un array associativo di tutti i parametri, che possono modificare a piacere e poi restituire. I filtri generali si definiscono sotto la chiave vuota. - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => 'Home', - 'action' => 'default', - '' => [ - Route::FilterIn => function (array $params): array { /* ... */ }, - Route::FilterOut => function (array $params): array { /* ... */ }, - ], -]); -``` - -I filtri generali offrono la possibilità di modificare il comportamento della route in assolutamente qualsiasi modo. Possiamo usarli, per esempio, per modificare parametri in base ad altri parametri. Per esempio per tradurre `` e `` in base al valore corrente del parametro ``. - -Se un parametro ha un proprio filtro definito ed esiste anche un filtro generale, il `FilterIn` personalizzato viene eseguito prima di quello generale e, al contrario, il `FilterOut` generale viene eseguito prima di quello personalizzato. Dentro il filtro generale i valori dei parametri `presenter` e `action` sono quindi scritti rispettivamente nello stile PascalCase e camelCase. - -Vedi [URL leggibili con gli slug |best-practices:pretty-urls] per un uso pratico di questi filtri: generare URL adatti alla SEO come `/article/123-how-to-bake-bread` senza modificare alcun template. - - -Flag OneWay ------------ - -Le route a senso unico servono a mantenere il funzionamento dei vecchi URL che l'applicazione non genera più, ma che accetta ancora. Le contrassegniamo con il flag `OneWay`: - -```php -// vecchio URL /product-info?id=123 -$router->addRoute('product-info', 'Product:detail', oneWay: true); -// nuovo URL /product/123 -$router->addRoute('product/', 'Product:detail'); -``` - -All'accesso al vecchio URL, il presenter reindirizza automaticamente al nuovo, così i motori di ricerca non indicizzeranno queste pagine due volte (vedi [#SEO e canonizzazione]). - - -Routing dinamico con le callback --------------------------------- - -Il routing dinamico con le callback vi permette di assegnare direttamente alle route delle funzioni (callback), che vengono eseguite quando si visita il percorso indicato. Questa funzionalità flessibile vi permette di creare rapidamente ed efficacemente vari endpoint per la vostra applicazione: - -```php -$router->addRoute('test', function () { - echo 'Vi trovate all\'indirizzo /test'; -}); -``` - -Nella maschera potete definire anche parametri, che vengono passati automaticamente alla vostra callback: - -```php -$router->addRoute('', function (string $lang) { - echo match ($lang) { - 'cs' => 'Benvenuti nella versione ceca del nostro sito!', - 'en' => 'Benvenuti nella versione inglese del nostro sito!', - }; -}); -``` - -Oltre ai parametri della maschera, la callback può ricevere anche servizi dal container DI. Vengono passati in base al tipo del parametro. Inoltre il parametro `$presenter` riceve un'istanza di [MicroPresenter |api:NetteModule\MicroPresenter], che elabora la route: - -```php -$router->addRoute('', function (string $lang, Nette\Http\Request $httpRequest, NetteModule\MicroPresenter $presenter) { - // ... -}); -``` - - -Moduli ------- - -Se abbiamo più route che appartengono a un [modulo |directory-structure#Presenter e template] comune, usiamo `withModule()`. Il modulo indicato viene anteposto automaticamente al presenter di ogni route del gruppo e sparisce del tutto dall'URL: - -```php -$router = new RouteList; -$router->withModule('Forum') // le route seguenti fanno parte del modulo Forum - ->addRoute('rss', 'Feed:rss') // il presenter sarà Forum:Feed - ->addRoute('/') - - ->withModule('Admin') // le route seguenti fanno parte del modulo Forum:Admin - ->addRoute('sign:in', 'Sign:in'); -``` - -Un'alternativa è il parametro `module`, che allo stesso modo imposta un modulo fisso e lo tiene fuori dall'URL: - -```php -// l'URL manage/dashboard/default è mappato sul presenter Admin:Dashboard -$router->addRoute('manage//', [ - 'module' => 'Admin', -]); -``` - -Ogni nome di presenter è completo solo insieme al proprio modulo, per esempio `Front:Admin:ProductList`. Ogni volta che un nome completo del genere finisce in un parametro dell'URL, il router lo codifica con due semplici regole: ogni due punti `:` (il separatore dei moduli) diventa un **punto** e ogni confine di parola in un nome PascalCase diventa un **trattino**. Così `Front:Admin:ProductList` compare nell'URL come `front.admin.product-list` e viene decodificato allo stesso modo. Ecco perché un'applicazione modulare, senza nessuno degli strumenti visti sopra, produce URL pieni di punti. - -Sia `withModule()` sia il parametro `module` lo evitano proprio perché tolgono dal nome del presenter un prefisso di modulo noto prima che arrivi all'URL: poiché il modulo è una costante, non ha bisogno di essere codificato affatto. - -A volte vogliamo che il modulo stesso vari e compaia nell'URL, quindi ricorriamo direttamente a `` nella maschera. Attenzione a un dettaglio essenziale: **`` cattura l'intero percorso del modulo**, cioè tutto ciò che precede l'ultimo due punti del nome del presenter. Per il presenter `Shop:Admin:Product` questo significa il modulo `Shop:Admin` e il presenter `Product` e, poiché i due punti diventano punti, otteniamo: - - -Sottodomini ------------ - -Le collezioni di route si possono suddividere per sottodomini: - -```php -$router = new RouteList; -$router->withDomain('example.com') - ->addRoute('rss', 'Feed:rss') - ->addRoute('/'); -``` - -Nel nome del dominio si possono usare anche i [caratteri jolly |#Caratteri jolly]: - -```php -$router = new RouteList; -$router->withDomain('example.%tld%') - // ... -``` - - -Prefisso del percorso ---------------------- - -Le collezioni di route si possono suddividere in base al percorso nell'URL: - -```php -$router = new RouteList; -$router->withPath('eshop') - ->addRoute('rss', 'Feed:rss') // corrisponde all'URL /eshop/rss - ->addRoute('/'); // corrisponde all'URL /eshop// -``` - - -Combinazioni ------------- - -I raggruppamenti visti sopra si possono combinare tra loro: - -```php -$router = (new RouteList) - ->withDomain('admin.example.com') - ->withModule('Admin') - ->addRoute(/* ... */) - ->addRoute(/* ... */) - ->end() - ->withModule('Images') - ->addRoute(/* ... */) - ->end() - ->end() - ->withDomain('example.com') - ->withPath('export') - ->addRoute(/* ... */) - // ... -``` - - -Parametri di query ------------------- - -Le maschere possono contenere anche parametri di query (i parametri che seguono il punto interrogativo nell'URL). Per essi non si può definire un'espressione di validazione, ma si può cambiare il nome con cui vengono passati al presenter: - -```php -// vogliamo usare il parametro di query 'cat' con il nome 'categoryId' nell'applicazione -$router->addRoute('product ? id= & cat=', /* ... */); -``` - - -Parametri foo -------------- - -Ora andiamo più a fondo. I parametri foo sono in sostanza parametri senza nome, che permettono di far corrispondere un'espressione regolare. Un esempio è una route che accetta `/index`, `/index.html`, `/index.htm` e `/index.php`: - -```php -$router->addRoute('index', /* ... */); -``` - -È anche possibile definire esplicitamente la stringa che verrà usata nella generazione dell'URL. La stringa va collocata subito dopo il punto interrogativo. La route seguente è simile alla precedente, ma genera `/index.html` invece di `/index`, perché come valore di generazione è impostata la stringa `.html`: - -```php -$router->addRoute('index', /* ... */); -``` - - -Integrazione -============ - -Per integrare il router creato nell'applicazione dobbiamo comunicarlo al container DI. Il modo più semplice è preparare una factory che crei l'oggetto router e dire al container, nella configurazione, di usarla. Supponiamo di scrivere a questo scopo il metodo `App\Core\RouterFactory::createRouter()`: - -```php -namespace App\Core; - -use Nette\Application\Routers\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute(/* ... */); - return $router; - } -} -``` - -Scriviamo poi nella [configurazione |dependency-injection:services]: - -```neon -services: - - App\Core\RouterFactory::createRouter -``` - -Le eventuali dipendenze, per esempio da un database, vengono passate al metodo factory come suoi parametri tramite l'[autowiring|dependency-injection:autowiring]: - -```php -public static function createRouter(Nette\Database\Connection $db): RouteList -{ - // ... -} -``` - - -SimpleRouter -============ - -Un router molto più semplice della collezione di route è [SimpleRouter |api:Nette\Application\Routers\SimpleRouter]. Lo usiamo quando non abbiamo esigenze particolari sul formato degli URL, se `mod_rewrite` (o le sue alternative) non è disponibile, oppure se non vogliamo ancora occuparci degli URL leggibili. - -Genera indirizzi più o meno in questa forma: - -``` -http://example.com/?presenter=Product&action=detail&id=123 -``` - -Il parametro del costruttore di `SimpleRouter` è il presenter e l'azione predefiniti, cioè l'azione da eseguire se apriamo per esempio `http://example.com/` senza altri parametri. - -```php -// il presenter predefinito sarà 'Home' e l'azione 'default' -$router = new Nette\Application\Routers\SimpleRouter('Home:default'); -``` - -Consigliamo di definire SimpleRouter direttamente nella [configurazione |dependency-injection:services]: - -```neon -services: - - Nette\Application\Routers\SimpleRouter('Home:default') -``` - - -SEO e canonizzazione -==================== - -Il framework contribuisce alla SEO (Search Engine Optimization) impedendo l'esistenza di contenuto duplicato su URL diversi. Se più indirizzi portano a una certa destinazione, per esempio `/index` e `/index.html`, il framework designa il primo come principale (canonico) e vi reindirizza gli altri con il codice HTTP 301. Grazie a questo i motori di ricerca non indicizzano le pagine due volte e non ne diluiscono il page rank. - -Questo processo si chiama canonizzazione. L'URL canonico è quello generato dal router, cioè dalla prima route corrispondente della collezione priva del flag OneWay. Nella collezione elenchiamo quindi **prima le route principali**. - -La canonizzazione viene eseguita dal presenter, maggiori informazioni nel capitolo [canonizzazione |presenters#Canonizzazione]. - - -HTTPS -===== - -Per usare il protocollo HTTPS è necessario attivarlo sull'hosting e configurare correttamente il server. - -Il redirect dell'intero sito verso HTTPS va impostato a livello di server, per esempio con il file `.htaccess` nella directory radice della nostra applicazione, con il codice HTTP 301. Le impostazioni possono variare a seconda dell'hosting e hanno più o meno questo aspetto: - -``` - - RewriteEngine On - ... - RewriteCond %{HTTPS} off - RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] - ... - -``` - -Il router genera gli URL con lo stesso protocollo con cui è stata caricata la pagina, quindi non serve impostare altro. - -Se però, in via eccezionale, abbiamo bisogno che route diverse girino con protocolli diversi, lo indichiamo nella maschera della route: - -```php -// genererà un indirizzo HTTP -$router->addRoute('http://%host%//', /* ... */); - -// genererà un indirizzo HTTPS -$router->addRoute('https://%host%//', /* ... */); -``` - - -Debugging del router -==================== - -Il pannello del routing mostrato nella [Tracy Bar |tracy:] è un aiuto utile: mostra l'elenco delle route e anche i parametri che il router ha ricavato dall'URL. - -La barra verde con il simbolo ✓ rappresenta la route che ha elaborato l'URL corrente; il colore blu e il simbolo ≈ indicano le route che avrebbero elaborato anch'esse l'URL, se quella verde non le avesse precedute. Vediamo inoltre il presenter e l'azione correnti. - -[* routing-debugger.webp *] - -Allo stesso tempo, se avviene un redirect inatteso a causa della [canonizzazione |#SEO e canonizzazione], è utile guardare nel pannello la barra *redirect*, dove potete scoprire come il router aveva interpretato in origine l'URL e perché ha reindirizzato. - -.[note] -Durante il debugging del router consigliamo di aprire gli strumenti per sviluppatori del browser (Ctrl+Shift+I o Cmd+Option+I) e di disattivare la cache nel pannello Network, così che i redirect non vi vengano salvati. - - -Prestazioni -=========== - -Il numero di route influisce sulla velocità del router. Il loro numero non dovrebbe assolutamente superare qualche decina. Se il vostro sito ha una struttura di URL troppo complicata, potete scrivere un [router personalizzato |#Router personalizzato]. - -Se il router non ha dipendenze, per esempio da un database, e la sua factory non accetta argomenti, possiamo serializzarne la forma compilata direttamente nel container DI e velocizzare così leggermente l'applicazione. - -```neon -routing: - cache: true -``` - - -Router personalizzato -===================== - -Le righe seguenti sono destinate agli utenti molto avanzati. Potete creare un vostro router e integrarlo naturalmente nella collezione di route. Il router è un'implementazione dell'interfaccia [api:Nette\Routing\Router], con due metodi: - -```php -use Nette\Http\IRequest as HttpRequest; -use Nette\Http\UrlScript; - -class MyRouter implements Nette\Routing\Router -{ - public function match(HttpRequest $httpRequest): ?array - { - // ... - } - - public function constructUrl(array $params, UrlScript $refUrl): ?string - { - // ... - } -} -``` - -Il metodo `match` elabora la richiesta corrente [$httpRequest |http:request], dalla quale si possono ottenere non solo l'URL ma anche gli header e altro, trasformandola in un array che contiene il nome del presenter e i suoi parametri. Se non riesce a elaborare la richiesta, restituisce null. Elaborando la richiesta dobbiamo restituire almeno il presenter; l'azione è facoltativa e, se non indicata, vale `default`. Il nome del presenter è completo e comprende gli eventuali moduli: - -```php -[ - 'presenter' => 'Front:Home', - 'action' => 'default', -] -``` - -Il metodo `constructUrl`, al contrario, costruisce l'URL assoluto risultante a partire dall'array di parametri. Può usare le informazioni del parametro [`$refUrl`|api:Nette\Http\UrlScript], che è l'URL corrente. - -Lo aggiungete alla collezione di route con `add()`: - -```php -$router = new Nette\Application\Routers\RouteList; -$router->add($myRouter); -$router->addRoute(/* ... */); -// ... -``` - - -Uso autonomo -============ - -Per uso autonomo intendiamo sfruttare le capacità del router in un'applicazione che non usa Nette Application e i presenter. Vale per essa quasi tutto ciò che abbiamo mostrato in questo capitolo, con queste differenze: - -- per le collezioni di route usiamo la classe [api:Nette\Routing\RouteList] -- come router semplice, la classe [api:Nette\Routing\SimpleRouter] -- poiché la coppia `Presenter:azione` non esiste, usiamo la [notazione avanzata |#Notazione avanzata] - -Creiamo quindi di nuovo un metodo che ci componga il router, per esempio: - -```php -namespace App\Core; - -use Nette\Routing\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute('rss.xml', [ - 'controller' => 'RssFeedController', - ]); - $router->addRoute('article/', [ - 'controller' => 'ArticleController', - ]); - // ... - return $router; - } -} -``` - -Se usate un container DI, cosa che consigliamo, aggiungete di nuovo il metodo alla configurazione e ottenete poi dal container il router insieme alla richiesta HTTP: - -```php -$router = $container->getByType(Nette\Routing\Router::class); -$httpRequest = $container->getByType(Nette\Http\IRequest::class); -``` - -Oppure create direttamente gli oggetti: - -```php -$router = App\Core\RouterFactory::createRouter(); -$httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); -``` - -Ora non resta che lasciare al router il suo lavoro: - -```php -$params = $router->match($httpRequest); -if ($params === null) { - // nessuna route corrispondente trovata, invia un errore 404 - exit; -} - -// elabora i parametri ottenuti -$controller = $params['controller']; -// ... -``` - -E, al contrario, usate il router per costruire un link: - -```php -$params = ['controller' => 'ArticleController', 'id' => 123]; -$url = $router->constructUrl($params, $httpRequest->getUrl()); -``` - - -{{composer: nette/routing}} -{{repo: nette/routing}} -{{api: https://api.nette.org/routing/}} diff --git a/application/it/templates.texy b/application/it/templates.texy deleted file mode 100644 index 55b7110349..0000000000 --- a/application/it/templates.texy +++ /dev/null @@ -1,419 +0,0 @@ -Template -******** - -.[perex] -Nette usa il sistema di template [Latte |latte:]. Latte viene usato perché è il sistema di template più sicuro per PHP e allo stesso tempo il più intuitivo. Non dovete imparare molto di nuovo: bastano la conoscenza di PHP e qualche tag. - -È consuetudine che una pagina sia composta dal template del layout più il template dell'azione specifica. Ecco che aspetto potrebbe avere un template di layout; notate i blocchi `{block}` e il tag `{include}`: - -```latte - - - - {block title}My App{/block} - - -
    ...
    - {include content} -
    ...
    - - -``` - -E questo sarebbe il template dell'azione: - -```latte -{block title}Homepage{/block} - -{block content} -

    Homepage

    -... -{/block} -``` - -Definisce il blocco `content`, che viene inserito al posto di `{include content}` nel layout, e ridefinisce inoltre il blocco `title`, che sovrascrive `{block title}` del layout. Provate a immaginare il risultato. - - -Ricerca dei template --------------------- - -Nei presenter non dovete indicare quale template va disegnato: il framework ne deduce automaticamente il percorso, risparmiandovi di scriverlo. - -Se usate una struttura di directory in cui ogni presenter ha la propria directory, basta collocare il template in questa directory con il nome dell'azione (cioè della vista). Per esempio, per l'azione `default` usate il template `default.latte`: - -/--pre -app/ -└── Presentation/ - └── Home/ - ├── HomePresenter.php - └── default.latte -\-- - -Se usate una struttura in cui i presenter stanno insieme in un'unica directory e i template in una cartella `templates`, salvatelo o nel file `..latte` oppure in `/.latte`: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── Home/ - │ └── default.latte ← 1ª variante - └── Home.default.latte ← 2ª variante -\-- - -La directory `templates` si può collocare anche un livello più in alto, cioè allo stesso livello della directory con le classi dei presenter. - -Se il template non viene trovato, il presenter risponde con un [errore 404 - pagina non trovata |presenters#Errore 404 e simili]. - -Potete cambiare la vista con `$this->setView('otherView')`. È anche possibile indicare direttamente il file del template con `$this->template->setFile('/path/to/template.latte')`. - -.[note] -I file in cui vengono cercati i template si possono cambiare sovrascrivendo il metodo [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], che restituisce un array dei possibili nomi di file. - - -Ricerca del template di layout ------------------------------- - -Nette cerca automaticamente anche il file del layout. - -Se usate una struttura di directory in cui ogni presenter ha la propria directory, collocate il layout o nella cartella del presenter, se è specifico solo di quello, oppure un livello più in alto, se è comune a più presenter: - -/--pre -app/ -└── Presentation/ - ├── @layout.latte ← layout comune - └── Home/ - ├── @layout.latte ← solo per il presenter Home - ├── HomePresenter.php - └── default.latte -\-- - -Se usate una struttura in cui i presenter sono raggruppati in un'unica directory e i template stanno in una cartella `templates`, il layout sarà atteso in queste posizioni: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── @layout.latte ← layout comune - ├── Home/ - │ └── @layout.latte ← solo per Home, 1ª variante - └── Home.@layout.latte ← solo per Home, 2ª variante -\-- - -Se il presenter si trova in un modulo, la ricerca prosegue anche verso i livelli superiori di directory, secondo l'annidamento dei moduli. - -Il nome del layout si può cambiare con `$this->setLayout('layoutAdmin')`, e allora sarà atteso nel file `@layoutAdmin.latte`. Potete anche indicare direttamente il file del template di layout con `$this->setLayout('/path/to/template.latte')`. - -Con `$this->setLayout(false)` oppure con il tag `{layout none}` dentro il template si disattiva la ricerca del layout. - -.[note] -I file in cui vengono cercati i template di layout si possono cambiare sovrascrivendo il metodo [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], che restituisce un array dei possibili nomi di file. - - -Variabili del template ----------------------- - -Le variabili si passano ai template scrivendole in `$this->template`. Diventano poi disponibili nel template come variabili locali: - -```php -$this->template->article = $this->articles->getById($id); -``` - -Per passare automaticamente al template il valore di una proprietà come variabile, contrassegnatela con l'attributo `#[TemplateVariable]` e con visibilità pubblica: .{data-version:3.2.9} - -```php -use Nette\Application\Attributes\TemplateVariable; - -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - #[TemplateVariable] - public string $siteName = 'My blog'; -} -``` - -Se passate al template una variabile con lo stesso nome, `#[TemplateVariable]` non la sovrascriverà. - - -Variabili predefinite ---------------------- - -I presenter e i componenti passano automaticamente ai template alcune variabili utili: - -- `$basePath` è il percorso URL assoluto della directory radice (per esempio `/eshop`) -- `$baseUrl` è l'URL assoluto della directory radice (per esempio `http://localhost/eshop`) -- `$user` è un oggetto che [rappresenta l'utente |security:authentication] -- `$presenter` è il presenter corrente -- `$control` è il componente o il presenter corrente -- `$flashes` è un array dei [messaggi |presenters#Messaggi flash] inviati dalla funzione `flashMessage()` - -Se usate una classe di template personalizzata, queste variabili vengono passate se create per esse una proprietà. - - -Template con tipi sicuri ------------------------- - -Sviluppando applicazioni solide, è utile definire esplicitamente quali variabili il template si aspetta e di che tipo sono. Questo offre il controllo dei tipi in PHP, suggerimenti intelligenti nell'IDE e permette all'analisi statica di individuare gli errori. - -Come si definisce un elenco del genere? Semplicemente come una classe con proprietà che rappresentano le variabili del template. Chiamatela come il presenter, aggiungendo alla fine `Template`: - -```php -/** - * @property-read ArticleTemplate $template - */ -class ArticlePresenter extends Nette\Application\UI\Presenter -{ -} - -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - public Model\Article $article; - public Nette\Security\User $user; - - // e altre variabili -} -``` - -L'oggetto `$this->template` nel presenter sarà ora un'istanza della classe `ArticleTemplate`. PHP controllerà quindi i tipi dichiarati durante la scrittura. - -Nette sceglie automaticamente la classe del template. Cerca prima una classe chiamata `Template`, per esempio `ArticleEditTemplate` per l'azione `edit`, e solo se non esiste ripiega su `Template`. - -L'annotazione `@property-read` è destinata all'IDE e all'analisi statica e abilita il completamento del codice, vedi "PhpStorm e il completamento del codice per $this⁠-⁠>⁠template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template. - -[* phpstorm-completion.webp *] - -Potete usare il completamento del codice anche direttamente nei template. Basta installare il plugin Latte per PhpStorm e indicare all'inizio del template il nome della classe dei parametri, maggiori informazioni nel capitolo [Latte: sistema di tipi |latte:type-system]: - -```latte -{templateType App\Presentation\Article\ArticleTemplate} -... -``` - -Lo stesso vale per i componenti. Basta seguire la convenzione di denominazione e creare una classe dei parametri `FifteenTemplate` per un componente come `FifteenControl`. - -Se vi serve usare una classe dei parametri diversa, usate il metodo `createTemplate()`: - -```php -public function renderDefault(): void -{ - $template = $this->createTemplate(SpecialTemplate::class); - $template->foo = 123; - // ... - $this->sendTemplate($template); -} -``` - -.{data-version:3.3.0} -Se vi serve influire su come il template viene completato prima del rendering, per esempio per aggiungere variabili condivise da tutte le azioni, potete sovrascrivere nel presenter il metodo `completeTemplate()`. Viene chiamato subito prima che il template venga disegnato: - -```php -protected function completeTemplate(Nette\Application\UI\Template $template): void -{ - parent::completeTemplate($template); - $template->siteName = 'My blog'; -} -``` - - -Creare i link -------------- - -Nel template i link verso altri presenter e azioni si creano così: - -```latte -dettaglio del prodotto -``` - -L'attributo `n:href` è molto comodo per i tag HTML ``. Se vogliamo stampare il link altrove, per esempio nel testo, usiamo `{link}`: - -```latte -L'URL è: {link Home:default} -``` - -Maggiori informazioni si trovano nel capitolo [Creazione di link URL|creating-links]. - - -Filtri, tag e altro personalizzati ----------------------------------- - -Il sistema di template Latte si può estendere con filtri, funzioni, tag e altri elementi personalizzati. Ci sono tre approcci disponibili, che vanno dalle soluzioni rapide ad hoc ai modelli architetturali per intere applicazioni. - -**Ad hoc nei metodi del presenter** - -L'approccio più rapido è aggiungere filtri o funzioni direttamente nel codice del presenter o del componente. Nei presenter si prestano bene i metodi `beforeRender()` o `render()`: - -```php -protected function beforeRender(): void -{ - // aggiunta di un filtro - $this->template->addFilter('money', fn($val) => '$' . number_format($val, 2)); - - // aggiunta di una funzione - $this->template->addFunction('isWeekend', fn($date) => $date->format('N') >= 6); -} -``` - -Nel template: - -```latte -

    Prezzo: {$price|money}

    - -{if isWeekend($now)} ... {/if} -``` - -Per una logica più complessa potete configurare direttamente l'oggetto `Latte\Engine`: - -```php -protected function beforeRender(): void -{ - $latte = $this->template->getLatte(); - $latte->setFeature(Latte\Feature::MigrationWarnings); -} -``` - -**Con gli attributi** - -Un approccio più elegante è definire filtri e funzioni come metodi direttamente nella [classe dei parametri del template|#Template con tipi sicuri] del presenter o del componente, contrassegnandoli con degli attributi: - -```php -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - #[Latte\Attributes\TemplateFilter] - public function money(float $val): string - { - return '$' . number_format($val, 2); - } - - #[Latte\Attributes\TemplateFunction] - public function isWeekend(DateTimeInterface $date): bool - { - return $date->format('N') >= 6; - } -} -``` - -Latte individua e registra automaticamente i metodi contrassegnati con questi attributi. Il nome del filtro o della funzione nei template coincide con il nome del metodo. Questi metodi devono essere pubblici. - -**Globalmente, con le estensioni** - -Gli approcci precedenti si prestano a filtri e funzioni necessari solo in determinati presenter o componenti, non nell'intera applicazione. Per tutta l'applicazione la soluzione migliore è creare un'[estensione |latte:extending-latte#Estensione di Latte]. Questa classe centralizza tutte le estensioni di Latte del vostro progetto. Un breve esempio: - -```php -namespace App\Presentation\Accessory; - -final class LatteExtension extends Latte\Extension -{ - public function __construct( - private App\Model\Facade $facade, - private Nette\Security\User $user, - // ... - ) { - } - - public function getFilters(): array - { - return [ - 'timeAgoInWords' => $this->filterTimeAgoInWords(...), - 'money' => $this->filterMoney(...), - // ... - ]; - } - - public function getFunctions(): array - { - return [ - 'canEditArticle' => - fn($article) => $this->facade->canEditArticle($article, $this->user->getId()), - // ... - ]; - } - - private function filterTimeAgoInWords(DateTimeInterface $time): string - { - // ... - } - - // ... -} -``` - -Registrate l'estensione tramite la [configurazione |configuration#Template Latte]: - -```neon -latte: - extensions: - - App\Presentation\Accessory\LatteExtension -``` - -Le estensioni offrono diversi vantaggi: il supporto della dependency injection, l'accesso allo strato del modello della vostra applicazione e la gestione centralizzata di tutte le estensioni. Supportano inoltre tag personalizzati, provider, compiler pass e altro ancora. - - -Configurare tutti i template ----------------------------- - -Il servizio `TemplateFactory`, che crea tutti i template, offre un array pubblico di callback `$onCreate`. Vengono chiamate ogni volta che viene creato un template qualsiasi, così potete impostare filtri, funzioni o variabili per tutti i template dell'applicazione da un unico punto. Ogni callback riceve il template appena creato. Fatevi [iniettare |dependency-injection:passing-dependencies] il servizio `TemplateFactory` e registrate le callback, per esempio all'avvio dell'applicazione: - -```php -$templateFactory->onCreate[] = function (Nette\Bridges\ApplicationLatte\Template $template): void { - $template->addFilter('money', fn($val) => '$' . number_format($val, 2)); -}; -``` - - -Traduzione ----------- - -Se programmate un'applicazione multilingue, probabilmente avrete bisogno di stampare nel template alcuni testi in lingue diverse. Nette Framework definisce a questo scopo l'interfaccia di traduzione [api:Nette\Localization\Translator], che ha un unico metodo `translate()`. Accetta il messaggio `$message`, di norma una stringa, e qualsiasi altro parametro. Il compito è restituire la stringa tradotta. Nette non ha un'implementazione predefinita; potete scegliere tra diverse soluzioni già pronte disponibili su [Componette |https://componette.org/search/localization], secondo le vostre esigenze. La loro documentazione spiega come configurare il traduttore. - -Ai template si può impostare un traduttore, che ci facciamo [passare |dependency-injection:passing-dependencies], con il metodo `setTranslator()`: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator); -} -``` - -In alternativa il traduttore si può impostare tramite la [configurazione |configuration#Template Latte]: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -Il traduttore si può poi usare, per esempio, come filtro `|translate`, compresi i parametri aggiuntivi che vengono passati al metodo `translate()` (vedi `foo, bar`): - -```latte -
    {='Carrello'|translate} -{$item|translate} -{$item|translate, foo, bar} -``` - -Oppure come tag con il trattino basso: - -```latte -{_'Carrello'} -{_$item} -{_$item, foo, bar} -``` - -Per tradurre una porzione di template esiste il tag di tipo pari `{translate}` (da Latte 2.11, prima si usava il tag `{_}`): - -```latte -{translate}Ordine{/translate} -{translate foo, bar}Ordine{/translate} -``` - -Il traduttore viene normalmente chiamato in fase di esecuzione, durante il rendering del template. Latte versione 3 è però in grado di tradurre tutti i testi statici già durante la compilazione del template. Questo fa risparmiare prestazioni, perché ogni stringa viene tradotta una sola volta e la traduzione risultante viene scritta nella forma compilata. Nella directory della cache nascono così più versioni compilate del template, una per ogni lingua. Per ottenerlo basta indicare la lingua come secondo parametro: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator, $lang); -} -``` - -Per testo statico si intende, per esempio, `{_'hello'}` oppure `{translate}hello{/translate}`. I testi non statici, come `{_$foo}`, continueranno a essere tradotti in fase di esecuzione. diff --git a/application/it/upgrading.texy b/application/it/upgrading.texy deleted file mode 100644 index 7cfec2644a..0000000000 --- a/application/it/upgrading.texy +++ /dev/null @@ -1,47 +0,0 @@ -Aggiornamento -************* - - -Aggiornamento alla versione 3.0 -=============================== - -Nette 3.0 aggiunge le dichiarazioni di tipo ai parametri e ai valori di ritorno dei metodi. Se sovrascrivete un metodo del genere in una classe che eredita da Nette (per esempio in un presenter o in un componente), dovete aggiungere le stesse dichiarazioni di tipo, altrimenti PHP emette l'errore "Declaration must be compatible". - -L'interfaccia `Nette\Application\IRouter` è cambiata. Il metodo `match()` ora restituisce, e `constructUrl()` accetta, un array di parametri invece di un oggetto `Nette\Application\Request`. - -Nette controlla ora che ogni segnale venga inviato dalla stessa origine (cioè dallo stesso dominio e sottodominio). Questa same-origin policy è un meccanismo di sicurezza essenziale, che aiuta a ridurre i possibili vettori di attacco. Se volete permettere altre origini, aggiungete al metodo handler l'annotazione `@crossOrigin`: - -```php -/** - * @crossOrigin - */ -public function handleXy(): void -{ -} -``` - -Lo stesso vale per l'invio dei form. Se volete permetterne l'invio da altre origini, procedete così: - -```php -$form = new Nette\Application\UI\Form; -$form->allowCrossOrigin(); -``` - -Il costruttore di `Nette\ComponentModel\Component` non veniva usato da anni ed è stato rimosso nella versione 3.0. È una rottura di compatibilità: se in un componente o in un presenter che eredita da `Nette\Application\UI\Presenter` chiamate il costruttore genitore, dovete rimuovere la chiamata. - - -Aggiornamento alla versione 2.4 -=============================== - -- `Route` e `SimpleRouter` generano ora lo stesso schema HTTP/HTTPS con cui si è acceduto al sito. Una route che richiede un protocollo specifico si può definire con lo schema, per esempio `Route('http://domain.cz/')`. -- Per i parametri di tipo bool dei metodi render/action (cioè con valore predefinito true o false) e per i parametri persistenti si distinguono ora `false` e `null`. Se il parametro non è presente nell'URL, il suo valore è ora `null` (prima era `false`). -- La classe restituita da `Presenter::getReflection()` non è più discendente di `Nette\Reflection\ClassType` e `getReflection()->getMethod()` non è più discendente di `Nette\Reflection\Method`. -- Il flag `SECURED` e `Route::$defaultFlags` sono deprecati. - - -Aggiornamento alla versione 2.3 -=============================== - -- le route e i nomi dei presenter fanno **distinzione tra maiuscole e minuscole**. Nette vi avvisa se usate le maiuscole sbagliate nel nome di un presenter; per motivi di prestazioni la maschera della Route non viene controllata, quindi verificatela manualmente. -- `Route::addStyle()` e `Route::setStyleProperty()` sono deprecati ed emettono ora `E_USER_DEPRECATED`. -- l'estensione dei template `.phtml` e la vecchia sintassi dei link non sono più supportate. diff --git a/application/ja/@home.texy b/application/ja/@home.texy deleted file mode 100644 index bf788071fd..0000000000 --- a/application/ja/@home.texy +++ /dev/null @@ -1,85 +0,0 @@ -Nette Application -***************** - -.[perex] -Nette Application は Nette フレームワークの中核で、現代的なウェブアプリケーションを作るための強力な道具を提供します。開発を大きく楽にし、コードの安全性と保守性を高める、他にはない機能が数多く揃っています。 - - -インストール ------- - -ライブラリは [Composer|best-practices:composer]でダウンロードしてインストールします。 - -```shell -composer require nette/application -``` - - -なぜ Nette Application を選ぶのか --------------------------- - -Nette は常にウェブ技術の先駆けであり続けてきました。 - -**双方向のルーター:** Nette には双方向性という点で他にない高度なルーティングのしくみがあります。URL をアプリケーションのアクションに変換するだけでなく、逆に URL を生成することもできます。つまり、 -- アプリケーション全体の URL の構造を、テンプレートを直さずにいつでも変えられます -- URL が自動的に正規化され、SEO が良くなります -- ルーティングが 1 か所で定義され、アノテーションに散らばりません - -**コンポーネントとシグナル:** Delphi と React.js に着想を得た組み込みのコンポーネントのしくみは、PHP のフレームワークの中でも他に例がありません。 -- 再利用できる UI 要素を作れます -- 階層的なコンポーネントの組み立てに対応しています -- シグナルを使った優雅な AJAX リクエストの処理を提供します -- [Componette](https://componette.org)に既製のコンポーネントの豊富なライブラリがあります - -**AJAX とスニペット:** Nette は 2009 年に、AJAX を扱う画期的な方法を導入しました。Ruby on Rails の Hotwire や Symfony UX Turbo のような似た解よりずっと前のことです。 -- スニペットを使うと、JavaScript を書かずにページの一部だけを更新できます -- コンポーネントのしくみと自動的に統合されます -- ページの部分を賢く無効化します -- データの転送は最小限です - -**直感的な [Latte|latte:]テンプレート:** 高度な機能を備えた、PHP で最も安全なテンプレートシステムです。 -- コンテキストに応じたエスケープによる自動的な XSS 対策 -- 独自のフィルタ、関数、タグで拡張できます -- テンプレートの継承と AJAX 用のスニペット -- 型システムを備えた優れた PHP 8.x サポート - -**Dependency Injection:** Nette は依存性注入を全面的に活用します。 -- 依存関係の自動的な受け渡し(オートワイヤリング) -- 分かりやすい NEON 形式による設定 -- コンポーネントのファクトリのサポート - - -主な利点 ----- - -- **安全性**: XSS、CSRF などの[脆弱性|nette:vulnerability-protection]に対する自動的な保護 -- **生産性**: 賢い設計のおかげで、書く量は少なく機能は多く -- **デバッグ**: ルーティングのパネルを備えた [Tracy デバッガ|tracy:] -- **性能**: 賢いキャッシュ、コンポーネントの遅延読み込み -- **柔軟さ**: アプリケーションが完成したあとでも URL を簡単に変えられます -- **コンポーネント**: 再利用できる UI 要素の他にないしくみ -- **現代的**: PHP 8.3+ と型システムの完全なサポート - - -はじめに ----- - -1. [アプリケーションはどう動くのか |how-it-works] - 基本のアーキテクチャの理解 -2. [プレゼンター |presenters] - プレゼンターとアクションの扱い -3. [テンプレート |templates] - Latte でのテンプレートの作成 -4. [ルーティング |routing] - URL アドレスの設定 -5. [インタラクティブなコンポーネント |components] - コンポーネントのしくみの利用 - - -PHP の互換性 --------- - -| バージョン | 対応する PHP -|-----------------------|------------------- -| Nette Application 3.3 | PHP 8.3 - 8.5 -| Nette Application 3.2 | PHP 8.1 - 8.5 -| 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/ja/@left-menu.texy b/application/ja/@left-menu.texy deleted file mode 100644 index 54cd9501d3..0000000000 --- a/application/ja/@left-menu.texy +++ /dev/null @@ -1,24 +0,0 @@ -Nette Application -***************** -- [概要 |@home] -- [アプリケーションはどう動くのか |how-it-works] -- [ブートストラップ|bootstrapping] -- [プレゼンター|presenters] -- [テンプレート|templates] -- [ディレクトリ構成 |directory-structure] -- [ルーティング|routing] -- [URL リンクの作成 |creating-links] -- [インタラクティブなコンポーネント |components] -- [AJAX とスニペット |ajax] -- [Multiplier |multiplier] -- [設定|configuration] -- [アップグレード|upgrading] - - -関連情報 -**** -- [なぜ Nette を使うのか|www:10-reasons-why-nette] -- [インストール |nette:installation] -- [最初のアプリケーションを作ろう |quickstart:] -- [ベストプラクティス |best-practices:] -- [トラブルシューティング |nette:troubleshooting] diff --git a/application/ja/@meta.texy b/application/ja/@meta.texy deleted file mode 100644 index 43b85f3cac..0000000000 --- a/application/ja/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette ドキュメント}} diff --git a/application/ja/ajax.texy b/application/ja/ajax.texy deleted file mode 100644 index 41c5fba15d..0000000000 --- a/application/ja/ajax.texy +++ /dev/null @@ -1,265 +0,0 @@ -AJAX とスニペット -*********** - -
    - -機能がサーバーとブラウザに分かれることの多い現代のウェブアプリケーションにおいて、AJAX は欠かせないつなぎ役です。この領域で Nette Framework は何を提供するのでしょうか。 -- テンプレートの一部、いわゆるスニペットの送信 -- PHP と JavaScript のあいだの変数の受け渡し -- AJAX リクエストをデバッグするための道具 - -
    - - -AJAX リクエスト -========== - -AJAX のリクエストは、本質的には通常の HTTP リクエストと変わりません。特定のパラメータでプレゼンターが呼ばれます。リクエストにどう応えるかはプレゼンター次第で、JSON 形式のデータを返すことも、HTML コードの一部や XML のドキュメントを送ることもできます。 - -ブラウザ側では、`fetch()` 関数で AJAX のリクエストを始めます。 - -```js -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -.then(response => response.json()) -.then(payload => { - // レスポンスを処理します -}); -``` - -サーバー側では、[HTTP リクエストを包む |http:request]サービスの `$httpRequest->isAjax()` メソッドで AJAX のリクエストを見分けます。検出には `X-Requested-With` の HTTP ヘッダーを使うので、それを送ることが決定的に重要です。プレゼンターの中では `$this->isAjax()` メソッドが使えます。 - -JSON 形式でデータを送りたい場合は [`sendJson()` |presenters#レスポンスの送信]メソッドを使います。このメソッドはプレゼンターの動作も終わらせます。 - -```php -public function actionExport(): void -{ - $this->sendJson($this->model->getData()); -} -``` - -AJAX 用に用意した特別なテンプレートで応えるつもりなら、次のようにできます。 - -```php -public function handleClick($param): void -{ - if ($this->isAjax()) { - $this->template->setFile('path/to/ajax.latte'); - } - // ... -} -``` - - -スニペット -===== - -サーバーとクライアントをつなぐために Nette が提供する最も強力な道具がスニペットです。これを使えば、ごくわずかな手間と数行のコードで、ふつうのアプリケーションを AJAX のアプリケーションに変えられます。Fifteen の例が全体の動きを示していて、そのコードは [GitHub |https://github.com/nette-examples/fifteen]にあります。 - -スニペットを使うと、ページ全体を読み込み直す代わりに、一部だけを更新できます。速く効率がよいだけでなく、より快適な利用体験も提供します。スニペットは Ruby on Rails の Hotwire や Symfony UX Turbo を思い起こさせるかもしれません。面白いことに、Nette はスニペットをその 14 年前に導入していました。 - -スニペットはどう動くのでしょうか。ページが最初に読み込まれるとき(AJAX でないリクエスト)は、すべてのスニペットを含むページ全体が読み込まれます。ユーザーがページを操作すると(ボタンをクリックする、フォームを送信するなど)、ページ全体を読み込み直す代わりに AJAX のリクエストが始まります。プレゼンターのコードが処理を行い、どのスニペットを更新すべきかを決めます。Nette はそのスニペットを描き、スニペットの配列を含む JSON のペイロードとして送ります。ブラウザ側の処理コードが、受け取ったスニペットをページに戻します。こうして変わったスニペットのコードだけが転送されるので、ページの内容全体を転送する場合と比べて帯域を節約でき、読み込みも速くなります。`redrawControl()` でスニペットが無効化されなければ、Nette は AJAX のリクエストでもページ全体を返します。スニペットが送られるのは、何かが無効化されたときだけです。 - - -Naja ----- - -ブラウザ側でスニペットを扱うには [Naja ライブラリ |https://naja.js.org]を使います。Node.js のパッケージとして[インストール |https://naja.js.org/#/guide/01-install-setup-naja]してください(Webpack、Rollup、Vite、Parcel などのバンドラーと使う場合)。 - -```shell -npm install naja -``` - -…あるいはページのテンプレートに直接書き込みます。 - -```latte - -``` - -まずライブラリを[初期化する |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization]必要があります。 - -```js -naja.initialize(); -``` - -ふつうのリンク(シグナル)やフォームの送信を AJAX のリクエストに変えるには、対象のリンク、フォーム、ボタンに `ajax` クラスを付けるだけです。 - -```latte -Go - -
    - -
    - -または - -
    - -
    -``` - - -スニペットの再描画 ---------- - -[Control |components]クラスのオブジェクト(プレゼンター自身も含みます)はすべて、再描画が必要な変更が起きたかどうかを覚えています。そのために `redrawControl()` メソッドを使います。 - -```php -public function handleLogin(string $user): void -{ - // ログイン後に該当する部分を再描画する必要があります - $this->redrawControl(); - // ... -} -``` - -Nette は、何を再描画すべきかをさらに細かく制御できます。このメソッドはスニペットの名前を引数に取れます。つまりテンプレートの部分の水準で無効化(すなわち再描画の強制)ができます。コンポーネント全体が無効化されると、その中のすべてのスニペットも再描画されます。 - -```php -// 'header' スニペットを無効化します -$this->redrawControl('header'); -``` - -第 2 パラメータ `$redraw` を使えば、保留中の無効化を取り消すこともできます。`$this->redrawControl('header', redraw: false)` を呼ぶと、そのスニペットは再描画不要と印を付けられます。完全なシグネチャは `redrawControl(?string $snippet = null, bool $redraw = true)` です。 - - -Latte でのスニペット -------------- - -Latte でスニペットを使うのはきわめて簡単です。テンプレートの一部をスニペットとして定義するには、`{snippet}` と `{/snippet}` のタグで囲むだけです。 - -```latte -{snippet header} -

    Hello ...

    -{/snippet} -``` - -スニペットは、生成された特別な `id` を持つ `
    ` 要素を HTML のページに作ります。スニペットが再描画されると、この要素の内容が更新されます。ですから、ページが最初に描かれるときには、たとえ最初は空でも、すべてのスニペットが描かれている必要があります。 - -n:属性を使えば、`
    ` 以外の要素でスニペットを作ることもできます。 - -```latte -
    -

    Hello ...

    -
    -``` - - -スニペット領域 -------- - -スニペットの名前は式にもできます。 - -```latte -{foreach $items as $id => $item} -
  • {$item}
  • -{/foreach} -``` - -これだけでは動かない途中の段階です。静的な `{snippet}` や `{snippetArea}` の外で描かれた動的スニペットは、*Dynamic snippets are allowed only inside static snippet/snippetArea.* というメッセージとともに `E_USER_WARNING` を出します。これは以下で直します。 - -これは `item-0`、`item-1` といった複数のスニペットを作ります。動的スニペット(たとえば `item-1`)を直接無効化しても、何も再描画されません。理由は、スニペットが本当に抜粋として働き、それ自身だけが直接描かれるからです。しかしテンプレートの中には、技術的には `item-1` という名前のスニペットは存在しません。それはスニペットを囲むコード、つまり foreach ループが実行されてはじめて生まれます。ですから、実行が必要なテンプレートの部分に `{snippetArea}` タグで印を付けます。 - -```latte -
      - {foreach $items as $id => $item} -
    • {$item}
    • - {/foreach} -
    -``` - -そして個々のスニペットと親の領域の両方の再描画を要求します。 - -```php -$this->redrawControl('itemsContainer'); -$this->redrawControl('item-1'); -``` - -あわせて、`$items` 配列には再描画すべき項目だけが入るようにするとよいでしょう。 - -`{include}` タグを使って、スニペットを含む別のテンプレートを主テンプレートに取り込む場合は、そのテンプレートの取り込みをもう一度 `snippetArea` で包み、スニペットとともに無効化する必要があります。 - -```latte -{snippetArea include} - {include 'included.latte'} -{/snippetArea} -``` - -```latte -{* included.latte *} -{snippet item} - ... -{/snippet} -``` - -```php -$this->redrawControl('include'); -$this->redrawControl('item'); -``` - - -コンポーネントの中のスニペット ---------------- - -[コンポーネント|components]の中にもスニペットを作れ、Nette が自動的に再描画します。ただし制限があります。スニペットを再描画するために、Nette は `render()` メソッドをパラメータなしで呼びます。ですからテンプレートでパラメータを渡しても働きません。 - -```latte -OK -{control productGrid} - -動きません: -{control productGrid, $arg, $arg} -{control productGrid:paginator} -``` - - -独自データの送信 --------- - -スニペットとあわせて、任意の追加データをクライアントに送れます。`payload` オブジェクトに書き込むだけです。 - -```php -public function actionDelete(int $id): void -{ - // ... - if ($this->isAjax()) { - $this->payload->message = 'Success'; - } -} -``` - - -リダイレクト ------- - -AJAX のリクエスト中、`redirect()` と `redirectUrl()` メソッドは HTTP のリダイレクトを送りません。代わりに、AJAX のレスポンスで送られるデータオブジェクトであるペイロードの `payload.redirect` プロパティに宛先の URL を書き込んで送ります。実際のリダイレクトはクライアント側のライブラリ(Naja)が行います。 - - -パラメータの受け渡し -========== - -AJAX のリクエストでコンポーネントにパラメータを送るとき、それがシグナルのパラメータであれ永続パラメータであれ、コンポーネント名を含むグローバルな名前をリクエストで指定しなければなりません。`getParameterId()` メソッドが完全なパラメータ名を返します。 - -```js -let url = new URL({link //foo!}); -url.searchParams.set({$control->getParameterId('bar')}, bar); - -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -``` - -そしてコンポーネント側の、対応するパラメータを持つ handle メソッドです。 - -```php -public function handleFoo(int $bar): void -{ -} -``` - - -関連情報 -==== - -- [動的スニペット |best-practices:dynamic-snippets] diff --git a/application/ja/bootstrapping.texy b/application/ja/bootstrapping.texy deleted file mode 100644 index 53830bd4ea..0000000000 --- a/application/ja/bootstrapping.texy +++ /dev/null @@ -1,312 +0,0 @@ -ブートストラップ -******** - -
    - -ブートストラップとは、アプリケーションの環境を初期化し、依存性注入(DI)コンテナを作り、アプリケーションを起動する過程のことです。ここでは次のことを扱います。 - -- Bootstrap クラスが環境をどう初期化するか -- NEON ファイルでアプリケーションをどう設定するか -- 本番モードと開発モードをどう見分けるか -- DI コンテナをどう作り、どう設定するか - -
    - - -ウェブのアプリケーションであれ、コマンドラインから実行するスクリプトであれ、その実行は何らかの環境の初期化から始まります。昔は `include.inc.php` のような名前のファイルがそれを担い、最初のファイルから読み込まれていました。現代の Nette のアプリケーションでは、それが `Bootstrap` クラスに置き換わり、アプリケーションの一部として `app/Bootstrap.php` ファイルにあります。たとえば次のような形になります。 - -```php -namespace App; - -use Nette; -use Nette\Bootstrap\Configurator; - -class Bootstrap -{ - private Configurator $configurator; - private string $rootDir; - - public function __construct() - { - $this->rootDir = dirname(__DIR__); - // Configurator はアプリケーションの環境とサービスの設定を担当します。 - $this->configurator = new Configurator; - // Nette が生成する一時ファイル(コンパイル済みテンプレートなど)のディレクトリを設定します - $this->configurator->setTempDirectory($this->rootDir . '/temp'); - } - - public function bootWebApplication(): Nette\DI\Container - { - $this->initializeEnvironment(); - $this->setupContainer(); - return $this->configurator->createContainer(); - } - - private function initializeEnvironment(): void - { - // Nette は賢いので開発モードは自動的に有効になります。 - // あるいは次の行のコメントを外して、特定の IP アドレスに対して有効にできます: - // $this->configurator->setDebugMode('secret@23.75.345.200'); - - // Tracy を有効にします。究極の「万能ナイフ」的デバッグツールです。 - $this->configurator->enableTracy($this->rootDir . '/log'); - - // RobotLoader: 選んだディレクトリのすべてのクラスを自動的に読み込みます - $this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); - } - - private function setupContainer(): void - { - // 設定ファイルを読み込みます - $this->configurator->addConfig($this->rootDir . '/config/common.neon'); - } -} -``` - - -index.php -========= - -ウェブアプリケーションの場合、最初のファイルは[公開ディレクトリ |directory-structure#公開ディレクトリ www/] `www/` にある `index.php` です。これは Bootstrap クラスに、環境を初期化して DI コンテナを作るよう指示します。そしてコンテナから `Application` サービスを取り出し、それがウェブアプリケーションを実行します。 - -```php -$bootstrap = new App\Bootstrap; -// 環境を初期化し、DI コンテナを作ります -$container = $bootstrap->bootWebApplication(); -// DI コンテナが Nette\Application\Application オブジェクトを作ります -$application = $container->getByType(Nette\Application\Application::class); -// Nette のアプリケーションを起動し、届いたリクエストを処理します -$application->run(); -``` - -.[note] -`$application` オブジェクトは、リクエストを処理しながら[イベント |nette:glossary#イベント]を発します。`onStartup`、`onRequest`、`onPresenter`、`onResponse`、`onShutdown`、そして(処理されなかった例外に対する)`onError` です。ハンドラを結びつけられるので、ログ記録やアプリケーション全体の監視に便利です。 - -ご覧のとおり、[api:Nette\Bootstrap\Configurator]クラスが環境の設定と依存性注入(DI)コンテナの生成を助けます。ここからそれを詳しく紹介します。 - - -開発モードと本番モード -=========== - -Nette は、開発サーバーで動いているか本番サーバーで動いているかによって振る舞いを変えます。 - -🛠️ 開発モード: - - 役立つ情報(SQL クエリ、実行時間、使用メモリ)を載せた Tracy のデバッグバーを表示します - - エラーのときは、関数の呼び出しと変数の内容を含む詳しいエラーページを表示します - - Latte のテンプレートや設定ファイルなどが変わると、キャッシュを自動的に更新します - - -🚀 本番モード: - - デバッグ情報は一切表示せず、すべてのエラーをログに書きます - - エラーのときは ErrorPresenter か、一般的な「Server Error」のページを表示します - - キャッシュは決して自動更新されません - - 速度と安全性のために最適化されています - - -モードの選択は自動検出で行われるので、ふつうは何も設定したりモードを手で切り替えたりする必要はありません。 - -- 開発モード: localhost(IP アドレス `127.0.0.1` または `::1`)で、プロキシがない場合(つまりその HTTP ヘッダーが検出されない場合) -- 本番モード: それ以外のすべての場所 - -たとえば特定の IP アドレスからアクセスするプログラマーのために、ほかの場合にも開発モードを有効にしたいなら、`setDebugMode()` を使います。 - -```php -$this->configurator->setDebugMode('23.75.345.200'); // IP アドレスの配列も渡せます -``` - -IP アドレスと cookie の組み合わせを強くおすすめします。`nette-debug` の cookie に秘密のトークン、たとえば `secret1234` を保存し、特定の IP アドレスからアクセスし、かつその cookie にそのトークンを持つプログラマーに対して開発モードを有効にします。 - -```php -$this->configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -localhost も含めて、開発モードを完全に無効にすることもできます。 - -```php -$this->configurator->setDebugMode(false); -``` - -値 `true` は開発モードを強制することに注意してください。本番サーバーでは**決して**あってはなりません。 - -自動検出は内部で静的メソッド `Configurator::detectDebugMode()` が行います。これは自分で呼ぶこともでき、たとえば configurator の外で開発モードを検出するのに使えます。IP アドレスやコンピュータ名の許可リストを任意で受け取り、現在のリクエストを開発モードで動かすべきかを返します。 - -```php -$debug = Nette\Bootstrap\Configurator::detectDebugMode('23.75.345.200'); -``` - - -デバッグツール Tracy -============= - -デバッグを楽にするために、素晴らしい道具 [Tracy |tracy:]を有効にします。開発モードではエラーを可視化し、本番モードでは指定したディレクトリにエラーを記録します。 - -```php -$this->configurator->enableTracy($this->rootDir . '/log'); -``` - - -一時ファイル -====== - -Nette は DI コンテナ、RobotLoader、テンプレートなどにキャッシュを使います。ですからキャッシュを保存するディレクトリのパスを設定する必要があります。 - -```php -$this->configurator->setTempDirectory($this->rootDir . '/temp'); -``` - -Linux や macOS では、`log/` と `temp/` ディレクトリに[書き込みの権限 |nette:troubleshooting#ディレクトリの権限の設定]を設定してください。 - - -RobotLoader -=========== - -ふつうはクラスを [RobotLoader |robot-loader:]で自動的に読み込みたいので、それを起動し、`Bootstrap.php` があるディレクトリ(つまり `__DIR__`)とそのすべてのサブディレクトリからクラスを読み込ませます。 - -```php -$this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); -``` - -別の方法として、PSR-4 に従って [Composer |best-practices:composer]だけでクラスを読み込むこともできます。 - - -タイムゾーン -====== - -既定のタイムゾーンは configurator で設定できます。 - -```php -$this->configurator->setTimeZone('Europe/Prague'); -``` - - -DI コンテナの設定 -========== - -起動の過程の一部が DI コンテナ、つまりオブジェクトのファクトリの生成で、これはアプリケーション全体の心臓です。実際には Nette が生成してキャッシュディレクトリに保存する PHP のクラスです。このファクトリがアプリケーションの主要なオブジェクトを作り、設定ファイルでその作り方と設定の仕方を指示することで、アプリケーション全体の振る舞いに影響を与えます。 - -設定ファイルはふつう [NEON 形式 |neon:format]で書きます。[何を設定できるか |nette:configuring]は別の章で読めます。 - -.[tip] -開発モードでは、コードや設定ファイルが変わるたびにコンテナが自動的に更新されます。本番モードでは一度だけ生成され、性能を最大にするために変更は確認されません。 - -`createContainer()` がコンテナを構築してそのインスタンスを返すのに対し、`loadContainer()` メソッドは生成されたコンテナのクラス名だけを返すので、自分でインスタンス化できます。高度な場面で役立ちます。 - -設定ファイルは `addConfig()` で読み込みます。 - -```php -$this->configurator->addConfig($this->rootDir . '/config/common.neon'); -``` - -設定ファイルをさらに足したい場合は、`addConfig()` 関数を何度も呼べます。 - -```php -$configDir = $this->rootDir . '/config'; -$this->configurator->addConfig($configDir . '/common.neon'); -$this->configurator->addConfig($configDir . '/services.neon'); -if (PHP_SAPI === 'cli') { - $this->configurator->addConfig($configDir . '/cli.php'); -} -``` - -`cli.php` という名前は誤りではありません。設定は、それを配列として返す PHP ファイルで書くこともできます。 - -ほかの設定ファイルは [`includes` セクション |dependency-injection:configuration#ファイルの読み込み]でも足せます。 - -設定ファイルに同じキーの項目が現れた場合、それらは上書きされるか、[配列なら統合されます |dependency-injection:configuration#統合]。あとで読み込まれたファイルのほうが、前のものより優先度が高くなります。`includes` セクションを持つファイルは、そこで読み込まれるファイルより優先度が高くなります。 - - -静的パラメータ -------- - -設定ファイルで使うパラメータは [`parameters` セクション |dependency-injection:configuration#パラメータ]で定義でき、`addStaticParameters()` メソッド(古い、いまは非推奨の別名は `addParameters()`)で渡す(または上書きする)こともできます。大事なのは、パラメータの値が違えば別の DI コンテナ、つまり別のクラスが生成されるという点です。 - -```php -$this->configurator->addStaticParameters([ - 'projectId' => 23, -]); -``` - -`projectId` パラメータは、設定の中で標準の書き方 `%projectId%` で参照できます。 - - -動的パラメータ -------- - -コンテナには動的パラメータも足せます。静的パラメータと違い、その値が違っても新しい DI コンテナは生成されません。 - -```php -$this->configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -こうすれば、たとえば環境変数を簡単に足せます。設定の中では `%env.variable%` の書き方で参照できます。 - -```php -$this->configurator->addDynamicParameters([ - 'env' => getenv(), -]); -``` - - -既定のパラメータ --------- - -設定ファイルでは次のパラメータが使えます。 - -- `%appDir%` は `Bootstrap.php` ファイルがあるディレクトリへの絶対パスです -- `%wwwDir%` は入口のファイル `index.php` があるディレクトリへの絶対パスです -- `%tempDir%` は一時ファイル用のディレクトリへの絶対パスです -- `%vendorDir%` は Composer がライブラリをインストールするディレクトリへの絶対パスです -- `%rootDir%` はプロジェクトのルートディレクトリへの絶対パスです -- `%baseUrl%` はルートディレクトリへの絶対 URL です(実行時に解決される動的パラメータです) -- `%debugMode%` はアプリケーションがデバッグモードかどうかを示します -- `%consoleMode%` はリクエストがコマンドラインから来たかどうかを示します - - -取り込まれるサービス ----------- - -さらに深く踏み込みましょう。DI コンテナの目的はオブジェクトを作ることですが、ときには既存のオブジェクトをコンテナに入れる必要が出てきます。そのためには `imported: true` フラグを付けてサービスを定義します。 - -```neon -services: - myservice: - type: App\Model\MyCustomService - imported: true -``` - -そして bootstrap でオブジェクトをコンテナに入れます。 - -```php -$this->configurator->addServices([ - 'myservice' => new App\Model\MyCustomService('foobar'), -]); -``` - - -さまざまな環境 -======= - -`Bootstrap` クラスは必要に応じて自由に書き換えてください。ウェブのプロジェクトを見分けるために `bootWebApplication()` メソッドにパラメータを足せます。あるいはメソッドを増やしてもよいでしょう。ユニットテスト用に環境を初期化する `bootTestEnvironment()`、コマンドラインから呼ばれるスクリプト用の `bootConsoleApplication()` などです。 - -```php -public function bootTestEnvironment(): Nette\DI\Container -{ - Tester\Environment::setup(); // Nette Tester の初期化 - $this->setupContainer(); - return $this->configurator->createContainer(); -} - -public function bootConsoleApplication(): Nette\DI\Container -{ - $this->configurator->setDebugMode(false); - $this->initializeEnvironment(); - $this->setupContainer(); - return $this->configurator->createContainer(); -} -``` diff --git a/application/ja/components.texy b/application/ja/components.texy deleted file mode 100644 index d734c1fedd..0000000000 --- a/application/ja/components.texy +++ /dev/null @@ -1,504 +0,0 @@ -インタラクティブなコンポーネント -**************** - -
    - -コンポーネントは、ページに埋め込む独立した再利用できるオブジェクトです。フォーム、データグリッド、アンケートなど、繰り返し使う意味のあるものなら何でもかまいません。ここでは次のことを扱います。 - -- コンポーネントの使い方 -- その書き方 -- シグナルとは何か - -
    - -Nette には組み込みのコンポーネントのしくみがあります。Delphi や ASP.NET Web Forms の熟練者には似たものが馴染み深いかもしれません。React や Vue.js も、遠く似た考えの上に築かれています。とはいえ PHP のフレームワークの世界では、これは他にない機能です。 - -同時にコンポーネントは、アプリケーション開発への向き合い方を根本から変えます。あらかじめ用意された部品からページを組み立てられます。管理画面にデータグリッドが必要ですか。Nette 向けのオープンソースのアドオン(コンポーネントに限りません)を集めた [Componette |https://componette.org/search/component]で見つけて、プレゼンターに差し込むだけです。 - -プレゼンターにはいくつでもコンポーネントを組み込めます。そしてコンポーネントの中にほかのコンポーネントを埋め込めます。こうしてプレゼンターを根とするコンポーネントの木ができます。 - - -ファクトリメソッド -========= - -コンポーネントはどうプレゼンターに差し込まれ、どう使われるのでしょうか。ふつうはファクトリメソッドを通じてです。 - -コンポーネントのファクトリは、本当に必要になったときにだけコンポーネントを作る(遅延、オンデマンド)優雅な方法です。魔法のすべては `createComponent()` という名前のメソッドを実装することにあります。`` は作られるコンポーネントの名前で、このメソッドがそれを作って返します。 - -```php .{file:DefaultPresenter.php} -class DefaultPresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentPoll(): PollControl - { - $poll = new PollControl; - $poll->items = $this->items; - return $poll; - } -} -``` - -すべてのコンポーネントが別々のメソッドで作られるので、コードが分かりやすくなります。 - -.[note] -コンポーネントの名前は、メソッド名では大文字で始まっていても、常に小文字で始まります。 - -ファクトリを直接呼ぶことは決してありません。コンポーネントを最初に使ったときに自動的に呼ばれます。おかげでコンポーネントは適切な瞬間に、しかも本当に必要な場合にだけ作られます。コンポーネントを使わなければ(ページの一部だけを転送する AJAX のリクエストや、テンプレートをキャッシュする場合など)まったく作られないので、サーバーの性能を節約できます。 - -```php .{file:DefaultPresenter.php} -// コンポーネントにアクセスします。それが最初なら -// createComponentPoll() が呼ばれて作られます -$poll = $this->getComponent('poll'); -// 別の書き方: $poll = $this['poll']; -``` - -テンプレートでは [{control} |#描画]タグでコンポーネントを描けます。ですからコンポーネントを手でテンプレートに渡す必要はありません。 - -```latte -

    投票してください

    - -{control poll} -``` - -.[tip] -数が変わるコンポーネントを動的に作るには [Multiplier |multiplier]を使います。 - -`createComponent()` のファクトリメソッドはプレゼンターだけのものではありません。同じやり方でコンポーネントの中にコンポーネントを入れ子にし、木に組み立てられます。たとえばコンポーネントの中で別々に描かれるフォームに便利です。 - - -ハリウッド流 -====== - -コンポーネントはふつう、私たちがハリウッド流と呼びたくなる新鮮な手法を使います。映画のオーディションの参加者がよく耳にする決まり文句をご存じでしょう。「こちらから連絡します、あなたからはしないでください。」まさにそういうことです。 - -Nette では、絶えず問い続ける(「フォームは送信されたか」「それは正しかったか」「ユーザーはこのボタンを押したか」)代わりに、フレームワークに「これが起きたら、このメソッドを呼んで」と伝えて、あとは任せます。JavaScript でプログラムしているなら、この書き方はよくご存じでしょう。ある出来事が起きたときに呼ばれる関数を書き、言語が適切なパラメータをそこに渡してくれます。 - -これはアプリケーションを書くときの見方をすっかり変えます。フレームワークに任せられる仕事が多いほど、あなたの手間は減ります。そして見落としも減ります。 - - -コンポーネントを書く -========== - -コンポーネントという語は、ふつう [api:Nette\Application\UI\Control]クラスの子孫を指します。(「control」という語のほうが正確ですが、言語によっては別の意味を持つので、「component」のほうが定着しました。)プレゼンター [api:Nette\Application\UI\Presenter]自身も `Control` クラスの子孫です。 - -```php .{file:PollControl.php} -use Nette\Application\UI\Control; - -class PollControl extends Control -{ -} -``` - - -描画 -=== - -コンポーネントを描くのに `{control componentName}` タグを使うことはすでに見ました。これは実際にはコンポーネントの `render()` メソッドを呼び、そこで描画の面倒を見ます。プレゼンターと同じく `$this->template` 変数に [Latte のテンプレート|templates]があり、そこにパラメータを渡します。プレゼンターと違うのは、テンプレートのファイルを指定して描かせる必要がある点です。 - -```php .{file:PollControl.php} -public function render(): void -{ - // テンプレートにいくつかのパラメータを入れます - $this->template->param = $value; - // そして描きます - $this->template->render(__DIR__ . '/poll.latte'); -} -``` - -`{control}` タグは `render()` メソッドにパラメータを渡せます。 - -```latte -{control poll, $id, $message} -``` - -```php .{file:PollControl.php} -public function render(int $id, string $message): void -{ - // ... -} -``` - -コンポーネントが、別々に描きたいいくつかの部分から成ることもあります。それぞれについて独自の描画メソッドを作ります。この例では `renderPaginator()` です。 - -```php .{file:PollControl.php} -public function renderPaginator(): void -{ - // ... -} -``` - -テンプレートでは次のように呼び出します。 - -```latte -{control poll:paginator} -``` - -理解を深めるために、このタグが PHP のコードにどう変換されるかを知っておくとよいでしょう。 - -```latte -{control poll} -{control poll:paginator, 123, 'hello'} -``` - -は次に変換されます。 - -```php -$control->getComponent('poll')->render(); -$control->getComponent('poll')->renderPaginator(123, 'hello'); -``` - -`getComponent()` メソッドが `poll` コンポーネントを返し、そのコンポーネントで `render()` メソッド、あるいはタグのコロンのあとに別の描画メソッドが指定されていれば `renderPaginator()` が呼ばれます。 - -.[caution] -注意してください。パラメータの中で角かっこの外に **`=>`** が現れると、すべてのパラメータが配列に包まれ、第 1 引数として渡されます。 - -```latte -{control poll, id: 123, message: 'hello'} -``` - -は次に変換されます。 - -```php -$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']); -``` - -サブコンポーネントの描画: - -```latte -{control cartControl-someForm} -``` - -は次に変換されます。 - -```php -$control->getComponent("cartControl-someForm")->render(); -``` - -コンポーネントもプレゼンターと同じく、いくつかの役立つ変数をテンプレートに自動的に渡します。 - -- `$basePath` はルートディレクトリへの絶対 URL パスです(たとえば `/eshop`) -- `$baseUrl` はルートディレクトリへの絶対 URL です(たとえば `http://localhost/eshop`) -- `$user` は[ユーザーを表す |security:authentication]オブジェクトです -- `$presenter` は現在のプレゼンターです -- `$control` は現在のコンポーネントです -- `$flashes` は `flashMessage()` 関数で送られた[メッセージ |#フラッシュメッセージ]の配列です - - -シグナル -==== - -Nette のアプリケーションでの移動が、`Presenter:action` の組へのリンクやリダイレクトから成ることはすでに見ました。しかし**現在のページ**で何か処理をしたいだけの場合はどうでしょうか。たとえば表の列の並べ替えを変える、項目を削除する、ライト/ダークモードを切り替える、フォームを送信する、アンケートに投票する、などです。 - -この種のリクエストをシグナルと呼びます。アクションが `action()` や `render()` メソッドを呼ぶのと同じように、シグナルは `handle()` メソッドを呼びます。アクション(やビュー)の考え方が純粋にプレゼンターに関わるのに対し、シグナルはすべてのコンポーネントに関わります。`UI\Presenter` は `UI\Control` の子孫なので、プレゼンターにも関わります。 - -```php -public function handleClick(int $x, int $y): void -{ - // ... シグナルの処理 ... -} -``` - -シグナルを呼ぶリンクはいつもどおりに作ります。つまりテンプレートでは `n:href` 属性か `{link}` タグ、コードでは `link()` メソッドです。詳しくは [URL リンクの作成 |creating-links#シグナルへのリンク]の章をご覧ください。 - -```latte -ここをクリック -``` - -シグナルは常に現在のプレゼンターとアクションで呼ばれます。別のプレゼンターやアクションで呼ぶことはできません。 - -ですからシグナルは、もとのリクエストと同じようにページを読み込み直しつつ、加えてシグナルを処理するメソッドを適切なパラメータで呼びます。そのメソッドがなければ [api:Nette\Application\UI\BadSignalException]例外が投げられ、ユーザーには 403 Forbidden のエラーページとして表示されます。 - - -スニペットと AJAX -=========== - -シグナルは AJAX を少し思い起こさせるかもしれません。現在のページで呼ばれるハンドラだからです。そのとおりで、シグナルは実際に AJAX で呼ばれることが多く、そのあとページの変わった部分だけがブラウザに転送されます。これをスニペットと呼びます。詳しくは [AJAX のページ |ajax]をご覧ください。 - - -フラッシュメッセージ -========== - -コンポーネントは、プレゼンターとは独立した自分のフラッシュメッセージの保管場所を持ちます。これはたとえば操作の結果を知らせるメッセージです。フラッシュメッセージの大事な性質は、リダイレクト後もテンプレートで使えることです。一度表示されたあとも、さらに 30 秒は有効なままです。たとえば通信のエラーでユーザーがページを再読み込みしても、メッセージがすぐ消えることはありません。 - -送信は [flashMessage |api:Nette\Application\UI\Control::flashMessage()]メソッドが担当します。第 1 パラメータはメッセージの本文(`string`、`Stringable`)か、メッセージを表す `stdClass` オブジェクトです。省略可能な第 2 パラメータはその種類(error、warning、info など)です。`flashMessage()` メソッドはフラッシュメッセージのインスタンスを `stdClass` オブジェクトとして返すので、さらに情報を足せます。 - -```php -$this->flashMessage('項目を削除しました。'); -$this->redirect(/* ... */); // そしてリダイレクト -``` - -これらのメッセージは、テンプレートの `$flashes` 変数に `stdClass` オブジェクトとして入っていて、`message`(メッセージの本文)、`type`(メッセージの種類)のプロパティを持ち、先ほど触れたユーザーの情報を含むこともあります。たとえば次のように描きます。 - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -シグナルの処理後のリダイレクト -=============== - -コンポーネントのシグナルの処理のあとには、リダイレクトが続くことがよくあります。フォームと同じで、送信後にはリダイレクトして、ブラウザでページを再読み込みしてもデータが再送信されないようにします。 - -```php -$this->redirect('this'); // 現在のプレゼンターとアクションにリダイレクトします -``` - -コンポーネントは再利用できる部品で、ふつう特定のプレゼンターへの直接のつながりを持つべきではないので、`redirect()` と `link()` メソッドはパラメータを自動的にコンポーネントのシグナルと解釈します。 - -```php -$this->redirect('click'); // 同じコンポーネントの 'click' シグナルにリダイレクトします -``` - -別のプレゼンターやアクションにリダイレクトする必要があるなら、プレゼンターを通して行えます。 - -```php -$this->getPresenter()->redirect('Product:show'); // 別のプレゼンター/アクションにリダイレクトします -``` - - -永続パラメータ -======= - -永続パラメータは、コンポーネントの状態をリクエストをまたいで保つために使います。その値はリンクをクリックしたあとも変わりません。セッションのデータと違い、URL で運ばれます。しかもそれは完全に自動的に起こり、同じページのほかのコンポーネントで作られたリンクにも及びます。 - -たとえば内容をページ分けするコンポーネントがあるとします。そうしたコンポーネントがページに複数あるかもしれません。そしてリンクをクリックしたあとも、すべてのコンポーネントが今のページにとどまってほしいとします。ですからページ番号(`page`)を永続パラメータにします。 - -Nette で永続パラメータを作るのはきわめて簡単です。public のプロパティを作り、アトリビュートで印を付けるだけです(以前は `/** @persistent */` が使われていました)。 - -```php -use Nette\Application\Attributes\Persistent; // この行が大事です - -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; // public でなければなりません -} -``` - -プロパティにはデータ型(`int` など)を指定することをおすすめしますし、既定値も与えられます。パラメータの値は[検証できます |#永続パラメータの検証]。 - -リンクを作るとき、永続パラメータの値は変えられます。 - -```latte -次へ -``` - -あるいは*リセット*して URL から取り除けます。その場合は既定値になります。 - -```latte -リセット -``` - - -永続コンポーネント -========= - -パラメータだけでなく、コンポーネントも永続にできます。その永続パラメータは、プレゼンターの異なるアクションのあいだや、複数のプレゼンターのあいだでも引き継がれます。永続コンポーネントには、プレゼンターのクラスにアトリビュートで印を付けます。たとえば `calendar` と `poll` のコンポーネントには次のように印を付けます。 - -```php -use Nette\Application\Attributes\Persistent; - -#[Persistent('calendar', 'poll')] -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -これらのコンポーネントの中のサブコンポーネントには印を付ける必要がありません。それらも永続になります。 - -古いアノテーション `@persistent` もまだ動きますが、非推奨で警告を出します。 - -```php -/** - * @persistent(calendar, poll) - */ -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -依存関係を持つコンポーネント -============== - -依存関係を持つコンポーネントを、それを使うプレゼンターを「散らかさず」に作るにはどうすればよいでしょうか。Nette の DI コンテナの賢い機能のおかげで、通常のサービスと同じく、ほとんどの仕事をフレームワークに任せられます。 - -`PollFacade` サービスに依存するコンポーネントを例に取りましょう。 - -```php -class PollControl extends Control -{ - public function __construct( - private int $id, // コンポーネントを作る対象のアンケートの ID - private PollFacade $facade, - ) { - } - - public function handleVote(int $voteId): void - { - $this->facade->vote($this->id, $voteId); - // ... - } -} -``` - -通常のサービスを書いているなら、議論の余地はありません。DI コンテナがすべての依存関係の受け渡しを見えないところでこなしてくれます。しかしコンポーネントの場合、ふつうは[ファクトリメソッド |#ファクトリメソッド] `createComponent…()` の中で、プレゼンターの中に新しいインスタンスを作って扱います。とはいえ、すべてのコンポーネントのすべての依存関係を、コンポーネントに渡すためだけにプレゼンターへ渡すのは面倒です。しかも書くコードの量ときたら……。 - -当然の疑問として、コンポーネントを通常のサービスとして登録し、プレゼンターに渡して `createComponent…()` メソッドで返せばよいのでは、と思うかもしれません。しかしこのやり方はふさわしくありません。必要ならコンポーネントを何度も作れるようにしたいからです。 - -正しい解は、コンポーネントのファクトリ、つまりコンポーネントを作ってくれるクラスを書くことです。 - -```php -class PollControlFactory -{ - public function __construct( - private PollFacade $facade, - ) { - } - - public function create(int $id): PollControl - { - return new PollControl($id, $this->facade); - } -} -``` - -このファクトリを設定でコンテナに登録します。 - -```neon -services: - - PollControlFactory -``` - -そして最後にプレゼンターで使います。 - -```php -class PollPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private PollControlFactory $pollControlFactory, - ) { - } - - protected function createComponentPollControl(): PollControl - { - $pollId = 1; // 自分のパラメータを渡せます - return $this->pollControlFactory->create($pollId); - } -} -``` - -素晴らしいのは、Nette DI がこうした単純なファクトリを[生成できる |dependency-injection:factory]ことです。ですからそのコード全体を書く代わりに、インターフェースを書くだけで済みます。 - -```php -interface PollControlFactory -{ - public function create(int $id): PollControl; -} -``` - -これだけです。Nette が内部でこのインターフェースを実装し、プレゼンターに注入してくれるので、そこで使えます。`$id` パラメータと `PollFacade` クラスのインスタンスを、魔法のようにコンポーネントに足してくれます。 - - -コンポーネントの詳細 -========== - -Nette Application のコンポーネントは、ページに埋め込むウェブアプリケーションの再利用できる部分で、この章はまるごとそれに捧げられてきました。そうしたコンポーネントには、正確には何ができるのでしょうか。 - -1) テンプレートに描ける -2) AJAX のリクエストのときに[自分のどの部分を |ajax#スニペット]描くべきかを知っている(スニペット) -3) 自分の状態を URL に保存できる(永続パラメータ) -4) ユーザーの操作に反応できる(シグナル) -5) 階層的な構造を作る(根はプレゼンター) - -これらの機能は、それぞれ継承の系列の中のいずれかのクラスが担当します。描画(1 + 2)は [api:Nette\Application\UI\Control]が、[ライフサイクル |presenters#プレゼンターのライフサイクル]への統合(3、4)は [api:Nette\Application\UI\Component]クラスが、階層的な構造の生成(5)は [Container と Component |component-model:]のクラスが担当します。 - -``` -Nette\ComponentModel\Component { IComponent } -| -+- Nette\ComponentModel\Container { IContainer } - | - +- Nette\Application\UI\Component { SignalReceiver, StatePersistent } - | - +- Nette\Application\UI\Control { Renderable } - | - +- Nette\Application\UI\Presenter { IPresenter } -``` - - -コンポーネントのライフサイクル ---------------- - -[* lifecycle-component.svg *] *** *コンポーネントのライフサイクル* .<> - - -永続パラメータの検証 ----------- - -URL から受け取った[永続パラメータ |#永続パラメータ]の値は、`loadState()` メソッドがプロパティに書き込みます。あわせてプロパティに指定されたデータ型と合うかも確認し、合わなければ 404 のエラーで応え、ページは表示されません。 - -永続パラメータを決して盲信しないでください。ユーザーに URL で簡単に書き換えられます。たとえばページ番号 `$this->page` が 0 より大きいかを次のように確かめます。適切な方法が、先ほどの `loadState()` メソッドの上書きです。 - -```php -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; - - public function loadState(array $params): void - { - parent::loadState($params); // ここで $this->page が設定されます - // 続いて独自の値のチェック: - if ($this->page < 1) { - $this->error(); - } - } -} -``` - -逆の処理、つまり永続プロパティから値を集めるのは `saveState()` メソッドが担当します。 - - -プレゼンターへの接続 ----------- - -コンポーネントがプレゼンターの階層の一部になった瞬間、`$onAnchor` 配列に入っているコールバックが呼ばれます。その時点からコンポーネントはプレゼンターを使えるようになり、安全にリンクを作ったり、永続パラメータを読んだりできます。 - -```php -$control->onAnchor[] = function ($control): void { - // コンポーネントがプレゼンターを使えるようになりました -}; -``` - - -シグナルの詳細 -------- - -シグナルは(AJAX で呼ばれる場合を除き)もとのリクエストとまったく同じようにページを読み込み直し、`signalReceived($signal)` メソッドを呼びます。`Nette\Application\UI\Component` クラスでのその既定の実装は、`handle` という語を組み合わせたメソッドを呼ぼうとします。そのあとの処理はそのオブジェクト次第です。`Component` を継承したオブジェクト(つまり `Control` と `Presenter`)は、`handle` メソッドを適切なパラメータで呼ぼうとして反応します。 - -言い換えると、`handle` 関数の定義を取り、リクエストとともに来たすべてのパラメータと、URL のパラメータを名前で引数に割り当てて、メソッドを呼ぼうとします。たとえば URL の `id` パラメータの値は `$id` 引数として、URL の `something` は `$something` として渡されます。そしてメソッドがなければ、`signalReceived` メソッドが[例外 |api:Nette\Application\UI\BadSignalException]を投げます。 - -URL のパラメータのほかに、シグナルは**リクエストの POST 本体**で送られたパラメータも読みます。シグナルは JavaScript から呼ばれることが多く、そこでは POST メソッドでデータを送るのが自然なので、これは便利です。ただし同じ名前のパラメータが URL と POST 本体の両方から来た場合、**URL の値が優先されます**。ですから POST のフィールドに URL やルートのパラメータと同じ名前を付けるのは避けてください。さもないと URL の値が黙ってそれを上書きしてしまいます。シグナルのパラメータは、アクションのパラメータや永続パラメータと共通の空間を持ちます。[共有されるパラメータの空間 |presenters#共有されるパラメータの空間]をご覧ください。 - -シグナルは、`SignalReceiver` インターフェースを実装してコンポーネントの木につながっている任意のコンポーネント、プレゼンター、オブジェクトが受け取れます。 - -シグナルの主な受け手は `Presenter` と、`Control` を継承した画面のコンポーネントになるでしょう。シグナルは、オブジェクトに何かをすべきだと知らせる合図として働きます。アンケートはユーザーの票を数えるべき、ニュースの欄は広がって 2 倍のニュースを表示すべき、フォームが送信されたのでデータを処理すべき、といった具合です。 - -シグナルの URL は [Component::link() |api:Nette\Application\UI\Component::link()]メソッドで作ります。`$destination` パラメータには文字列 `{signal}!` を、`$args` にはシグナルに渡したい引数の配列を渡します。シグナルは常に現在のプレゼンターとアクションで、現在のパラメータとともに呼ばれ、シグナルのパラメータが足されるだけです。さらに**シグナルを指定するパラメータ `?do`** が足されます。 - -その形式は `{signal}` か `{signalReceiver}-{signal}` です。`{signalReceiver}` はプレゼンターの中のコンポーネントの名前です。ですからコンポーネント名にハイフンは使えません。コンポーネント名とシグナルを分けるのに使われるからです。とはいえ、この方法で複数のコンポーネントを入れ子にできます。 - -[isSignalReceiver()|api:Nette\Application\UI\Presenter::isSignalReceiver()]メソッドは、コンポーネント(第 1 引数)がシグナル(第 2 引数)の受け手かどうかを調べます。第 2 引数は省略でき、その場合はそのコンポーネントが何らかのシグナルの受け手かを調べます。第 2 パラメータを `true` にすると、指定したコンポーネントかその子孫のいずれかが受け手かどうかを確かめます。 - -`handle` より前のどの段階でも、[processSignal()|api:Nette\Application\UI\Presenter::processSignal()]メソッドを呼んでシグナルを手動で実行できます。このメソッドがシグナルの処理を引き受け、シグナルの受け手とされたコンポーネント(受け手が指定されていなければプレゼンター自身)を取って、そこへシグナルを送ります。 - -例を挙げます。 - -```php -if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) { - $this->processSignal(); -} -``` - -これでシグナルが前倒しで実行され、もう一度呼ばれることはありません。 diff --git a/application/ja/configuration.texy b/application/ja/configuration.texy deleted file mode 100644 index 28bb3a9af9..0000000000 --- a/application/ja/configuration.texy +++ /dev/null @@ -1,186 +0,0 @@ -アプリケーションの設定 -*********** - -.[perex] -Nette Application の設定オプションの概要です。 - - -Application -=========== - -```neon -application: - # Tracy の BlueScreen に「Nette Application」パネルを表示しますか? - debugger: ... # (bool) Tracy が使えれば有効 - - # 本番では例外は常に error-presenter が扱います。 - # このオプションは開発モードでもその振る舞いを有効にするだけです - catchExceptions: ... # (bool) 既定は false。つまり開発では無効、本番では常に有効 - - # error-presenter の名前 - errorPresenter: Error # (string|array) 既定は 'Nette:Error' - - # プレゼンターとアクションの別名を定義します - aliases: ... - - # プレゼンター名をクラスに変換する規則を定義します - mapping: ... - - # 不正なリンクの警告を抑制しますか? - # 開発モードでのみ有効です - silentLinks: ... # (bool) 既定は false -``` - -`nette/application` のバージョン 3.2 以降、エラー用のプレゼンターを 2 つ定義できます。 - -```neon -application: - errorPresenter: - 4xx: Error4xx # Nette\Application\BadRequestException 用 - 5xx: Error5xx # そのほかの例外用 -``` - -分けると便利なのは、この 2 つの状況が根本的に異なるからです。`BadRequestException`(4xx のコード)は、アプリケーションは正常で、訪問者が存在しないものを求めただけであることを意味します。ですからサイトのレイアウトの中で親切なメッセージを出す、機能の揃ったプレゼンターを使えます。逆に 5xx のエラーは、アプリケーションの何かが壊れていて、それが何かは分からないことを意味します。5xx のプレゼンターは、描画中にほかの何かが失敗しないよう、できるだけ最小限に保ってください。理想としては、データベースにもレイアウトにもログイン中のユーザーにも触れないことです。 - -`silentLinks` オプションは、開発モードでリンクの生成が失敗したとき(たとえばプレゼンターが存在しないときなど)に Nette がどう振る舞うかを決めます。既定値の `false` は、Nette が `E_USER_WARNING` のエラーを出すことを意味します。`true` にするとこのエラーメッセージが抑制されます。本番環境では常に `E_USER_WARNING` が出ます。この振る舞いは、プレゼンターの変数 [$invalidLinkMode |creating-links#不正なリンク]の設定でも変えられます。 - -[別名を使うと |creating-links#別名]、よく使うプレゼンターを簡単に参照できます。 - -[mapping は規則を定義し |directory-structure#プレゼンターのマッピング]、プレゼンター名からクラス名が導かれます。 - - -プレゼンターの自動登録 ------------ - -Nette はプレゼンターをサービスとして DI コンテナに自動的に追加し、その生成を大きく速くします。Nette がプレゼンターをどう探すかは設定できます。 - -```neon -application: - # Composer のクラスマップでプレゼンターを探しますか? - scanComposer: ... # (bool) 既定は true - - # クラス名とファイル名が一致すべきマスク - scanFilter: ... # (string) 既定は '*Presenter' - - # どのディレクトリでプレゼンターを探しますか? - scanDirs: # (string[]|false) 既定は '%appDir%' - - %vendorDir%/mymodule -``` - -`scanDirs` に並べたディレクトリは既定値 `%appDir%` を上書きせず、それを補うので、`scanDirs` には `%appDir%` と `%vendorDir%/mymodule` の両方のパスが入ります。既定のディレクトリを外したい場合は[感嘆符 |dependency-injection:configuration#統合]を使います。 - -```neon -application: - scanDirs!: - - %vendorDir%/mymodule -``` - -値を `false` にすると、ディレクトリの走査を無効にできます。するとプレゼンターはサービスとして登録されなくなるので、[decorator |dependency-injection:configuration#Decorator]セクションで調整できなくなり、生成も遅くなります。ですから自動登録を完全に抑えることはおすすめしません。アプリケーションの性能が落ちるからです。 - - -Latte テンプレート -============ - -この設定は、コンポーネントとプレゼンターにおける Latte の振る舞いに全体として影響します。 - -```neon -latte: - # Tracy バーに Latte パネルを、主テンプレートについて表示(true)、それとも全コンポーネントについて(all)? - debugger: ... # (true|false|'all') Tracy が使えれば有効(デバッグモードのみ) - - # declare(strict_types=1) のヘッダー付きでテンプレートを生成します - strictTypes: ... # (bool) 既定は false - - # [厳格なパーサーモード |latte:develop#strict mode]を有効にします - strictParsing: ... # (bool) 既定は false - - # 変数のスコープをループ本体に限定します - scopedLoopVariables: ... # (bool) 既定は false - - # ペアタグの入れ子によるインデントを取り除きます - dedent: ... # (bool) 既定は false - - # [生成されたコードのチェック |latte:develop#Checking Generated Code]を有効にします - phpLinter: ... # (string) 既定は null - - # ロケールを設定します - locale: cs_CZ # (string) 既定は null - - # $this->template オブジェクトのクラス - templateClass: App\MyTemplateClass # 既定は Nette\Bridges\ApplicationLatte\DefaultTemplate -``` - -新しい[拡張 |latte:extending-latte#Latte Extension]は次のように追加できます。 - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - - -ルーティング -====== - -基本の設定です。 - -```neon -routing: - # Tracy バーにルーティングのパネルを表示しますか? - debugger: ... # (bool) Tracy が使えれば有効(デバッグモードのみ) - - # ルーターを DI コンテナにシリアライズします - cache: ... # (bool) 既定は false -``` - -ルーティングはふつう [RouterFactory |routing#ルートのコレクション]クラスで定義します。あるいは `mask: action` の組を使って設定でルートを定義することもできますが、この方法はあまり柔軟ではありません。 - -```neon -routing: - routes: - 'detail/': Admin:Home:default - '/': Front:Home:default -``` - - -定数 -=== - -PHP の定数を作ります。 - -```neon -constants: - Foobar: 'baz' -``` - -`Foobar` 定数はアプリケーションの起動後に作られます。 - -.[note] -定数はグローバルに使える変数の代わりにすべきではありません。オブジェクトに値を渡すには[依存性注入 |dependency-injection:passing-dependencies]を使ってください。 - - -PHP -=== - -PHP のディレクティブの設定です。すべてのディレクティブの一覧は [php.net |https://www.php.net/manual/en/ini.list.php]にあります。 - -```neon -php: - date.timezone: Europe/Prague -``` - - -DI サービス -======= - -次のサービスが DI コンテナに追加されます。 - -| 名前 | 型 | 説明 -|----------------------------|---------------------------------------------------|----------------------------------------- -| `application.application` | [api:Nette\Application\Application] | [アプリケーションの実行役 |how-it-works#Nette Application] -| `application.linkGenerator` | [api:Nette\Application\LinkGenerator] | [LinkGenerator |creating-links#LinkGenerator] -| `application.presenterFactory` | [api:Nette\Application\IPresenterFactory] | プレゼンターのファクトリ -| `application.###` | [api:Nette\Application\UI\Presenter] | 個々のプレゼンター -| `routing.router` | [api:Nette\Routing\Router] | ルーター -| `latte.latteFactory` | [api:Nette\Bridges\ApplicationLatte\LatteFactory] | `Latte\Engine` オブジェクトのファクトリ -| `latte.templateFactory` | [api:Nette\Application\UI\TemplateFactory] | [`$this->template` |templates]のファクトリ diff --git a/application/ja/creating-links.texy b/application/ja/creating-links.texy deleted file mode 100644 index 283a57f846..0000000000 --- a/application/ja/creating-links.texy +++ /dev/null @@ -1,319 +0,0 @@ -URL リンクの作成 -********** - -
    - -Nette でのリンクの作成は、指を差すのと同じくらい簡単です。狙いを定めるだけで、あとはフレームワークが全部やってくれます。ここでは次のことを扱います。 - -- テンプレートやそのほかの場所でリンクを作る方法 -- 現在のページへのリンクを見分ける方法 -- 不正なリンクへの対処 - -
    - - -[双方向のルーティング |routing]のおかげで、あとで変わるかもしれない、あるいは組み立てが面倒なアプリケーションの URL を、テンプレートやコードに直接書き込む必要はもうありません。リンクにはプレゼンターとアクションを指定し、必要なパラメータを渡すだけで、URL はフレームワークが生成します。実のところ、関数を呼ぶのとよく似ています。きっと気に入るでしょう。 - - -プレゼンターのテンプレートで -============== - -リンクを作るのはたいていテンプレートの中で、そこでは `n:href` 属性が頼もしい助けになります。 - -```latte -詳細 -``` - -HTML の属性 `href` の代わりに [n:属性 |latte:syntax#n:属性]の `n:href` を使っていることに注目してください。その値は、`href` 属性の場合のような URL ではなく、プレゼンターとアクションの名前です。 - -リンクをクリックすることは、簡単にいえば `ProductPresenter::renderShow()` メソッドを呼ぶようなものです。そしてそのシグネチャにパラメータがあれば、引数を付けて呼べます。 - -```latte -商品の詳細 -``` - -名前付きパラメータも渡せます。次のリンクは、値 `en` を持つパラメータ `lang` を渡します。 - -```latte -商品の詳細 -``` - -`ProductPresenter::renderShow()` メソッドのシグネチャに `$lang` がなければ、`$lang = $this->getParameter('lang')` か[プロパティ |presenters#リクエストのパラメータ]でその値を取得できます。 - -パラメータが配列に入っているなら、`...` 演算子で展開できます。 - -```latte -{var $args = [$product->id, lang => en]} -商品の詳細 -``` - -いわゆる[永続パラメータ |presenters#永続パラメータ]もリンクに自動的に渡されます。 - -`n:href` 属性は HTML の `` タグにとても便利です。リンクをそれ以外の場所、たとえば文章の中に出したい場合は `{link}` を使います。 - -```latte -URL は次のとおりです: {link Home:default} -``` - - -コードの中で -====== - -プレゼンターでリンクを作るには `link()` メソッドを使います。 - -```php -$url = $this->link('Product:show', $product->id); -``` - -パラメータは配列としても渡せ、その中で名前付きパラメータも指定できます。 - -```php -$url = $this->link('Product:show', [$product->id, 'lang' => 'en']); -``` - -リンクは [#LinkGenerator]とその `link()` メソッドを使えば、プレゼンターなしでも作れます。 - -ときには、いまリンクを作りつつ、実際の URL の生成はあとにしたいことがあります。そのために `lazyLink()` メソッドがあり、`Nette\Application\UI\Link` オブジェクトを返します。利点は、このオブジェクトをたとえばテンプレートへ渡し、描かれる前に `setParameter()` メソッドでそのパラメータをまだ調整できることです。URL そのものは、オブジェクトが文字列に変換されるときにはじめて組み立てられます。 - -```php -$link = $this->lazyLink('Product:show', $id); -// ... -echo $link; // URL はここではじめて生成されます -``` - - -プレゼンターへのリンク -=========== - -リンクの行き先がプレゼンターとアクションなら、次の構文になります。 - -``` -[//] [[[[:]module:]presenter:]action | this] [#fragment] -``` - -この形式は、すべての Latte のタグと、リンクを扱うすべてのプレゼンターのメソッド、つまり `n:href`、`{link}`、`{plink}`、`link()`、`lazyLink()`、`isLinkCurrent()`、`redirect()`、`redirectPermanent()`、`forward()`、`canonicalize()`、そして [#LinkGenerator]で使えます。ですから例で `n:href` を使っていても、そこにはこれらのどの関数が来てもかまいません。 - -基本の形は `Presenter:action` です。 - -```latte -トップページ -``` - -現在のプレゼンターのアクションへリンクするなら、その名前は省けます。 - -```latte -トップページ -``` - -行き先のアクションが `default` なら省けますが、コロンは残さなければなりません。 - -```latte -トップページ -``` - -リンクはほかの[モジュール |directory-structure#プレゼンターとテンプレート]も指せます。ここでは、入れ子のサブモジュールからの相対と、絶対とが区別されます。原理はディスクのパスと同じで、スラッシュの代わりにコロンを使うだけです。現在のプレゼンターが `Front` モジュールの一部だとすると、次のように書きます。 - -```latte -Front:Shop:Product:show へのリンク -Admin:Product:show へのリンク -``` - -特別な場合が[自分自身への |#現在のページへのリンク]リンクで、行き先に `this` を指定します。 - -```latte -更新 -``` - -ハッシュ記号 `#` のあとのいわゆるフラグメントで、ページの特定の部分を指せます。 - -```latte -Home:default とフラグメント #main へのリンク -``` - -.{data-version:3.3.0} -フラグメントは `#` のキーを持つ引数として動的に設定することもできます。その値は自動的にエンコードされ、行き先に指定されたフラグメントより優先されます。 - -```php -$this->link('Home:default', ['#' => $fragment]); -``` - - -絶対パス -==== - -`link()` や `n:href` で生成されるリンクは常に絶対パス(つまり `/` で始まるもの)ですが、`https://domain` のようにプロトコルとドメインを含む絶対 URL ではありません。 - -絶対 URL を生成するには、先頭にスラッシュを 2 つ足します(たとえば `n:href="//Home:"`)。あるいは `$this->absoluteUrls = true` を設定して、プレゼンターが絶対リンクだけを生成するように切り替えられます。 - -テンプレートでは `|absoluteUrl` フィルタを使って、相対パスを絶対パスに変換することもできます。 - - -現在のページへのリンク -=========== - -行き先 `this` は現在のページへのリンクを作ります。 - -```latte -更新 -``` - -同時に、`action()` または(`action()` が定義されていなければ)`render()` メソッドのシグネチャに書かれたすべてのパラメータが引き継がれます。ですから `id: 123` で `Product:show` のページにいるなら、`this` へのリンクもこのパラメータを渡します。 - -もちろん、パラメータを直接指定することもできます。 - -```latte -更新 -``` - -`isLinkCurrent()` 関数は、リンクの行き先が現在のページと同じかどうかを調べます。たとえばテンプレートでリンクを見分けるのに使えます。 - -パラメータは `link()` メソッドと同じですが、具体的なアクションの代わりにワイルドカード `*` も使えます。これは指定したプレゼンターの任意のアクションを意味します。 - -```latte -{if !isLinkCurrent('Admin:login')} - ログイン -{/if} - -
  • - ... -
  • -``` - -ひとつの要素の中で `n:href` と組み合わせる場合は、短い形が使えます。 - -```latte -... -``` - -ワイルドカード `*` はアクションの代わりにだけ使え、プレゼンターの代わりには使えません。 - -特定のモジュールやそのサブモジュールにいるかどうかを判定するには、`isModuleCurrent(moduleName)` メソッドを使います。 - -```latte -
  • - ... -
  • -``` - - -リンクの基準の変更 .{data-version:3.2.7} -=============================== - -既定では、相対リンクは現在のプレゼンターから導かれます。これは `{linkBase}` で変えられます。 - -```latte -{linkBase Admin:Dashboard} -商品の詳細 -``` - -このリンクは `Admin:Dashboard:Product:show` に向かいます。影響を受けるのは相対リンクだけで、コロンで始まる絶対リンクと現在のプレゼンターへのリンク(`this`、`show`)は変わりません。 - -`{linkBase}` はテンプレート全体に適用され、とくにレイアウトのテンプレートで役立ちます。呼び出し元のプレゼンターに関係なく、一貫したリンクを保証してくれるからです。 -このタグはテンプレートの先頭に置かなければならず、さもないと `CompileException` を投げます。 - - -シグナルへのリンク -========= - -リンクの行き先はプレゼンターとアクションだけでなく、[シグナル |components#シグナル]でもかまいません(`handle()` メソッドを呼びます)。その場合の構文は次のとおりです。 - -``` -[//] [sub-component:]signal! [#fragment] -``` - -シグナルは感嘆符で見分けられます。 - -```latte -シグナル -``` - -サブコンポーネント(やそのサブコンポーネント)のシグナルへのリンクも作れます。 - -```latte -シグナル -``` - - -コンポーネントの中のリンク -============= - -[コンポーネント|components]は、周りのプレゼンターと何のつながりも持つべきでない独立した再利用可能な部品なので、ここではリンクの働きが少し違います。Latte の属性 `n:href` とタグ `{link}`、そしてコンポーネントの `link()` などのメソッドは、**リンクの行き先を常にシグナル名とみなします**。ですから感嘆符を付ける必要さえありません。 - -```latte -アクションではなくシグナル -``` - -コンポーネントのテンプレートからプレゼンターへリンクしたい場合は、`{plink}` タグを使います。 - -```latte -トップ -``` - -あるいはコードの中で - -```php -$this->getPresenter()->link('Home:default') -``` - - -別名 .{data-version:3.2.3} -======================== - -Presenter:action の組に、覚えやすい別名を割り当てると便利なことがあります。たとえばトップページの `Front:Home:default` を単に `home`、`Admin:Dashboard:default` を `admin` と呼ぶ、といった具合です。 - -別名は[設定|configuration]の `application › aliases` キーで定義します。 - -```neon -application: - aliases: - home: Front:Home:default - admin: Admin:Dashboard:default - sign: Front:Sign:in -``` - -リンクの中ではアットマークを付けて書きます。たとえば次のようにです。 - -```latte -管理画面 -``` - -`redirect()` など、リンクを扱うすべてのメソッドでも使えます。 - - -不正なリンク -====== - -不正なリンクを作ってしまうことがあります。存在しないプレゼンターを指している、行き先のメソッドのシグネチャが受け取るより多くのパラメータを渡している、あるいは行き先のアクションに対して URL を生成できない場合です。不正なリンクをどう扱うかは、プレゼンターの `$this->invalidLinkMode` で設定します。次の値(定数)の組み合わせを取れます。 - -- `Presenter::InvalidLinkSilent` - 静かなモード。URL として文字 # を返します -- `Presenter::InvalidLinkWarning` - E_USER_WARNING の警告が出ます。本番モードでは記録されますが、スクリプトの実行は止まりません -- `Presenter::InvalidLinkTextual` - 目に見える警告。エラーをリンクに直接書き出します -- `Presenter::InvalidLinkException` - InvalidLinkException を投げます - -既定の設定は、本番モードでは `InvalidLinkWarning`、開発モードでは `InvalidLinkWarning | InvalidLinkTextual` です。本番環境の `InvalidLinkWarning` はスクリプトを止めませんが、警告は記録されます。開発環境では [Tracy |tracy:]がそれを捕まえてブルースクリーンを表示します。`InvalidLinkTextual` は、`#error:` の文字で始まるエラーメッセージを URL として返すことで働きます。そうしたリンクを一目で見つけられるよう、CSS に次を足してください。 - -```css -a[href^="#error:"] { - background: red; - color: white; -} -``` - -開発環境で警告を出したくない場合は、[設定|configuration]で直接抑制できます。 - -```neon -application: - silentLinks: true -``` - - -LinkGenerator -============= - -`link()` メソッドと同じ快適さで、しかしプレゼンターなしでリンクを作るにはどうすればよいでしょうか。そのためにあるのが [api:Nette\Application\LinkGenerator]です。 - -LinkGenerator はサービスで、コンストラクタで受け取り、その `link()` メソッドでリンクを作れます。 - -プレゼンターとの違いがひとつあります。LinkGenerator はすべてのリンクを絶対 URL として直接作ります。さらに「現在のプレゼンター」がないので、行き先にアクション名だけを指定する `link('default')` はできませんし、モジュールへの相対パスも使えません。 - -不正なリンクは常に `Nette\Application\UI\InvalidLinkException` を投げます。 diff --git a/application/ja/directory-structure.texy b/application/ja/directory-structure.texy deleted file mode 100644 index ad5c9b5e19..0000000000 --- a/application/ja/directory-structure.texy +++ /dev/null @@ -1,526 +0,0 @@ -アプリケーションのディレクトリ構成 -***************** - -
    - -Nette Framework のプロジェクトで、見通しがよく拡張しやすいディレクトリ構成をどう設計すればよいでしょうか。コードの整理に役立つ実績ある作法を紹介します。次のことを学びます。 - -- アプリケーションをディレクトリに**論理的に構造化する**方法 -- プロジェクトの成長に**うまく追随する**構成の設計 -- **考えられる代替案**とその長所・短所 - -
    - - -まず大事なこととして、Nette Framework 自体は特定の構成を強制しません。どんな必要や好みにも簡単に合わせられるよう設計されています。 - - -プロジェクトの基本構成 -=========== - -Nette Framework は決まったディレクトリ構成を押し付けませんが、[Web Project|https://github.com/nette/web-project]という形で実績のある既定の配置があります。 - -/--pre -web-project/ -├── app/ ← アプリケーションのディレクトリ -├── assets/ ← SCSS、JS ファイル、画像など。あるいは resources/ -├── bin/ ← コマンドライン用のスクリプト -├── config/ ← 設定 -├── log/ ← 記録されたエラー -├── temp/ ← 一時ファイル、キャッシュ -├── tests/ ← テスト -├── vendor/ ← Composer がインストールしたライブラリ -└── www/ ← 公開ディレクトリ(document-root) -\-- - -この構成は必要に応じて自由に変えられます。フォルダの名前を変えたり移動したりできます。あとは `Bootstrap.php` と、必要なら `composer.json` のディレクトリへの相対パスを調整するだけです。それ以上は何も要りません。複雑な設定のやり直しも、定数の変更も不要です。Nette には賢い自動検出があり、アプリケーションの場所とその基準 URL を自動的に認識します。 - - -コードを整理する原則 -========== - -新しいプロジェクトをはじめて見るとき、素早く見当が付くべきです。`app/Model/` ディレクトリをクリックして、次の構成が現れたと想像してください。 - -/--pre -app/Model/ -├── Services/ -├── Repositories/ -└── Entities/ -\-- - -ここから分かるのは、このプロジェクトが何らかのサービス、リポジトリ、エンティティを使っているということだけです。アプリケーションが実際に何をするのかは何も分かりません。 - -別のやり方、**ドメインによる整理**を見てみましょう。 - -/--pre -app/Model/ -├── Cart/ -├── Payment/ -├── Order/ -└── Product/ -\-- - -こちらは違います。一目でネットショップだと分かります。ディレクトリの名前そのものが、アプリケーションに何ができるかを教えてくれます。支払い、注文、商品を扱うのだ、と。 - -ひとつめのやり方(クラスの種類による整理)は、実務でいくつかの問題を生みます。論理的に関係するコードが別々のフォルダに散らばり、そのあいだを行き来しなければなりません。ですからドメインで整理します。 - - -名前空間 ----- - -ディレクトリ構成をアプリケーションの名前空間に対応させるのが慣習です。つまりファイルの物理的な場所が、その名前空間と一致するということです。たとえば `app/Model/Product/ProductRepository.php` にあるクラスは、名前空間 `App\Model\Product` を持つべきです。この原則はコードを辿るのを助け、オートローディングを単純にします。 - - -名前の単数形と複数形 ----------- - -アプリケーションの主要なディレクトリには単数形を使っていることに注目してください。`app`、`config`、`log`、`temp`、`www` です。アプリケーションの中でも同じで、`Model`、`Core`、`Presentation` です。それぞれがひとつのまとまった概念を表しているからです。 - -同じく `app/Model/Product` は、商品に関わるすべてを表します。`Products` と呼ばないのは、商品が詰まったフォルダ(それなら `nokia.php`、`samsung.php` のようなファイルが入るでしょう)ではないからです。商品を扱うクラス、`ProductRepository.php`、`ProductService.php` を含む名前空間なのです。 - -`app/Tasks` フォルダが複数形なのは、独立した実行可能なスクリプトの集まり、`CleanupTask.php`、`ImportTask.php` を含むからです。そのひとつひとつが独立した単位です。 - -一貫性のために、次をおすすめします。 -- 機能のまとまりを表す名前空間には単数形(複数のエンティティを扱う場合でも) -- 独立した単位の集まりには複数形 -- 迷ったとき、あるいは考えたくないときは単数形 - - -公開ディレクトリ `www/` -=============== - -このディレクトリだけがウェブからアクセスできます(document-root です)。`www/` の代わりに `public/` という名前もよく見かけますが、これは慣習の問題にすぎず、アプリケーションの動作には影響しません。このディレクトリには次のものが入ります。 -- アプリケーションの[入口 |bootstrapping#index.php] `index.php` -- mod_rewrite の規則を含む `.htaccess` ファイル(Apache 用) -- 静的ファイル(CSS、JavaScript、画像) -- アップロードされたファイル - -アプリケーションを適切に守るには、[document-root が正しく設定されている |nette:troubleshooting#URL から www のディレクトリを変えたり取り除いたりするには]ことが決定的に重要です。 - -.[note] -`node_modules/` フォルダをこのディレクトリに置いては決していけません。実行可能かもしれない何千ものファイルが入っていて、公開すべきではありません。 - - -アプリケーションのディレクトリ `app/` -====================== - -アプリケーションのコードを含む主要なディレクトリです。基本の構成は次のとおりです。 - -/--pre -app/ -├── Core/ ← インフラに関わる事柄 -├── Model/ ← ビジネスロジック -├── Presentation/ ← プレゼンターとテンプレート -├── Tasks/ ← コマンドのスクリプト -└── Bootstrap.php ← アプリケーションの起動クラス -\-- - -`Bootstrap.php` は[アプリケーションの起動クラス|bootstrapping]で、環境を初期化し、設定を読み込み、DI コンテナを作ります。 - -では個々のサブディレクトリを詳しく見ていきましょう。 - - -プレゼンターとテンプレート -============= - -アプリケーションのプレゼンテーション層は `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` 名前空間にあります([#プレゼンターのマッピング]をご覧ください)。 - -```php -namespace App\Presentation\Admin\Dashboard; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -`Admin` モジュールの中の `Dashboard` プレゼンターは、アプリケーションの中でコロン記法を使って `Admin:Dashboard` と参照します。その `default` アクションは `Admin:Dashboard:default` です。入れ子のモジュールでは、コロンを複数使います。たとえば `Shop:Order:Detail:default` です。 - - -構成の柔軟な発展 --------- - -この構成の大きな利点のひとつが、プロジェクトの膨らむ要求に優雅に追随できることです。例として XML のフィードを生成する部分を取り上げましょう。最初は単純な形です。 - -/--pre -Export/ -├── ExportPresenter.php ← すべてのエクスポート用のプレゼンター 1 つ -├── 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` などのプレゼンター(のフォルダ)を入れられます。 - - -テンプレートの置き場 ----------- - -これまでの例では、テンプレートがプレゼンターと同じフォルダに置かれていました。 - -/--pre -Dashboard/ -├── DashboardPresenter.php ← プレゼンター -├── DashboardTemplate.php ← 任意のテンプレートクラス -└── default.latte ← テンプレート -\-- - -実務ではこの置き場が最も便利だと分かっています。関係するファイルがすべてすぐ手元にあるからです。 - -あるいはテンプレートを `templates/` のサブフォルダに置くこともできます。Nette はどちらの形にも対応しています。テンプレートを `Presentation/` フォルダの完全に外に置くことさえできます。テンプレートの置き場の選択肢については、[テンプレートの探索 |templates#テンプレートの探索]の章にすべてあります。 - - -補助クラスとコンポーネント -------------- - -プレゼンターとテンプレートには、ほかの補助ファイルが伴うことがよくあります。それらは適用範囲に応じて論理的に置きます。 - -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` に。選び方はチームの慣習によります。 - - -Model - アプリケーションの心臓 -=================== - -モデルには、アプリケーションのビジネスロジックがすべて入ります。その整理の規則もやはり、ドメインによる構造化です。 - -/--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 -// '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, - ]); - } -} -``` - -**値オブジェクト**: 自分の同一性を持たない値、たとえば金額やメールアドレスを表す不変のオブジェクトです。同じ値を持つ 2 つの値オブジェクトのインスタンスは、同一とみなされます。 - - -インフラのコード -======== - -`Core/` フォルダ(あるいは `Infrastructure/`)は、アプリケーションの技術的な土台の置き場です。インフラのコードにはふつう次のものが含まれます。 - -/--pre -app/Core/ -├── Router/ ← ルーティングと URL の管理 -│ └── RouterFactory.php -├── Security/ ← 認証と認可 -│ ├── Authenticator.php -│ └── Authorizator.php -├── Logging/ ← ログ記録と監視 -│ ├── SentryLogger.php -│ └── FileLogger.php -├── Cache/ ← キャッシュの層 -│ └── FullPageCache.php -└── Integration/ ← 外部サービスとの統合 - ├── Slack/ - └── Stripe/ -\-- - -小さなプロジェクトなら、当然ながら平らな構成で十分です。 - -/--pre -Core/ -├── RouterFactory.php -├── Authenticator.php -└── QueueMailer.php -\-- - -ここに入るのは次のようなコードです。 - -- 技術的なインフラ(ルーティング、ログ記録、キャッシュ)を扱う -- 外部サービス(Sentry、Elasticsearch、Redis)と統合する -- アプリケーション全体に基本的なサービス(メール、データベース)を提供する -- 特定のドメインからはおおむね独立している。キャッシュやロガーは、ネットショップでもブログでも同じように働きます。 - -あるクラスがここに属するのか、モデルに属するのか迷いますか。決定的な違いは、`Core/` のコードが次の性質を持つことです。 - -- ドメイン(商品、注文、記事)について何も知らない -- たいてい別のプロジェクトに持っていける -- 「どう動くか」(メールをどう送るか)を解き、「何をするか」(どのメールを送るか)は解かない - -分かりやすくするための例です。 - -- `App\Core\MailerFactory` - メールを送るクラスのインスタンスを作り、SMTP の設定を扱います -- `App\Model\OrderMailer` - `MailerFactory` を使って注文についてのメールを送り、そのテンプレートといつ送るべきかを知っています - - -コマンドのスクリプト -========== - -アプリケーションは、通常の HTTP リクエストの外で処理を行う必要にしばしば迫られます。バックグラウンドのデータ処理、メンテナンス、定期的な仕事などです。実行には `bin/` ディレクトリの単純なスクリプトを使い、実装のロジック自体は `app/Tasks/`(あるいは `app/Commands/`)に置きます。 - -例を挙げます。 - -/--pre -app/Tasks/ -├── Maintenance/ ← メンテナンスのスクリプト -│ ├── CleanupCommand.php ← 古いデータの削除 -│ └── DbOptimizeCommand.php ← データベースの最適化 -├── Integration/ ← 外部システムとの統合 -│ ├── ImportProducts.php ← 仕入先システムからの取り込み -│ └── SyncOrders.php ← 注文の同期 -└── Scheduled/ ← 定期的な仕事 - ├── NewsletterCommand.php ← ニュースレターの送信 - └── ReminderCommand.php ← 顧客への通知 -\-- - -何がモデルに属し、何がコマンドのスクリプトに属するのでしょうか。たとえばメールを 1 通送るロジックはモデルの一部で、何千通ものメールの一括送信は `Tasks/` に属します。 - -タスクはふつうコマンドラインか cron から実行します。`bin/` のスクリプトが [bootConsoleApplication() |bootstrapping#さまざまな環境]メソッドで DI コンテナを作り、そこから必要なサービスを取り出します。HTTP のリクエストから実行することもできますが、安全性を考える必要があります。タスクを実行するプレゼンターは、たとえばログイン済みのユーザーだけに限る、あるいは強力なトークンと許可された IP アドレスからのアクセスに限る、といった形で守らなければなりません。長時間かかるタスクでは、スクリプトの時間制限を延ばし、セッションのロックを避けるために `session_write_close()` を使う必要があります。 - - -そのほかの考えられるディレクトリ -================ - -ここまで挙げた基本のディレクトリのほかにも、プロジェクトの必要に応じて専用のフォルダを足せます。よくあるものとその用途を見てみましょう。 - -/--pre -app/ -├── Api/ ← プレゼンテーション層から独立した API のロジック -├── Database/ ← マイグレーションのスクリプトとテストデータの seeder -├── 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/ ← メールのテンプレート -│ ├── order-confirmation.latte -│ └── welcome.latte -└── OrderMailer.php -\-- - - -プレゼンターのマッピング -============ - -マッピングは、プレゼンター名からクラス名を導く規則を定めます。[設定|configuration]の `application › mapping` キーで指定します。 - -このページでは、プレゼンターを `app/Presentation` フォルダ(または `app/UI`)に置いてきました。Nette Application 3.3 以降、これは設定しなくてよい既定の慣習です。別の構成を使う場合や、マッピングを明示的に指定したい場合、既定の設定は次の行に相当します。 - -```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.3 以上が必要です)。 - -```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` になります。 - -別の書き方として、文字列の代わりに 3 つの要素から成る配列を使えます。上に示した例について、この書き方は前のものと等価です。 - -```neon -application: - mapping: - *: [App\Presentation, *, **Presenter] - Api: [App\Api, '', *Presenter] -``` diff --git a/application/ja/how-it-works.texy b/application/ja/how-it-works.texy deleted file mode 100644 index bd9fcbaf5e..0000000000 --- a/application/ja/how-it-works.texy +++ /dev/null @@ -1,200 +0,0 @@ -アプリケーションはどう動くのか -*************** - -
    - -あなたは今、Nette ドキュメントの土台となる章を読んでいます。リクエストが生まれた瞬間から PHP スクリプトの実行が終わるまで、ウェブアプリケーションがどう動くのかを A から Z まで学びます。読み終わると次のことが分かります。 - -- 全体がどう動くのか -- Bootstrap、プレゼンター、DI コンテナとは何か -- ディレクトリ構成はどんな形か - -
    - - -ディレクトリ構成 -======== - -[WebProject|https://github.com/nette/web-project]というウェブアプリケーションの雛形の例を開いてください。読みながら、話に出てくるファイルを参照できます。 - -ディレクトリ構成はおおよそ次のようになっています。 - -/--pre -web-project/ -├── app/ ← アプリケーションのディレクトリ -│ ├── Core/ ← 動作に必要な中核のクラス -│ │ └── RouterFactory.php ← URL アドレスの設定 -│ ├── Presentation/ ← プレゼンター、テンプレートなど -│ │ ├── @layout.latte ← レイアウトのテンプレート -│ │ └── Home/ ← Home プレゼンターのディレクトリ -│ │ ├── HomePresenter.php ← Home プレゼンターのクラス -│ │ └── default.latte ← default アクションのテンプレート -│ └── Bootstrap.php ← 起動クラス Bootstrap -├── assets/ ← リソース(SCSS、TypeScript、元画像) -├── bin/ ← コマンドラインから実行するスクリプト -├── config/ ← 設定ファイル -│ ├── common.neon -│ └── services.neon -├── log/ ← 記録されたエラー -├── temp/ ← 一時ファイル、キャッシュなど -├── vendor/ ← Composer がインストールしたライブラリ -│ ├── ... -│ └── autoload.php ← インストールされた全パッケージのオートローディング -├── www/ ← 公開ディレクトリ。プロジェクトの document-root -│ ├── assets/ ← コンパイル済みの静的ファイル(CSS、JS、画像など) -│ ├── .htaccess ← mod_rewrite の規則 -│ └── index.php ← アプリケーションを起動する最初のファイル -└── .htaccess ← www 以外のすべてのディレクトリへのアクセスを禁じます -\-- - -ディレクトリ構成は好きなように変えられ、フォルダの名前を変えたり移動したりできます。完全に柔軟です。Nette には賢い自動検出もあり、アプリケーションの場所とその URL の基準を自動的に認識します。 - -少し大きなアプリケーションでは、プレゼンターとテンプレートのフォルダを[サブディレクトリ |directory-structure#プレゼンターとテンプレート]にまとめ、クラスを名前空間でまとめられます。これをモジュールと呼びます。 - -`www/` ディレクトリは、プロジェクトの公開ディレクトリ、つまり document-root にあたります。アプリケーション側でほかに何も設定せずに名前を変えられます。document-root がこのディレクトリを指すよう[ホスティングを設定する |nette:troubleshooting#URL から www のディレクトリを変えたり取り除いたりするには]だけです。 - -WebProject は Nette ごと [Composer |best-practices:composer]で直接ダウンロードすることもできます。 - -```shell -composer create-project nette/web-project -``` - -Linux や macOS では、`log/` と `temp/` ディレクトリに[書き込みの権限 |nette:troubleshooting#ディレクトリの権限の設定]を設定してください。 - -WebProject アプリケーションはすぐに動く状態です。何も設定する必要はなく、`www/` フォルダにアクセスすればブラウザでそのまま見られます。 - - -HTTP リクエスト -========== - -すべては、ユーザーがブラウザでページを開いたときに始まります。ブラウザはサーバーに HTTP リクエストを送ります。このリクエストは、公開ディレクトリ `www/` にあるただひとつの PHP ファイル、`index.php` に向かいます。リクエストがアドレス `https://example.com/product/123` に対するものだとしましょう。適切な[サーバーの設定 |nette:troubleshooting#きれいな URL のためにサーバーを設定するには]のおかげで、この URL も `index.php` ファイルに割り当てられ、それが実行されます。 - -その役目は次のとおりです。 - -1) 環境を初期化する -2) ファクトリを手に入れる -3) リクエストを処理する Nette アプリケーションを実行する - -ファクトリ? 私たちはトラクターを作っているのではなく、ウェブサイトを作っているのですが。ご心配なく、すぐに説明します。 - -「環境の初期化」とは、たとえば [Tracy|tracy:]を有効にすることです。Tracy はエラーの記録と可視化のための素晴らしい道具です。本番サーバーではエラーを記録し、開発環境では直接表示します。ですから初期化には、サイトが本番モードで動いているのか開発モードで動いているのかの判定も含まれます。Nette はそのために[賢い自動検出 |bootstrapping#開発モードと本番モード]を使います。localhost でサイトを動かせば開発モードになります。何も設定する必要はなく、アプリケーションは開発にも本番の公開にもすぐ対応できます。これらの手順は [Bootstrap クラス|bootstrapping]の章で実行され、詳しく説明されています。 - -3 つめ(2 つめは飛ばしましたが、あとで戻ります)はアプリケーションの実行です。Nette では HTTP リクエストの処理を `Nette\Application\Application` クラス(以下 `Application`)が担当します。ですからアプリケーションを実行するとは、具体的にはこのクラスのオブジェクトで、その名も `run()` というメソッドを呼ぶことを指します。 - -Nette は指導役として、実績のある方法論に沿ってきれいなアプリケーションを書くよう導いてくれます。その中でも最も確立されたもののひとつが**依存性注入**、略して DI です。ここで DI の説明で負担をかけるつもりはありません。それには[専用の章|dependency-injection:introduction]があります。肝心の帰結は、重要なオブジェクトがふつう **DI コンテナ**(DIC)と呼ばれるオブジェクトのファクトリによって作られるということです。そう、これが先ほど触れたファクトリです。`Application` オブジェクトもここが作ってくれるので、まずコンテナが必要になります。コンテナは `Configurator` クラスで手に入れ、`Application` オブジェクトを作らせ、その `run()` メソッドを呼ぶ。こうして Nette のアプリケーションが始まります。これがまさに [index.php |bootstrapping#index.php]ファイルで起きていることです。 - - -Nette Application -================= - -`Application` クラスの役目はひとつだけです。HTTP リクエストに応えることです。 - -Nette で書かれたアプリケーションは、いわゆるプレゼンターの集まりに分かれています(ほかのフレームワークでは「コントローラ」という言葉に出会うかもしれませんが、本質的には同じものです)。プレゼンターはクラスで、それぞれがウェブサイトの特定のページを表します。トップページ、ネットショップの商品、ログインフォーム、サイトマップのフィードなどです。アプリケーションはプレゼンターをひとつから何千まで持てます。 - -`Application` はまず、いわゆるルーターに、現在のリクエストをどのプレゼンターが処理すべきかを決めさせます。ルーターが責任の所在を定めるのです。入力の URL `https://example.com/product/123` を調べ、その設定にもとづいて、この仕事はたとえば `Product` **プレゼンター**のもので、`id: 123` の商品について `show` **アクション**を行うべきだと決めます。プレゼンターとアクションの組はコロンで区切って `Product:show` と書くのが良い習慣です。 - -こうしてルーターは URL を `Presenter:action` の組とパラメータに変換しました。この場合は `Product:show` と `id: 123` です。そうしたルーターがどんな形かは `app/Core/RouterFactory.php` ファイルで見られますし、[ルーティング |Routing]の章で詳しく説明します。 - -先へ進みましょう。`Application` はプレゼンターの名前を知ったので、次に進めます。`Product` プレゼンターのコードを含む `ProductPresenter` クラスのインスタンスを作るのです。より正確には、オブジェクトの生成はその役目なので、DI コンテナにプレゼンターを作らせます。 - -プレゼンターはたとえば次のような形になります。 - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ProductRepository $repository, - ) { - } - - public function renderShow(int $id): void - { - // モデルからデータを取得してテンプレートに渡します - $this->template->product = $this->repository->getProduct($id); - } -} -``` - -プレゼンターがリクエストの処理を引き継ぎます。仕事ははっきりしています。`id: 123` で `show` アクションを実行することです。プレゼンターの用語でいえば、`renderShow()` メソッドが呼ばれ、`$id` パラメータに `123` を受け取る、ということです。 - -プレゼンターは複数のアクションを扱えます。つまり `render()` メソッドを複数持てます。とはいえ、プレゼンターはアクションひとつ、あるいはできるだけ少ない数で設計することをおすすめします。 - -というわけで `renderShow(123)` メソッドが呼ばれました。そのコードは架空の例ですが、`$this->template` に書き込むことでデータがテンプレートに渡される様子を示しています。 - -続いてプレゼンターはレスポンスを返します。HTML のページ、画像、XML のドキュメント、ディスクからのファイルの送信、JSON、あるいは別のページへのリダイレクトかもしれません。大事なのは、どう応えるかを明示的に指定しなければ(`ProductPresenter` の場合がそうです)、テンプレートを HTML のページに描くのがレスポンスになる、という点です。なぜでしょうか。99 % の場合、私たちはテンプレートを描きたいからです。ですからプレゼンターは、私たちの手間を省くためにこの振る舞いを既定としています。それが Nette の本質です。 - -描くテンプレートを指定する必要さえありません。フレームワークがパスを自動的に導きます。`show` アクションの場合は単に、`ProductPresenter` クラスと同じディレクトリにある `show.latte` テンプレートを読み込もうとします。レイアウトも `@layout.latte` ファイルから探そうとします(詳しくは[テンプレートの探索 |templates#テンプレートの探索]をご覧ください)。 - -そしてテンプレートが描かれます。これでプレゼンターの、そしてアプリケーション全体の仕事が終わります。テンプレートが存在しなければ 404 のエラーページが返ります。プレゼンターについて詳しくは[プレゼンター|presenters]のページで学べます。 - -[* request-flow.svg *] - -念のため、少し違う URL で全体の流れをおさらいしましょう。 - -1) URL は `https://example.com` -2) アプリケーションが起動し、DI コンテナが作られ、`Application::run()` が実行されます。 -3) ルーターが URL を `Home:default` の組に読み解きます。 -4) `HomePresenter` クラスのインスタンスが作られます。 -5) (存在すれば)`renderDefault()` メソッドが呼ばれます。 -6) テンプレート、たとえば `default.latte` が、レイアウト、たとえば `@layout.latte` とともに描かれます。 - - -ここまでで新しい概念にたくさん出会ったかもしれませんが、どれも筋が通っていると感じてもらえたはずです。Nette でのアプリケーション開発は、驚くほど素直です。 - - -テンプレート -====== - -テンプレートといえば、Nette は [Latte |latte:]というテンプレートシステムを使います。テンプレートのファイルの拡張子が `.latte` なのはそのためです。Latte を使う第一の理由は、PHP で最も安全なテンプレートシステムであり、しかも最も直感的だからです。新しく覚えることは多くありません。PHP の知識といくつかのタグで十分です。必要なことはすべて[ドキュメント |templates]にあります。 - -テンプレートでは、ほかのプレゼンターやアクションへの[リンクを作ります |creating-links]。次のようにです。 - -```latte -商品の詳細 -``` - -実際の URL の代わりに、見慣れた `Presenter:action` の組を書き、必要なパラメータを添えるだけです。仕掛けは `n:href` にあり、これがこの属性を処理するよう Nette に伝えます。すると次が生成されます。 - -```latte -商品の詳細 -``` - -URL の生成は、先ほどのルーターが担当します。Nette のルーターが並外れているのは、URL から `Presenter:action` の組への変換だけでなく、その逆、つまりプレゼンター名、アクション、パラメータから URL を生成することもできる点です。おかげで、完成した Nette のアプリケーション全体の URL の形式を、テンプレートやプレゼンターの文字を 1 つも変えずに、ルーターを直すだけで丸ごと変えられます。これはいわゆる正規化も可能にします。同じ内容が異なる URL に存在するのを自動的に防いで SEO を高める、Nette ならではのもうひとつの機能です。多くのプログラマーがこの能力に驚きます。 - - -インタラクティブなコンポーネント -================ - -プレゼンターについてもうひとつお伝えすることがあります。プレゼンターには組み込みのコンポーネントのしくみがあります。経験のある方は Delphi や ASP.NET Web Forms の似たものを思い出すかもしれません。React や Vue.js も、いくらか近い考え方の上に築かれています。PHP のフレームワークの世界では、これはまったく他にない機能です。 - -コンポーネントは独立した再利用できる部品で、ページ(つまりプレゼンター)に埋め込みます。[フォーム |forms:in-presenter]、[データグリッド |https://componette.org/contributte/datagrid/]、メニュー、アンケートなど、再利用する意味のあるものなら何でもです。自分でコンポーネントを作ることも、[豊富な品揃え |https://componette.org]のオープンソースのコンポーネントを使うこともできます。 - -コンポーネントはアプリケーション開発への向き合い方を根本から変えます。あらかじめ用意された部品からページを組み立てるという新しい可能性を開きます。そして[ハリウッド |components#ハリウッド流]とも通じるものがあります。 - - -DI コンテナと設定 -========== - -DI コンテナ、つまりオブジェクトのファクトリは、アプリケーション全体の心臓です。 - -ご心配なく。これまでの行から想像されるような魔法のブラックボックスではありません。実際には、Nette が生成してキャッシュディレクトリに保存する、かなり地味な PHP のクラスです。`createServiceAbcd()` のような名前のメソッドが数多く入っていて、それぞれが特定のオブジェクトを作って返せます。そう、`index.php` でアプリケーションを実行するのに必要だった `Nette\Application\Application` のインスタンスを作る `createServiceApplication__application()` メソッドもあります。個々のプレゼンターを作るメソッドなどもあります。 - -DI コンテナが作るオブジェクトは、なぜかサービスと呼ばれます。 - -このクラスが本当に特別なのは、あなたがそれをプログラムしないという点です。フレームワークがするのです。実際に PHP のコードを生成してディスクに保存します。あなたは、コンテナがどのオブジェクトを作れるべきか、そしてそれを正確にどう作るかの指示を与えるだけです。この指示は[設定ファイル |bootstrapping#DI コンテナの設定]に書きます。設定ファイルは [NEON|neon:format]形式を使うので、拡張子は `.neon` です。 - -設定ファイルは、純粋に DI コンテナへ指示を与えるためのものです。ですからたとえば [session |http:configuration#セッション]セクションで `expiration: 14 days` を指定すると、DI コンテナはセッションを表す `Nette\Http\Session` オブジェクトを作るときに `setExpiration('14 days')` メソッドを呼び、その設定を実現します。 - -何を[設定できるか |nette:configuring]、そして[独自のサービスをどう定義するか |dependency-injection:services]を説明する章がまるごと用意されています。 - -サービスの生成に少し踏み込むと、[オートワイヤリング |dependency-injection:autowiring]という言葉に出会います。これはあなたの生活を驚くほど楽にしてくれる機能です。あなたが何もしなくても、必要な場所(たとえばクラスのコンストラクタ)にオブジェクトを自動的に渡してくれます。Nette の DI コンテナが小さな奇跡であることに気づくでしょう。 - - -次は何を? -===== - -Nette のアプリケーションの基本原則を見てきました。ここまでは表面をなぞる概観でしたが、まもなくもっと深く踏み込み、やがては素晴らしいウェブアプリケーションを作れるようになります。次はどこへ行きましょうか。[最初のアプリケーションを作ろう|quickstart:]のチュートリアルはもう試しましたか。 - -ここまで説明したもののほかにも、Nette には[便利なクラス|utils:]の一式や[データベース層|database:]などがあります。ドキュメントをあちこちクリックしてみてください。あるいは[ブログ|https://blog.nette.org]を訪ねてみてください。面白いものがたくさん見つかります。 - -このフレームワークがあなたに大きな喜びをもたらしますように 💙 diff --git a/application/ja/multiplier.texy b/application/ja/multiplier.texy deleted file mode 100644 index 40d0258078..0000000000 --- a/application/ja/multiplier.texy +++ /dev/null @@ -1,63 +0,0 @@ -Multiplier: 動的なコンポーネント -********************** - -.[perex] -インタラクティブなコンポーネントを動的に作るための道具です。 - -典型的な例から始めましょう。ネットショップの商品一覧で、各商品に「カートに追加」のフォームを付けたいとします。ひとつのやり方は、一覧全体をひとつのフォームで包むことです。しかし [api:Nette\Application\UI\Multiplier]は、はるかに便利な方法を提供します。 - -Multiplier を使うと、複数のコンポーネントのファクトリを手軽に定義できます。原理は入れ子のコンポーネントで、[api:Nette\ComponentModel\Container]を継承したコンポーネントは、ほかのコンポーネントを含められます。 - -.[tip] -ドキュメントの[コンポーネントモデル |components#コンポーネントの詳細]の章をご覧ください。 - -Multiplier の本質は、コンストラクタに渡されたコールバックを使って子を動的に作れる親として働くことです。例を見てみましょう。 - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function () { - $form = new Nette\Application\UI\Form; - $form->addInteger('amount', '数量:') - ->setRequired(); - $form->addSubmit('send', 'カートに追加'); - return $form; - }); -} -``` - -これでテンプレートでは、商品ごとにフォームをそのまま描けます。しかもそのひとつひとつが本当に別々のコンポーネントになります。 - -```latte -{foreach $items as $item} -

    {$item->title}

    - {$item->description} - - {control "shopForm-$item->id"} -{/foreach} -``` - -`{control}` タグに渡した引数は、次の意味を持つ形式に従っています。 - -1. コンポーネント `shopForm` を取得する。 -2. そこから `$item->id` という名前の子を取得する。 - -**1** の最初の呼び出しでは `shopForm` コンポーネントがまだ存在しないので、そのファクトリ `createComponentShopForm` が呼ばれます。次に、得られたコンポーネント(Multiplier のインスタンス)に対して、個別のフォームのファクトリが呼ばれます。それが Multiplier のコンストラクタに渡した無名関数です。 - -foreach ループの次の反復では、`createComponentShopForm` メソッドはもう呼ばれません(コンポーネントがすでに存在するからです)。しかし探している子が違う(反復ごとに `$item->id` が違う)ので、無名関数がもう一度呼ばれ、新しいフォームを返します。 - -あとはフォームが正しい商品をカートに追加するようにするだけです。今のところフォームはどの商品でも同じだからです。ここで Multiplier の(そして一般に Nette Framework のどのコンポーネントファクトリの)性質が役立ちます。どのファクトリも、作られるコンポーネントの名前を第 1 引数として受け取ります。さらに Multiplier のファクトリは、第 2 引数として Multiplier のインスタンス自身を受け取ります。この場合、第 1 引数は `$item->id` であり、まさに必要な情報です。ですからフォームの生成を少し書き換えるだけで済みます。 - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function (string $itemId) { - $form = new Nette\Application\UI\Form; - $form->addInteger('amount', '数量:') - ->setRequired(); - $form->addHidden('itemId', $itemId); - $form->addSubmit('send', 'カートに追加'); - return $form; - }); -} -``` diff --git a/application/ja/presenters.texy b/application/ja/presenters.texy deleted file mode 100644 index f5f32a61aa..0000000000 --- a/application/ja/presenters.texy +++ /dev/null @@ -1,595 +0,0 @@ -プレゼンター -****** - -
    - -Nette でプレゼンターとテンプレートをどう書くのかを見ていきます。読み終えると次のことが分かります。 - -- プレゼンターがどう動くのか -- 永続パラメータとは何か -- テンプレートがどう描かれるのか - -
    - -プレゼンターがウェブアプリケーションの特定のページ、たとえばトップページ、ネットショップの商品、ログインフォーム、サイトマップのフィードなどを表すクラスであることは、[すでに見てきました |how-it-works#Nette Application]。アプリケーションはプレゼンターをひとつから何千まで持てます。ほかのフレームワークではコントローラとも呼ばれます。 - -ふつうプレゼンターという語は、ウェブのインターフェースを生成するのに向いた [api:Nette\Application\UI\Presenter]クラスの子孫を指し、この章の以降もそれを中心に扱います。より一般的な意味では、プレゼンターとは [api:Nette\Application\IPresenter]インターフェースを実装した任意のオブジェクトです。 - - -プレゼンターのライフサイクル -============== - -プレゼンターの役目は、リクエストを処理してレスポンス(HTML のページ、画像、リダイレクトなど)を返すことです。 - -ですからまずリクエストが渡されます。これは直接の HTTP リクエストではなく、ルーターの助けを借りて HTTP リクエストが変換された [api:Nette\Application\Request]オブジェクトです。このオブジェクトを直接扱うことはふつうありません。プレゼンターがリクエストの処理をほかのメソッドに巧みに委ねるからです。それをこれから見ていきます。 - -[* lifecycle.svg *] *** プレゼンターのライフサイクル .<> - -図は、存在すれば上から下へ順に呼ばれるメソッドの一覧を示しています。どれも必須ではありません。メソッドがひとつもない、まったく空のプレゼンターを作り、その上に単純な静的サイトを築くこともできます。 - - -`__construct()` ---------------- - -コンストラクタは、オブジェクトが作られる瞬間に呼ばれるので、厳密にはプレゼンターのライフサイクルには属しません。それでもその重要さゆえに触れておきます。コンストラクタは([inject メソッド|best-practices:inject-method-attribute]とともに)依存関係を渡すために使います。 - -プレゼンターは、アプリケーションのビジネスロジックを扱ったり、データベースに読み書きしたり、計算したりすべきではありません。それはモデルと呼ばれる層のクラスの責務です。たとえば `ArticleRepository` クラスが記事の読み込みと保存を担うでしょう。プレゼンターがそれを使うには、[依存性注入で渡してもらう |dependency-injection:passing-dependencies]必要があります。 - - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articles, - ) { - } -} -``` - - -`startup()` ------------ - -リクエストを受け取った直後に `startup()` メソッドが呼ばれます。プロパティの初期化やユーザーの権限の確認などに使えます。このメソッドは必ず親を呼ぶ必要があります: `parent::startup()`。 - - -`action(args...)` .{toc: action()} --------------------------------------------------- - -`render()` メソッドと似ています。`render()` がこのあと描かれる特定のテンプレートのためにデータを用意するものであるのに対し、`action()` はリクエストを処理するもので、そのあとにテンプレートを描くとは限りません。たとえばデータを処理したり、ユーザーをログイン・ログアウトさせたりしてから、[別の場所へリダイレクト |#リダイレクト]することもあります。 - -大事なのは、`action()` が `render()` より*先に*呼ばれることです。おかげで、アクションのメソッドの中でリクエストの流れを変えられます。たとえば `setView('otherView')` を使って、描かれるテンプレートや、呼ばれる `render()` メソッドさえ変えられます。 - -.{data-version:3.2.3} -`switch('otherAction')` メソッドを使えば、まったく別のアクションに切り替えることもできます。現在のメソッドを中断し、代わりに新しいアクションの `action()` と `render()` メソッドを実行します(そして自動的な[正規化|#正規化]を無効にします)。リクエスト自体は続き、いま走っているメソッドだけが中断されます。 - -リクエストのパラメータがメソッドに渡されます。これらのパラメータには型を指定でき、そうすることをおすすめします。たとえば `actionShow(int $id, ?string $slug = null)` です。`id` パラメータがない、あるいは整数でない場合、プレゼンターは [404 エラー |#404 などのエラー]を返して終わります。 - - -`handle(args...)` .{toc: handle()} --------------------------------------------------- - -このメソッドは、いわゆるシグナルを処理します。シグナルについては[コンポーネント |components#シグナル]の章で学びます。主にコンポーネントと AJAX リクエストの処理のためのものです。 - -`action()` と同じく、型チェックも含めてリクエストのパラメータがメソッドに渡されます。 - - -`beforeRender()` ----------------- - -`beforeRender` メソッドは、その名のとおり、すべての `render()` メソッドの前に呼ばれます。テンプレートの共通の設定、レイアウトへの変数の受け渡しなどに使います。 - - -`render(args...)` .{toc: render()} ----------------------------------------------- - -ここでは、このあと描かれるテンプレートを準備し、データを渡したりします。 - -`action()` と同じく、型チェックも含めてリクエストのパラメータがメソッドに渡されます。 - -```php -public function renderShow(int $id): void -{ - // モデルからデータを取得してテンプレートに渡します - $this->template->article = $this->articles->getById($id); -} -``` - - -`afterRender()` ---------------- - -`afterRender` メソッドは、これもまた名のとおり、すべての `render()` メソッドのあとに呼ばれます。使われることはむしろ稀です。 - - -`shutdown()` ------------- - -プレゼンターのライフサイクルの終わりに呼ばれます。 - - -イベント ----- - -プレゼンターのライフサイクルの一部として呼ばれる `startup()`、`beforeRender()`、`shutdown()` メソッドのほかに、自動的に呼ばれる関数を定義できます。プレゼンターはいわゆる[イベント |nette:glossary#イベント]を定義していて、そのハンドラを `$onStartup`、`$onRender`、`$onShutdown` の配列に足します。 - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct() - { - $this->onStartup[] = function () { - // ... - }; - } -} -``` - -`$onStartup` 配列のハンドラは `startup()` メソッドの直前に、`$onRender` のハンドラは `beforeRender()` と `render()` のあいだに、そして `$onShutdown` のハンドラは `shutdown()` の直前に呼ばれます。 - - -**先へ進む前にひとつ助言を。** ご覧のとおり、プレゼンターは複数のアクション/ビューを扱えます。つまり `render()` メソッドを複数持てます。とはいえ、プレゼンターはアクションひとつ、あるいはできるだけ少ない数で設計することをおすすめします。 - - -レスポンスの送信 -======== - -プレゼンターのレスポンスはふつう[テンプレートを HTML のページに描くこと|templates]ですが、ファイルや JSON を送ることも、別のページへリダイレクトすることもできます。 - -ライフサイクルのどの時点でも、次のいずれかのメソッドでレスポンスを送り、同時にプレゼンターを終わらせられます。 - -- `redirect()`、`redirectPermanent()`、`redirectUrl()`、`forward()` は[リダイレクト |#リダイレクト]を行います -- `error()` は[エラーのために |#404 などのエラー]プレゼンターを終わらせます -- `sendJson($data)` はプレゼンターを終わらせ、JSON 形式で[データを送ります |#JSON の送信] -- `sendTemplate()` はプレゼンターを終わらせ、すぐに[テンプレートを描きます |templates] -- `sendResponse($response)` はプレゼンターを終わらせ、[独自のレスポンス |#レスポンス]を送ります -- `terminate()` はレスポンスなしでプレゼンターを終わらせます - -これらのメソッドはいずれも、静かな終了の例外 `Nette\Application\AbortException` を投げて、ただちにプレゼンターを終わらせます。 - -これらのメソッドをどれも呼ばなければ、プレゼンターは自動的にテンプレートの描画に進みます。なぜでしょうか。99 % の場合、私たちはテンプレートを描きたいからです。ですからプレゼンターは、私たちの手間を省くためにこの振る舞いを既定としています。 - - -リンクの作成 -====== - -プレゼンターには、ほかのプレゼンターへの URL リンクを作る `link()` メソッドがあります。第 1 パラメータは行き先のプレゼンターとアクションで、そのあとに引数が続きます。引数は配列としても渡せます。 - -```php -$url = $this->link('Product:show', $id); - -$url = $this->link('Product:show', [$id, 'lang' => 'en']); -``` - -テンプレートでは、ほかのプレゼンターやアクションへのリンクを次のように作ります。 - -```latte -商品の詳細 -``` - -実際の URL の代わりに、見慣れた `Presenter:action` の組を書き、必要なパラメータを添えるだけです。仕掛けは `n:href` にあり、これがこの属性を処理して本当の URL を生成するよう Latte に伝えます。Nette では URL のことを考える必要はまったくなく、プレゼンターとアクションのことだけを考えればよいのです。 - -詳しくは [URL リンクの作成|creating-links]の章をご覧ください。 - - -リダイレクト -====== - -別のプレゼンターに切り替えるには `redirect()` と `forward()` メソッドを使います。[link() |#リンクの作成]メソッドとよく似た構文です。 - -`forward()` メソッドは、HTTP のリダイレクトなしにただちに新しいプレゼンターへ切り替えます。 - -```php -$this->forward('Product:show'); -``` - -HTTP コード 302(現在のリクエストのメソッドが POST なら 303)の一時的なリダイレクトの例です。 - -```php -$this->redirect('Product:show', $id); -``` - -HTTP コード 301 の恒久的なリダイレクトには、次を使います。 - -```php -$this->redirectPermanent('Product:show', $id); -``` - -アプリケーションの外の別の URL へは `redirectUrl()` メソッドでリダイレクトできます。HTTP コードは第 2 パラメータで指定でき、既定は 302(現在のリクエストのメソッドが POST なら 303)です。 - -```php -$this->redirectUrl('https://nette.org'); -``` - -リダイレクトは、いわゆる静かな終了の例外 `Nette\Application\AbortException` を投げて、ただちにプレゼンターの動作を終わらせます。 - -リダイレクトの前に、[フラッシュメッセージ |#フラッシュメッセージ]、つまりリダイレクト後のテンプレートに表示されるメッセージを送れます。 - - -フラッシュメッセージ -========== - -これはふつう、何らかの操作の結果を知らせるメッセージです。フラッシュメッセージの大事な性質は、リダイレクト後もテンプレートで使えることです。一度表示されたあとも、さらに 30 秒は有効なままです。たとえば通信のエラーでユーザーがページを再読み込みしても、メッセージがすぐ消えることはありません。 - -[flashMessage() |api:Nette\Application\UI\Control::flashMessage()]メソッドを呼ぶだけで、テンプレートへの受け渡しはプレゼンターが担当します。第 1 パラメータはメッセージの本文、省略可能な第 2 パラメータはその種類(error、warning、info など)です。`flashMessage()` メソッドはフラッシュメッセージのインスタンスを返すので、追加の情報を足せます。 - -```php -$this->flashMessage('項目を削除しました。'); -$this->redirect(/* ... */); // そしてリダイレクト -``` - -テンプレートでは、これらのメッセージが `$flashes` 変数に `stdClass` オブジェクトとして入っていて、`message`(メッセージの本文)、`type`(メッセージの種類)、そして先ほど触れたユーザーが足した情報のプロパティを持ちます。次のように描きます。 - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -404 などのエラー -========== - -たとえば表示したい記事がデータベースにないなど、リクエストに応えられない場合は、`error(string $message = '', int $httpCode = 404)` メソッドで 404 のエラーを投げます。 - -```php -public function renderShow(int $id): void -{ - $article = $this->articles->getById($id); - if (!$article) { - $this->error(); - } - // ... -} -``` - -HTTP のエラーコードは第 2 パラメータで渡せ、既定は 404 です。このメソッドは `Nette\Application\BadRequestException` を投げることで働き、そのあと `Application` が制御をエラー用のプレゼンターに渡します。これは、起きたエラーを知らせるページを表示する役目のプレゼンターです。エラー用のプレゼンターは[アプリケーションの設定|configuration]で指定します。 - - -JSON の送信 -======== - -`sendJson($data)` メソッドは、渡されたデータを JSON にエンコードして HTTP のレスポンスとして送り、プレゼンターを終わらせます。例を挙げます。 - -```php -public function actionData(): void -{ - $data = ['hello' => 'nette']; - $this->sendJson($data); -} -``` - - -リクエストのパラメータ .{data-version:3.1.14} -================================== - -プレゼンターも、各コンポーネントも、そのパラメータを HTTP のリクエストから得ます。値は `getParameter($name)` や `getParameters()` メソッドで取り出せます。値は文字列か文字列の配列で、要するに URL から直接得た生のデータです。 - -もっと便利にするために、プロパティ経由でパラメータにアクセスすることをおすすめします。`#[Parameter]` アトリビュートで印を付けるだけです。 - -```php -use Nette\Application\Attributes\Parameter; // この行が大事です - -class HomePresenter extends Nette\Application\UI\Presenter -{ - #[Parameter] - public string $theme; // public でなければなりません -} -``` - -プロパティにはデータ型(`string` など)を指定することをおすすめします。そうすれば Nette が値を自動的にキャストします。パラメータの値は[検証する |#パラメータの検証]こともできます。 - -リンクを作るとき、パラメータの値を直接設定できます。 - -```latte -クリック -``` - - -永続パラメータ -======= - -永続パラメータは、リクエストをまたいで状態を保つために使います。その値はリンクをクリックしたあとも変わりません。セッションのデータと違い、URL で運ばれます。しかもそれは完全に自動的に起こるので、`link()` や `n:href` で明示的に書く必要はありません。 - -使いどころの例を挙げましょう。多言語のアプリケーションがあるとします。現在の言語は、常に URL の一部でなければならないパラメータです。しかしそれをすべてのリンクに書くのは、途方もなく面倒です。ですからそれを永続パラメータ `lang` にすれば、自動的に運ばれていきます。素敵ですね。 - -Nette で永続パラメータを作るのはきわめて簡単です。public のプロパティを作り、アトリビュートで印を付けるだけです(以前は `/** @persistent */` が使われていました)。 - -```php -use Nette\Application\Attributes\Persistent; // この行が大事です - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; // public でなければなりません -} -``` - -`$this->lang` が `'en'` のような値を持っていれば、`link()` や `n:href` で作られたリンクにもパラメータ `lang=en` が入ります。そしてリンクをクリックしたあと、`$this->lang` はまた `'en'` になります。 - -プロパティにはデータ型(`string` など)を指定することをおすすめしますし、既定値も与えられます。パラメータの値は[検証できます |#パラメータの検証]。 - -永続パラメータはふつう、あるプレゼンターのすべてのアクションのあいだで引き継がれます。複数のプレゼンターをまたいで引き継ぐには、次のいずれかで定義する必要があります。 - -- プレゼンターが継承する共通の祖先で -- あるいはプレゼンターが使うトレイトで: - -```php -trait LanguageAware -{ - #[Persistent] - public string $lang; -} - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - use LanguageAware; -} -``` - -リンクを作るとき、永続パラメータの値は変えられます。 - -```latte -チェコ語での詳細 -``` - -あるいは*リセット*して URL から取り除けます。その場合は既定値になります。 - -```latte -クリック -``` - - -共有されるパラメータの空間 -============= - -リクエストのパラメータ、[永続パラメータ |#永続パラメータ]、そして `action`、`render`、`handle`(シグナル)メソッドのパラメータは、ひとつの空間を共有していて、それぞれが名前で識別されます。同じ名前が複数に現れれば、それらはまったく同じ値を指します。 - -これはしばしば都合よく使われます。たとえば永続パラメータ `lang` とアクションやシグナルのメソッドの引数 `$lang` は同じものなので、メソッドのシグネチャに並べるだけで永続パラメータの現在の値を読めます。 - -```php -#[Persistent] -public string $lang; - -public function handleSearch(string $query, string $lang): void -{ - // $lang には永続パラメータ lang の現在の値が入ります -} -``` - -この空間は共有されているので、意図的に値を共有したい場合を除き、パラメータの名前は一意に保ってください。これはシグナルにも当てはまり、シグナルはさらにリクエストの POST 本体からもパラメータを読みます。[シグナルの詳細 |components#シグナルの詳細]をご覧ください。 - - -インタラクティブなコンポーネント -================ - -プレゼンターには組み込みのコンポーネントのしくみがあります。コンポーネントは、プレゼンターに埋め込む独立した再利用できる部品です。[フォーム |forms:in-presenter]、データグリッド、メニューなど、繰り返し使う意味のあるものなら何でもかまいません。 - -コンポーネントはどうプレゼンターに埋め込まれ、どう使われるのでしょうか。それは[コンポーネント |components]の章で学べます。ハリウッドとの共通点まで見つかります。 - -コンポーネントはどこで手に入るのでしょうか。[Componette |https://componette.org/search/component]には、フレームワークのコミュニティの有志が寄せたオープンソースのコンポーネントと、Nette のためのそのほか多くのアドオンがあります。 - - -さらに深く -===== - -.[tip] -この章でここまで扱った内容で、たいていの用途には十分でしょう。以降の節は、プレゼンターをもっと深く知りたい、何もかも知りたいという方のためのものです。 - - -パラメータの検証 --------- - -URL から受け取った[リクエストのパラメータ |#リクエストのパラメータ]と[永続パラメータ |#永続パラメータ]の値は、`loadState()` メソッドがプロパティに書き込みます。あわせてプロパティに指定されたデータ型と合うかも確認し、合わなければ 404 のエラーで応え、ページは表示されません。 - -URL から受け取ったパラメータを決して盲信しないでください。ユーザーに簡単に書き換えられます。たとえば言語 `$this->lang` が対応しているものの中にあるかを、次のように確かめます。そのための適切な方法が、先ほどの `loadState()` メソッドの上書きです。 - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; - - public function loadState(array $params): void - { - parent::loadState($params); // ここで $this->lang が設定されます - // 続いて独自の値のチェック: - if (!in_array($this->lang, ['en', 'cs'])) { - $this->error(); - } - } -} -``` - - -リクエストの保存と復元 ------------ - -プレゼンターが処理するリクエストは [api:Nette\Application\Request]オブジェクトで、プレゼンターの `getRequest()` メソッドが返します。 - -現在のリクエストはセッションに保存でき、逆にそこから復元してプレゼンターにもう一度実行させられます。これは、たとえばユーザーがフォームに入力している最中にログインのセッションが切れたときに役立ちます。データを失わないよう、ログインページにリダイレクトする前に `$reqId = $this->storeRequest()` で現在のリクエストをセッションに保存します。これは短い文字列の識別子を返すので、それをパラメータとしてログインのプレゼンターに渡します。 - -ログイン後に `$this->restoreRequest($reqId)` メソッドを呼ぶと、セッションからリクエストを取り出します。POST のリクエストはそこへ forward され、それ以外(GET)はリクエストの URL にリダイレクトされます。このメソッドは、そのリクエストがいまログインしているのと同じユーザーによって作られたかを確認します。別のユーザーがログインした場合やキーが正しくない場合は何もせず、プログラムはいつもどおり続きます。 - -ガイド[前のページに戻るには |best-practices:restore-request]をご覧ください。 - - -正規化 ---- - -プレゼンターには、SEO(検索エンジン最適化)の向上に寄与する本当に優れた機能があります。異なる URL に同じ内容が存在するのを自動的に防ぐのです。たとえば `/index` と `/index?page=1` のように複数の URL が特定の行き先に通じている場合、フレームワークはそのひとつを主要(正規)なものと定め、ほかを HTTP コード 301 でそこへリダイレクトします。おかげで検索エンジンがページを二重に登録して、そのページランクを薄めることがなくなります。 - -この過程を正規化と呼びます。正規の URL は[ルーター|routing]が生成するもので、ふつうはコレクションの中で最初に一致するルートのものです。 - -正規化は既定で有効で、`$this->autoCanonicalize = false` で無効にできます。 - -AJAX や POST のリクエストではリダイレクトは起こりません。データを失いかねませんし、SEO 上の利点もないからです。 - -`canonicalize()` メソッドを使えば、正規化を手動で起こすこともできます。`link()` メソッドと同じように、プレゼンター、アクション、パラメータを渡します。リンクを生成して現在の URL アドレスと比べ、違っていれば生成したリンクへリダイレクトします。 - -```php -public function actionShow(int $id, ?string $slug = null): void -{ - $realSlug = $this->facade->getSlugForId($id); - // $slug が $realSlug と違えばリダイレクトします - $this->canonicalize('Product:show', [$id, $realSlug]); -} -``` - -ルートのフィルタと `canonicalize()` を組み合わせて SEO に強い URL を作る完全なパターンは、[スラッグを使った読みやすい URL |best-practices:pretty-urls]をご覧ください。 - - -レスポンス ------ - -プレゼンターが返すレスポンスは、[api:Nette\Application\Response]インターフェースを実装したオブジェクトです。あらかじめ用意されたレスポンスがいくつかあります。 - -- [api:Nette\Application\Responses\CallbackResponse] - コールバックを送ります -- [api:Nette\Application\Responses\FileResponse] - ファイルを送ります -- [api:Nette\Application\Responses\ForwardResponse] - forward() -- [api:Nette\Application\Responses\JsonResponse] - JSON を送ります -- [api:Nette\Application\Responses\RedirectResponse] - リダイレクト -- [api:Nette\Application\Responses\TextResponse] - テキストを送ります -- [api:Nette\Application\Responses\VoidResponse] - 空のレスポンス - -レスポンスは `sendResponse()` メソッドで送ります。 - -```php -use Nette\Application\Responses; - -// 素のテキスト -$this->sendResponse(new Responses\TextResponse('Hello Nette!')); - -// ファイルを送ります -$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf')); - -// コールバックを送ります -$callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) { - if ($httpResponse->getHeader('Content-Type') === 'text/html') { - echo '

    Hello

    '; - } -}; -$this->sendResponse(new Responses\CallbackResponse($callback)); -``` - -独自のレスポンスを書くこともできます。`Nette\Application\Response` インターフェースを実装するだけです。このインターフェースには、HTTP のリクエストとレスポンスを受け取る `send()` メソッドがひとつだけあります。たとえばメモリに保持したくないデータをストリームで送るときに役立ちます。 - -```php -class CsvResponse implements Nette\Application\Response -{ - public function __construct( - private string $fileName, - private iterable $rows, - ) { - } - - public function send(Nette\Http\IRequest $request, Nette\Http\IResponse $response): void - { - $response->setContentType('text/csv', 'utf-8'); - $response->sendAsFile($this->fileName); - - $handle = fopen('php://output', 'w'); - foreach ($this->rows as $row) { - fputcsv($handle, $row); - } - - fclose($handle); - } -} -``` - -あとはプレゼンターでいつもどおり送ります: `$this->sendResponse(new CsvResponse('export.csv', $rows));` - - -HTTP キャッシュ ----------- - -`lastModified()` メソッドを使うと、HTTP のキャッシュを簡単に活用できます。内容が最後に変更された日時(タイムスタンプ、文字列、`DateTimeInterface` オブジェクト)を渡し、必要なら ETag の検証子(内容の現在の版を表す短い文字列。そのハッシュなど)と有効期限も渡します。ブラウザがすでに一致する版を持っていれば、プレゼンターは `304 Not Modified` のレスポンスを送って終わるので、ページが無駄に描かれたり転送されたりしません。 - -```php -public function renderArticle(int $id): void -{ - $article = $this->articles->getById($id); - $this->lastModified($article->updatedAt); - // ... -} -``` - - -テンプレートの仕上げ .{data-version:3.3.0} --------------------------------- - -プレゼンターがテンプレートを描くとき、`sendTemplate()` メソッドは描画の直前に `completeTemplate()` を呼びます。このメソッドは `#[TemplateVariable]` アトリビュートで印の付いた変数を埋め、テンプレートのファイルを探します(既定の変数は、テンプレートが作られるときに `TemplateFactory` がすでに設定しています)。この protected のメソッドを上書きすれば、すべてのビューで共通の変数を足したり、別のファイルを指定したりできます。 - -```php -protected function completeTemplate(Nette\Application\UI\Template $template): void -{ - parent::completeTemplate($template); - $template->siteName = 'My App'; -} -``` - - -`#[Requires]` によるアクセスの制限 .{data-version:3.2.3} ----------------------------------------------- - -`#[Requires]` アトリビュートは、プレゼンターとそのメソッドへのアクセスを制限する高度な選択肢を提供します。HTTP のメソッドを指定する、AJAX のリクエストを要求する、同一オリジンに限る、forward 経由のアクセスだけを許すといったことができます。このアトリビュートは、プレゼンターのクラスにも、`action()`、`render()`、`handle()`、`createComponent()` といった個々のメソッドにも付けられます。 - -次の制限を指定できます。 -- HTTP のメソッドについて: `#[Requires(methods: ['GET', 'POST'])]` -- AJAX のリクエストを要求する: `#[Requires(ajax: true)]` -- 同一オリジンからのアクセスだけ: `#[Requires(sameOrigin: true)]` -- forward 経由のアクセスだけ: `#[Requires(forward: true)]` -- 特定のアクションへの制限: `#[Requires(actions: 'default')]` - -.[note] -バージョン 3.3 以降、同一オリジンの判定はブラウザの `Sec-Fetch-Site` ヘッダーで行われます(以前は SameSite の cookie 経由でした)。こちらのほうが確実で、スキーム、ドメイン、ポートの厳密な一致を確認します。 - -詳しくはガイド [Requires アトリビュートの使い方 |best-practices:attribute-requires]をご覧ください。 - - -HTTP メソッドのチェック --------------- - -Nette のプレゼンターは、主に安全のために、届いたすべてのリクエストの HTTP メソッドを自動的に確認します。既定では `GET`、`POST`、`HEAD`、`PUT`、`DELETE`、`PATCH` のメソッドが許されます。 - -たとえば `OPTIONS` メソッドも追加で許したい場合は、`#[Requires]` アトリビュートを使います(Nette Application v3.2.3 以降)。 - -```php -#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])] -class MyPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -バージョン 3.1.13 以降、確認は `checkHttpMethod()` で行われ、リクエストで指定されたメソッドが `$presenter->allowedMethods` 配列に含まれるかを調べます。バージョン 3.2.3 以降、この方法は非推奨で、`#[Requires]` が推奨されます。このメソッドは次のように上書きできます。 - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - protected function checkHttpMethod(): void - { - $this->allowedMethods[] = 'OPTIONS'; - parent::checkHttpMethod(); - } -} -``` - -強調しておくべきなのは、`OPTIONS` メソッドを有効にしたら、プレゼンターの中でそれを適切に扱わなければならない、ということです。このメソッドはいわゆるプリフライトのリクエストとしてよく使われ、CORS(Cross-Origin Resource Sharing)のポリシーに照らしてリクエストが許されるかを判断する必要があるとき、ブラウザが実際のリクエストの前に自動的に送ります。メソッドを有効にしながら正しい応答を実装しないと、食い違いや潜在的なセキュリティの問題につながりかねません。 - - -非推奨のアクションへの印 .{data-version:3.2.3} ----------------------------------- - -`#[Deprecated]` アトリビュートは、アクション、シグナル、あるいはプレゼンター全体を非推奨で将来削除予定と印を付けます。アプリケーションの非推奨の部分へのリンクを生成すると、Nette は警告を出して開発者に知らせます。 - -このアトリビュートは、プレゼンターのクラス全体にも、個々の `action()`、`render()`、`handle()` メソッドにも付けられます。 - - -関連情報 -==== - -- [inject メソッドとアトリビュート |best-practices:inject-method-attribute] -- [トレイトによるプレゼンターの構成 |best-practices:presenter-traits] -- [プレゼンターへの設定の受け渡し |best-practices:passing-settings-to-presenters] -- [前のページに戻るには |best-practices:restore-request] diff --git a/application/ja/routing.texy b/application/ja/routing.texy deleted file mode 100644 index b0cabf02a8..0000000000 --- a/application/ja/routing.texy +++ /dev/null @@ -1,749 +0,0 @@ -ルーティング -****** - -
    - -ルーターは URL アドレスにまつわるすべてを引き受けるので、あなたが URL のことを考える必要はありません。ここでは次のことを扱います。 - -- URL を思いどおりの見た目にするためのルーターの設定 -- SEO とリダイレクト -- そして独自のルーターの書き方 - -
    - - -人にやさしい URL(かっこいい URL、きれいな URL とも呼ばれます)は使いやすく、覚えやすく、SEO にもよい影響を与えます。Nette はそれを心得ていて、開発者の求めるものにしっかり応えます。アプリケーションの URL の構造を、思いどおりに設計できます。しかもアプリケーションが完成したあとでも設計できます。コードやテンプレートの変更が要らないからです。それはすべてのプレゼンターにアノテーションとして散らばるのではなく、[ただ一か所 |#組み込み]、つまりルーターで優雅に定義されます。 - -Nette のルーターが並外れているのは、**双方向**だからです。HTTP のリクエストから URL を読み解くことも、リンクを作ることもできます。ですから [Nette Application |how-it-works#Nette Application]で欠かせない役割を果たします。現在のリクエストをどのプレゼンターとアクションが処理するかを決めるだけでなく、テンプレートなどで [URL を生成する |creating-links]のにも使われるからです。 - -とはいえルーターの用途はそれだけではありません。プレゼンターをまったく使わないアプリケーションや REST API などでも使えます。詳しくは [#単独での利用]の節をご覧ください。 - - -ルートのコレクション -========== - -アプリケーションの URL アドレスの構造を定義するもっとも心地よい方法は、[api:Nette\Application\Routers\RouteList]クラスが提供します。その定義は、いわゆるルートの一覧、つまり URL アドレスのマスクと、それに結びつくプレゼンターやアクションから成り、単純な API で書けます。ルートに名前を付ける必要はまったくありません。 - -```php -$router = new Nette\Application\Routers\RouteList; -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('article/', 'Article:view'); -// ... -``` - -この例からわかるように、ブラウザで `https://domain.com/rss.xml` を開くと `Feed` プレゼンターの `rss` アクションが表示されます。`https://domain.com/article/12` なら `Article` プレゼンターの `view` アクションが表示される、といった具合です。ふさわしいルートが見つからなければ、Nette Application は [BadRequestException |api:Nette\Application\BadRequestException]を投げて応え、ユーザーには 404 Not Found のエラーページとして表示されます。 - - -ルートの順序 ------- - -個々のルートを並べる**順序**はきわめて**重要**です。上から下へ順に評価されるからです。原則は、ルートを**具体的なものから一般的なものへ**と宣言することです。 - -```php -// 誤り: 'rss.xml' は最初のルートに捕まえられ、その文字列が と解釈されます -$router->addRoute('', 'Article:view'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// 正しい -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('', 'Article:view'); -``` - -リンクを生成するときも、ルートは上から下へ評価されます。 - -```php -// 誤り: 'Feed:rss' へのリンクが 'admin/feed/rss' として生成されます -$router->addRoute('admin//', 'Admin:default'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// 正しい -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('admin//', 'Admin:default'); -``` - -ルートを正しく組み立てるにはいくらか慣れが要ることは、隠さずに申し上げます。身につくまでは [ルーティングのパネル |#ルーターのデバッグ]が役に立つ道具になるでしょう。 - - -マスクとパラメータ ---------- - -マスクはウェブサイトのルートディレクトリからの相対パスを表します。もっとも単純なマスクは静的な URL です。 - -```php -$router->addRoute('products', 'Products:default'); -``` - -マスクには**パラメータ**が入ることがよくあります。これは山かっこで囲まれ(たとえば ``)、対象のプレゼンターに、たとえば `renderShow(int $year)` メソッドや永続パラメータ `$year` に渡されます。 - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -この例からわかるように、ブラウザで `https://example.com/chronicle/2020` を開くと、`History` プレゼンターの `show` アクションがパラメータ `year: 2020` とともに表示されます。 - -パラメータの既定値はマスクの中で直接指定でき、そうするとそのパラメータは省略できるようになります。 - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -これでこのルートは `https://example.com/chronicle/` という URL も受け付け、やはり `History:show` をパラメータ `year: 2020` とともに表示します。 - -もちろんプレゼンターやアクションの名前もパラメータにできます。たとえば次のようにです。 - -```php -$router->addRoute('/', 'Home:default'); -``` - -このルートはたとえば `/article/edit` や `/catalog/list` の形の URL を受け付け、それぞれプレゼンターとアクション `Article:edit`、`Catalog:list` と解釈します。 - -同時に `presenter` と `action` のパラメータに既定値 `Home` と `default` を与えるので、これらも省略できるようになります。ですからこのルートは `/article` という URL も受け付け、`Article:default` と解釈します。逆に `Product:default` へのリンクはパス `/product` を生成し、既定の `Home:default` へのリンクはパス `/` を生成します。 - -マスクはウェブサイトのルートディレクトリからの相対パスだけでなく、スラッシュで始めれば絶対パスを、スラッシュ 2 つで始めれば絶対 URL 全体を表せます。 - -```php -// ドキュメントルートからの相対 -$router->addRoute('/', /* ... */); - -// 絶対パス(ドメインからの相対) -$router->addRoute('//', /* ... */); - -// ドメインを含む絶対 URL(スキームからの相対) -$router->addRoute('//.example.com//', /* ... */); - -// スキームを含む絶対 URL -$router->addRoute('https://.example.com//', /* ... */); -``` - - -検証の正規表現 -------- - -パラメータごとに [正規表現|https://www.php.net/manual/en/reference.pcre.pattern.syntax.php]で検証の条件を指定できます。たとえば `id` パラメータには、正規表現 `\d+` で数字しか入れられないと指定します。 - -```php -$router->addRoute('/[/]', /* ... */); -``` - -すべてのパラメータの既定の正規表現は `[^/]+`、つまりスラッシュ以外のすべてです。パラメータにスラッシュも受け付けさせたいなら、正規表現を `.+` にします。 - -```php -// https://example.com/a/b/c を受け付け、path は 'a/b/c' になります -$router->addRoute('', /* ... */); -``` - - -省略できる部分 -------- - -マスクの中では、角かっこで省略できる部分に印を付けられます。マスクのどの部分も省略できるようにでき、その中にパラメータを入れられます。 - -```php -$router->addRoute('[/]', /* ... */); - -// 次のパスを受け付けます: -// /en/download => lang => en, name => download -// /download => lang => null, name => download -``` - -パラメータが省略できる部分の中にあると、当然そのパラメータも省略できるようになります。既定値が指定されていなければ null になります。 - -省略できる部分はドメインにも置けます。 - -```php -$router->addRoute('//[.]example.com//', /* ... */); -``` - -省略できる部分は自由に入れ子にしたり組み合わせたりできます。 - -```php -$router->addRoute( - '[[-]/][/page-]', - 'Home:default', -); - -// 次のパスを受け付けます: -// /en/hello -// /en-us/hello -// /hello -// /hello/page-12 -``` - -URL を生成するときは、もっとも短い形が優先されるので、省ける部分はすべて省かれます。ですからたとえばルート `index[.html]` はパス `/index` を生成します。この振る舞いは、開き角かっこのうしろに感嘆符を置くと逆にできます。 - -```php -// /hello と /hello.html を受け付け、/hello を生成します -$router->addRoute('[.html]', /* ... */); - -// /hello と /hello.html を受け付け、/hello.html を生成します -$router->addRoute('[!.html]', /* ... */); -``` - -角かっこのない、省略できるパラメータ(つまり既定値を持つパラメータ)は、実質的に次のように囲まれているかのように振る舞います。 - -```php -$router->addRoute('//', /* ... */); - -// これは次と同じです: -$router->addRoute('[/[/[]]]', /* ... */); -``` - -末尾のスラッシュの振る舞いに手を入れて、たとえば `/home/` の代わりに `/home` を生成させたいなら、次のようにできます。 - -```php -$router->addRoute('[[/[/]]]', /* ... */); -``` - - -ワイルドカード -------- - -絶対 URL のマスクでは次のワイルドカードを使えます。たとえば開発環境と本番環境で違うかもしれないドメインを、マスクに書かずに済ませられます。 - -- `%tld%` = トップレベルドメイン、たとえば `com` や `org` -- `%sld%` = セカンドレベルドメイン、たとえば `example` -- `%domain%` = サブドメインを除いたドメイン、たとえば `example.com` -- `%host%` = ホスト全体、たとえば `www.example.com` -- `%basePath%` = ルートディレクトリへのパス - -```php -$router->addRoute('//www.%domain%/%basePath%//', /* ... */); -$router->addRoute('//www.%sld%.%tld%/%basePath%//', /* ... */); -``` - - -詳しい書き方 ------- - -ふつう `Presenter:action` の形で書くルートの行き先は、個々のパラメータとその既定値を定義する配列でも書けます。 - -```php -$router->addRoute('/[/]', [ - 'presenter' => 'Home', - 'action' => 'default', -]); -``` - -より細かく指定するには、さらに広げた形も使えます。そこでは既定値のほかに、検証の正規表現などパラメータのほかの性質も設定できます(`id` パラメータをご覧ください)。 - -```php -use Nette\Routing\Route; - -$router->addRoute('/[/]', [ - 'presenter' => [ - Route::Value => 'Home', - ], - 'action' => [ - Route::Value => 'default', - ], - 'id' => [ - Route::Pattern => '\d+', - ], -]); -``` - -大事なのは、配列で定義したパラメータがパスのマスクに現れていない場合、その値は変えられないという点です。URL の疑問符のうしろに書くクエリパラメータでも変えられません。 - -これは**固定のパラメータ**、つまりあるページに短く覚えやすい URL を与えるのに役立ちます。たとえば `/tos` がいつも `Article:view` を `id: 123` で開くようにするには次のようにします。 - -```php -$router->addRoute('tos', [ - 'presenter' => 'Article', - 'action' => 'view', - 'id' => 123, -]); -``` - - -フィルタと変換 -------- - -アプリケーションのソースコードは英語で書きますが、ウェブサイトの URL をチェコ語にしたい場合、次のような単純なルーティングでは、 - -```php -$router->addRoute('/', 'Home:default'); -``` - -`/product/123` や `/cart` のような英語の URL が生成されます。URL の中のプレゼンターとアクションをチェコ語(たとえば `/produkt/123` や `/kosik`)で表したいなら、変換の辞書を使えます。それを書くには、第 2 パラメータの「詳しい」書き方がもう必要です。 - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterTable => [ - // URL の中の文字列 => プレゼンター - 'produkt' => 'Product', - 'kosik' => 'Cart', - 'katalog' => 'Catalog', - ], - ], - 'action' => [ - Route::Value => 'default', - Route::FilterTable => [ - 'seznam' => 'list', - ], - ], -]); -``` - -変換の辞書では、複数のキーが同じプレゼンターを指しても構いません。そうするとそのプレゼンターにいろいろな別名ができます。最後のキーが正式な形(つまり生成される URL に入る形)と見なされます。 - -変換の表はこのやり方でどのパラメータにも使えます。変換が存在しなければ、もとの値がそのまま使われます。この振る舞いは `Route::FilterStrict => true` を足すと変えられ、値が辞書にない場合、そのルートは URL を受け付けなくなります。 - -配列の形の変換の辞書のほかに、独自の変換の関数も使えます。 - -```php -use Nette\Routing\Route; - -$router->addRoute('//', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterIn => function (string $s): string { /* ... */ }, - Route::FilterOut => function (string $s): string { /* ... */ }, - ], - 'action' => 'default', - 'id' => null, -]); -``` - -`Route::FilterIn` の関数は、URL の中のパラメータと、そのあとプレゼンターに渡される文字列とのあいだの変換を行い、`FilterOut` の関数は逆向きの変換を受け持ちます。 - -`presenter`、`action`、`module` のパラメータには、PascalCase や camelCase の書き方と、URL で使われる kebab-case とのあいだを変換するフィルタがあらかじめ用意されています。パラメータの既定値は、アプリケーションに渡される形(プレゼンターとモジュールは PascalCase、アクションは camelCase)で書くので、たとえばプレゼンターなら `` と書き、`` とは書きません。 - - -一般のフィルタ -------- - -特定のパラメータ向けのフィルタのほかに、一般のフィルタも定義できます。これはすべてのパラメータの連想配列を受け取り、好きなように変えて返せます。一般のフィルタは空のキーの下に定義します。 - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => 'Home', - 'action' => 'default', - '' => [ - Route::FilterIn => function (array $params): array { /* ... */ }, - Route::FilterOut => function (array $params): array { /* ... */ }, - ], -]); -``` - -一般のフィルタを使えば、ルートの振る舞いを本当に好きなように変えられます。たとえばほかのパラメータをもとにパラメータを変えるのに使えます。たとえば `` パラメータの今の値をもとに `` と `` を変換する、といった具合です。 - -パラメータに独自のフィルタが定義されていて、一般のフィルタもある場合、独自の `FilterIn` が一般のものより先に実行され、逆に一般の `FilterOut` が独自のものより先に実行されます。ですから一般のフィルタの中では、`presenter` と `action` のパラメータの値はそれぞれ PascalCase と camelCase の書き方になっています。 - -これらのフィルタの実用的な使い方は [スラッグ付きのきれいな URL |best-practices:pretty-urls]をご覧ください。テンプレートを一切変えずに `/article/123-how-to-bake-bread` のような SEO にやさしい URL を生成します。 - - -OneWay フラグ ----------- - -一方通行のルートは、アプリケーションがもう生成しないけれど受け付けはする古い URL を生かしておくのに使います。`OneWay` フラグで印を付けます。 - -```php -// 古い URL /product-info?id=123 -$router->addRoute('product-info', 'Product:detail', oneWay: true); -// 新しい URL /product/123 -$router->addRoute('product/', 'Product:detail'); -``` - -古い URL にアクセスすると、プレゼンターが自動的に新しい URL へリダイレクトするので、検索エンジンがこれらのページを二重に登録することはありません([#SEO と正規化]をご覧ください)。 - - -コールバックによる動的なルーティング ------------------- - -コールバックによる動的なルーティングを使うと、ルートに関数(コールバック)を直接割り当てられ、そのパスが訪れられたときに実行されます。この柔軟な機能のおかげで、アプリケーションのさまざまなエンドポイントを手早く効率よく作れます。 - -```php -$router->addRoute('test', function () { - echo 'You are at the /test address'; -}); -``` - -マスクの中にパラメータを定義することもでき、それは自動的にコールバックに渡されます。 - -```php -$router->addRoute('', function (string $lang) { - echo match ($lang) { - 'cs' => 'Welcome to the Czech version of our website!', - 'en' => 'Welcome to the English version of our website!', - }; -}); -``` - -マスクからのパラメータのほかに、コールバックは DI コンテナのサービスも受け取れます。それはパラメータの型をもとに渡されます。さらに `$presenter` パラメータには、そのルートを処理する [MicroPresenter |api:NetteModule\MicroPresenter]のインスタンスが渡されます。 - -```php -$router->addRoute('', function (string $lang, Nette\Http\Request $httpRequest, NetteModule\MicroPresenter $presenter) { - // ... -}); -``` - - -モジュール ------ - -共通の[モジュール |directory-structure#プレゼンターとテンプレート]に属するルートが複数あるなら、`withModule()` を使います。指定したモジュールは、そのグループのすべてのルートのプレゼンターに自動的に前置され、URL からはすっかり消えます。 - -```php -$router = new RouteList; -$router->withModule('Forum') // 以下のルートは Forum モジュールの一部です - ->addRoute('rss', 'Feed:rss') // プレゼンターは Forum:Feed になります - ->addRoute('/') - - ->withModule('Admin') // 以下のルートは Forum:Admin モジュールの一部です - ->addRoute('sign:in', 'Sign:in'); -``` - -代わりに `module` パラメータも使えます。これも同じように固定のモジュールを設定し、URL には現れないようにします。 - -```php -// URL manage/dashboard/default はプレゼンター Admin:Dashboard に対応します -$router->addRoute('manage//', [ - 'module' => 'Admin', -]); -``` - -プレゼンターの名前は、そのモジュールと合わせてはじめて完全になります。たとえば `Front:Admin:ProductList` です。こうした完全な名前が URL のパラメータに入るとき、ルーターは 2 つの単純な規則でそれを符号化します。コロン `:`(モジュールの区切り)はすべて**ドット**になり、PascalCase の名前の語の切れ目はすべて**ハイフン**になります。ですから `Front:Admin:ProductList` は URL の中で `front.admin.product-list` として現れ、同じやり方で読み戻されます。モジュールに分かれたアプリケーションが、上のような道具を使わないとドットだらけの URL を作るのは、これが理由です。 - -`withModule()` も `module` パラメータも、まさにこれを避けます。プレゼンターの名前が URL に届く前に、分かっているモジュールの接頭辞を取り除くからです。モジュールが決まりきったものである以上、そもそも符号化する必要はありません。 - -ときにはモジュール自体を変えられるようにして、URL に現れてほしいこともあります。そこでマスクの中で `` を直接使います。ただし大事な点にご注意ください。**`` はモジュールのパス全体**、つまりプレゼンターの名前の最後のコロンまでを取り込みます。プレゼンター `Shop:Admin:Product` ならモジュールは `Shop:Admin`、プレゼンターは `Product` ということになり、コロンはドットになるので、次のようになります。 - - -サブドメイン ------- - -ルートのコレクションはサブドメインごとに分けられます。 - -```php -$router = new RouteList; -$router->withDomain('example.com') - ->addRoute('rss', 'Feed:rss') - ->addRoute('/'); -``` - -ドメイン名には [#ワイルドカード]も使えます。 - -```php -$router = new RouteList; -$router->withDomain('example.%tld%') - // ... -``` - - -パスの接頭辞 ------- - -ルートのコレクションは URL のパスごとに分けられます。 - -```php -$router = new RouteList; -$router->withPath('eshop') - ->addRoute('rss', 'Feed:rss') // URL /eshop/rss に一致します - ->addRoute('/'); // URL /eshop// に一致します -``` - - -組み合わせ ------ - -上のグループ分けは互いに組み合わせられます。 - -```php -$router = (new RouteList) - ->withDomain('admin.example.com') - ->withModule('Admin') - ->addRoute(/* ... */) - ->addRoute(/* ... */) - ->end() - ->withModule('Images') - ->addRoute(/* ... */) - ->end() - ->end() - ->withDomain('example.com') - ->withPath('export') - ->addRoute(/* ... */) - // ... -``` - - -クエリパラメータ --------- - -マスクにはクエリパラメータ(URL の疑問符のうしろのパラメータ)も入れられます。これには検証の正規表現を定義できませんが、プレゼンターに渡される名前は変えられます。 - -```php -// クエリパラメータ 'cat' をアプリケーションでは 'categoryId' という名前で使いたい -$router->addRoute('product ? id= & cat=', /* ... */); -``` - - -Foo パラメータ ---------- - -さらに深いところへ進みます。Foo パラメータは要するに名前のないパラメータで、正規表現に一致させられます。例として `/index`、`/index.html`、`/index.htm`、`/index.php` を受け付けるルートを挙げます。 - -```php -$router->addRoute('index', /* ... */); -``` - -URL を生成するときに使われる文字列を、はっきり定義することもできます。その文字列は疑問符のすぐうしろに置かなければなりません。次のルートは前のものと似ていますが、`/index` の代わりに `/index.html` を生成します。生成に使う値として文字列 `.html` が設定されているからです。 - -```php -$router->addRoute('index', /* ... */); -``` - - -組み込み -==== - -作ったルーターをアプリケーションに組み込むには、DI コンテナにそれを教える必要があります。もっとも簡単なのは、ルーターのオブジェクトを作るファクトリを用意して、設定でそれを使うようにコンテナに伝えることです。そのために `App\Core\RouterFactory::createRouter()` メソッドを書くとしましょう。 - -```php -namespace App\Core; - -use Nette\Application\Routers\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute(/* ... */); - return $router; - } -} -``` - -そして[設定 |dependency-injection:services]に次のように書きます。 - -```neon -services: - - App\Core\RouterFactory::createRouter -``` - -データベースなどへの依存関係があれば、[オートワイヤリング|dependency-injection:autowiring]によってファクトリメソッドのパラメータとして渡されます。 - -```php -public static function createRouter(Nette\Database\Connection $db): RouteList -{ - // ... -} -``` - - -SimpleRouter -============ - -ルートのコレクションよりずっと単純なルーターが [SimpleRouter |api:Nette\Application\Routers\SimpleRouter]です。URL の形に特別な要求がないとき、`mod_rewrite`(やその代わりになるもの)が使えないとき、あるいはまだきれいな URL に手を出したくないときに使います。 - -だいたい次のような形のアドレスを生成します。 - -``` -http://example.com/?presenter=Product&action=detail&id=123 -``` - -`SimpleRouter` のコンストラクタのパラメータは既定のプレゼンターとアクション、つまり `http://example.com/` をほかのパラメータなしで開いたときに実行されるアクションです。 - -```php -// 既定のプレゼンターは 'Home'、アクションは 'default' になります -$router = new Nette\Application\Routers\SimpleRouter('Home:default'); -``` - -SimpleRouter は[設定 |dependency-injection:services]で直接定義することをおすすめします。 - -```neon -services: - - Nette\Application\Routers\SimpleRouter('Home:default') -``` - - -SEO と正規化 -======== - -フレームワークは、異なる URL に同じ内容が現れるのを防ぐことで SEO(検索エンジン最適化)に貢献します。ある行き先に複数のアドレス、たとえば `/index` と `/index.html` が通じている場合、フレームワークは最初のものを主要なもの(正式なもの)と定め、ほかはそこへ HTTP コード 301 でリダイレクトします。おかげで検索エンジンがページを二重に登録して、そのページランクを薄めることがありません。 - -この処理を正規化と呼びます。正式な URL は、ルーターが生成するもの、つまりコレクションの中で最初に一致した OneWay フラグのないルートが生成するものです。ですからコレクションでは**主要なルートを先に**並べます。 - -正規化はプレゼンターが行います。詳しくは [正規化 |presenters#正規化]の章をご覧ください。 - - -HTTPS -===== - -HTTPS プロトコルを使うには、ホスティングでそれを有効にし、サーバーを正しく設定する必要があります。 - -ウェブサイト全体の HTTPS へのリダイレクトは、サーバーの水準で、たとえばアプリケーションのルートディレクトリの `.htaccess` ファイルで HTTP コード 301 を使って設定しなければなりません。設定はホスティングによって違うことがあり、だいたい次のようになります。 - -``` - - RewriteEngine On - ... - RewriteCond %{HTTPS} off - RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] - ... - -``` - -ルーターはページが読み込まれたのと同じプロトコルで URL を生成するので、これ以上の設定は要りません。 - -とはいえ、例外的にルートごとに違うプロトコルで動かす必要があるなら、ルートのマスクで指定します。 - -```php -// HTTP のアドレスを生成します -$router->addRoute('http://%host%//', /* ... */); - -// HTTPS のアドレスを生成します -$router->addRoute('https://%host%//', /* ... */); -``` - - -ルーターのデバッグ -========= - -[Tracy Bar |tracy:]に表示されるルーティングのパネルは役に立つ助っ人で、ルートの一覧と、ルーターが URL から取り出したパラメータを見せてくれます。 - -✓ の印が付いた緑の帯は、現在の URL を処理したルートを表します。青色と ≈ の印は、緑のルートに先を越されなければやはりその URL を処理したであろうルートを示します。さらに現在のプレゼンターとアクションも見られます。 - -[* routing-debugger.webp *] - -同時に、[正規化 |#SEO と正規化]によって思いがけないリダイレクトが起きたときは、パネルの *redirect* の帯を見るとよいでしょう。そこでルーターがもともとその URL をどう解釈し、なぜリダイレクトしたのかがわかります。 - -.[note] -ルーターをデバッグするときは、ブラウザで開発者ツールを開き(Ctrl+Shift+I または Cmd+Option+I)、Network のパネルでキャッシュを無効にして、リダイレクトが保存されないようにすることをおすすめします。 - - -性能 -=== - -ルートの数はルーターの速さに影響します。その数は数十を超えないようにすべきです。ウェブサイトの URL の構造が複雑すぎるなら、独自の[ルーター |#独自のルーター]を書けます。 - -ルーターがデータベースなどへの依存関係を持たず、そのファクトリが引数を取らないなら、その組み立て済みの形を DI コンテナに直接シリアライズして、アプリケーションをわずかに速くできます。 - -```neon -routing: - cache: true -``` - - -独自のルーター -======= - -以下の記述はとても進んだ利用者向けです。独自のルーターを作って、ルートのコレクションに自然に組み込めます。ルーターは 2 つのメソッドを持つ [api:Nette\Routing\Router]インターフェースの実装です。 - -```php -use Nette\Http\IRequest as HttpRequest; -use Nette\Http\UrlScript; - -class MyRouter implements Nette\Routing\Router -{ - public function match(HttpRequest $httpRequest): ?array - { - // ... - } - - public function constructUrl(array $params, UrlScript $refUrl): ?string - { - // ... - } -} -``` - -`match` メソッドは現在のリクエスト [$httpRequest |http:request](そこからは URL だけでなくヘッダーなども取り出せます)を、プレゼンターの名前とそのパラメータを含む配列に変えます。リクエストを処理できなければ null を返します。リクエストを処理するときは、少なくともプレゼンターを返さなければなりません。アクションは省略でき、指定がなければ `default` になります。プレゼンターの名前は完全で、モジュールも含みます。 - -```php -[ - 'presenter' => 'Front:Home', - 'action' => 'default', -] -``` - -一方 `constructUrl` メソッドは、パラメータの配列から結果となる絶対 URL を組み立てます。そのとき現在の URL である [`$refUrl`|api:Nette\Http\UrlScript]パラメータの情報を使えます。 - -`add()` でルートのコレクションに足します。 - -```php -$router = new Nette\Application\Routers\RouteList; -$router->add($myRouter); -$router->addRoute(/* ... */); -// ... -``` - - -単独での利用 -====== - -単独での利用とは、Nette Application とプレゼンターを使わないアプリケーションでルーターの機能を活かすことです。この章で見てきたことのほとんどはそこでも当てはまり、違いは次の点だけです。 - -- ルートのコレクションには [api:Nette\Routing\RouteList]クラスを使います -- 単純なルーターには [api:Nette\Routing\SimpleRouter]クラスを使います -- `Presenter:action` の組が存在しないので、[#詳しい書き方]を使います - -ですからここでもルーターを組み立てるメソッドを作ります。たとえば次のようにです。 - -```php -namespace App\Core; - -use Nette\Routing\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute('rss.xml', [ - 'controller' => 'RssFeedController', - ]); - $router->addRoute('article/', [ - 'controller' => 'ArticleController', - ]); - // ... - return $router; - } -} -``` - -おすすめの DI コンテナを使っているなら、このメソッドをやはり設定に足して、コンテナからルーターを HTTP のリクエストとともに受け取ります。 - -```php -$router = $container->getByType(Nette\Routing\Router::class); -$httpRequest = $container->getByType(Nette\Http\IRequest::class); -``` - -あるいはオブジェクトを直接作ります。 - -```php -$router = App\Core\RouterFactory::createRouter(); -$httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); -``` - -あとはルーターに仕事をさせるだけです。 - -```php -$params = $router->match($httpRequest); -if ($params === null) { - // 一致するルートが見つからなかったので 404 のエラーを送ります - exit; -} - -// 得られたパラメータを処理します -$controller = $params['controller']; -// ... -``` - -逆にルーターを使ってリンクを組み立てます。 - -```php -$params = ['controller' => 'ArticleController', 'id' => 123]; -$url = $router->constructUrl($params, $httpRequest->getUrl()); -``` - - -{{composer: nette/routing}} -{{repo: nette/routing}} -{{api: https://api.nette.org/routing/}} diff --git a/application/ja/templates.texy b/application/ja/templates.texy deleted file mode 100644 index 5e38d60b86..0000000000 --- a/application/ja/templates.texy +++ /dev/null @@ -1,419 +0,0 @@ -テンプレート -****** - -.[perex] -Nette は [Latte |latte:]というテンプレートシステムを使います。Latte を使うのは、PHP で最も安全なテンプレートシステムであり、同時に最も直感的なシステムだからです。新しく覚えることは多くありません。PHP の知識といくつかのタグで十分です。 - -ページはたいてい、レイアウトのテンプレートと個別のアクションのテンプレートから組み立てられます。レイアウトのテンプレートはたとえば次のような形です。`{block}` のブロックと `{include}` タグに注目してください。 - -```latte - - - - {block title}My App{/block} - - -
    ...
    - {include content} -
    ...
    - - -``` - -そしてこれがアクションのテンプレートです。 - -```latte -{block title}Homepage{/block} - -{block content} -

    Homepage

    -... -{/block} -``` - -ここでは `content` ブロックを定義していて、レイアウトの `{include content}` の場所に挿入されます。あわせて `title` ブロックを定義し直していて、レイアウトの `{block title}` を上書きします。結果を思い浮かべてみてください。 - - -テンプレートの探索 ---------- - -プレゼンターでは、どのテンプレートを描くべきかを指定する必要はありません。フレームワークが自動的にパスを導くので、書く手間が省けます。 - -プレゼンターごとにディレクトリを持つ構成を使っているなら、そのディレクトリにアクション(つまりビュー)の名前でテンプレートを置くだけです。たとえば `default` アクションには `default.latte` テンプレートを使います。 - -/--pre -app/ -└── Presentation/ - └── Home/ - ├── HomePresenter.php - └── default.latte -\-- - -プレゼンターをひとつのディレクトリにまとめ、テンプレートを `templates` フォルダに置く構成を使っているなら、`..latte` か `/.latte` のファイルに保存します。 - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── Home/ - │ └── default.latte ← 1 つめの形 - └── Home.default.latte ← 2 つめの形 -\-- - -`templates` ディレクトリは 1 階層上、つまりプレゼンターのクラスがあるディレクトリと同じ階層に置くこともできます。 - -テンプレートが見つからない場合、プレゼンターは [404 - ページが見つかりません |presenters#404 などのエラー]で応えます。 - -ビューは `$this->setView('otherView')` で変えられます。`$this->template->setFile('/path/to/template.latte')` でテンプレートのファイルを直接指定することもできます。 - -.[note] -テンプレートを探すファイルは、可能なファイル名の配列を返す [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()]メソッドを上書きすれば変えられます。 - - -レイアウトのテンプレートの探索 ---------------- - -Nette はレイアウトのファイルも自動的に探します。 - -プレゼンターごとにディレクトリを持つ構成を使っているなら、そのプレゼンター専用のレイアウトはプレゼンターのフォルダに、複数のプレゼンターで共通のレイアウトは 1 階層上に置きます。 - -/--pre -app/ -└── Presentation/ - ├── @layout.latte ← 共通のレイアウト - └── Home/ - ├── @layout.latte ← Home プレゼンター専用 - ├── HomePresenter.php - └── default.latte -\-- - -プレゼンターをひとつのディレクトリにまとめ、テンプレートを `templates` フォルダに置く構成なら、レイアウトは次の場所で探されます。 - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── @layout.latte ← 共通のレイアウト - ├── Home/ - │ └── @layout.latte ← Home 専用、1 つめの形 - └── Home.@layout.latte ← Home 専用、2 つめの形 -\-- - -プレゼンターがモジュールの中にある場合は、モジュールの入れ子に応じてディレクトリの階層をさらに上へ探していきます。 - -レイアウトの名前は `$this->setLayout('layoutAdmin')` で変えられ、その場合は `@layoutAdmin.latte` ファイルが期待されます。`$this->setLayout('/path/to/template.latte')` でレイアウトのテンプレートファイルを直接指定することもできます。 - -`$this->setLayout(false)` を使うか、テンプレートの中で `{layout none}` タグを使うと、レイアウトの探索を無効にできます。 - -.[note] -レイアウトのテンプレートを探すファイルは、可能なファイル名の配列を返す [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()]メソッドを上書きすれば変えられます。 - - -テンプレートの変数 ---------- - -変数は `$this->template` に書き込むことでテンプレートに渡します。テンプレートの中ではローカル変数として使えるようになります。 - -```php -$this->template->article = $this->articles->getById($id); -``` - -プロパティの値を変数として自動的にテンプレートへ渡すには、`#[TemplateVariable]` アトリビュートを付け、public にします。 .{data-version:3.2.9} - -```php -use Nette\Application\Attributes\TemplateVariable; - -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - #[TemplateVariable] - public string $siteName = 'My blog'; -} -``` - -同じ名前の変数をテンプレートに渡した場合、`#[TemplateVariable]` はそれを上書きしません。 - - -既定の変数 ------ - -プレゼンターとコンポーネントは、いくつかの役立つ変数をテンプレートに自動的に渡します。 - -- `$basePath` はルートディレクトリへの絶対 URL パスです(たとえば `/eshop`) -- `$baseUrl` はルートディレクトリへの絶対 URL です(たとえば `http://localhost/eshop`) -- `$user` は[ユーザーを表す |security:authentication]オブジェクトです -- `$presenter` は現在のプレゼンターです -- `$control` は現在のコンポーネントまたはプレゼンターです -- `$flashes` は `flashMessage()` 関数で送られた[メッセージ |presenters#フラッシュメッセージ]の配列です - -独自のテンプレートクラスを使っている場合、これらの変数はそのためのプロパティを作れば渡されます。 - - -型安全なテンプレート ----------- - -堅牢なアプリケーションを開発するときは、テンプレートがどの変数をどんな型で期待するかを明示的に定義しておくと役立ちます。PHP による型チェック、IDE の賢い補完が得られ、静的解析でエラーを捕まえられるようになります。 - -そうした一覧はどう定義するのでしょうか。テンプレートの変数を表すプロパティを持つクラスとして書くだけです。名前はプレゼンターに似せ、末尾に `Template` を付けます。 - -```php -/** - * @property-read ArticleTemplate $template - */ -class ArticlePresenter extends Nette\Application\UI\Presenter -{ -} - -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - public Model\Article $article; - public Nette\Security\User $user; - - // そしてほかの変数 -} -``` - -これでプレゼンターの `$this->template` オブジェクトは `ArticleTemplate` クラスのインスタンスになります。書き込むとき、PHP が宣言された型を確認してくれます。 - -Nette はテンプレートのクラスを自動的に選びます。まず `Template` という名前のクラス、たとえば `edit` アクションなら `ArticleEditTemplate` を探し、それがない場合にだけ `Template` に戻ります。 - -`@property-read` アノテーションは IDE と静的解析のためのもので、コード補完を可能にします。"PhpStorm と $this⁠-⁠>⁠template のコード補完":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template をご覧ください。 - -[* phpstorm-completion.webp *] - -テンプレートの中でも直接コード補完を使えます。PhpStorm 用の Latte プラグインをインストールし、テンプレートの先頭でテンプレートパラメータのクラス名を指定するだけです。詳しくは [Latte: 型システム |latte:type-system]の章をご覧ください。 - -```latte -{templateType App\Presentation\Article\ArticleTemplate} -... -``` - -同じことがコンポーネントにも当てはまります。命名の決まりに従って、`FifteenControl` のようなコンポーネントには `FifteenTemplate` というパラメータのクラスを作るだけです。 - -別のパラメータのクラスを使う必要があるなら、`createTemplate()` メソッドを使います。 - -```php -public function renderDefault(): void -{ - $template = $this->createTemplate(SpecialTemplate::class); - $template->foo = 123; - // ... - $this->sendTemplate($template); -} -``` - -.{data-version:3.3.0} -描画の前にテンプレートがどう仕上げられるかに手を入れたい場合、たとえばすべてのアクションで共通の変数を足したい場合は、プレゼンターの `completeTemplate()` メソッドを上書きできます。これはテンプレートが描かれる直前に呼ばれます。 - -```php -protected function completeTemplate(Nette\Application\UI\Template $template): void -{ - parent::completeTemplate($template); - $template->siteName = 'My blog'; -} -``` - - -リンクの作成 ------- - -テンプレートでは、ほかのプレゼンターやアクションへのリンクを次のように作ります。 - -```latte -商品の詳細 -``` - -`n:href` 属性は HTML の `` タグにとても便利です。リンクをそれ以外の場所、たとえば文章の中に出したい場合は `{link}` を使います。 - -```latte -URL は次のとおりです: {link Home:default} -``` - -詳しくは [URL リンクの作成|creating-links]の章をご覧ください。 - - -カスタムフィルタ、タグなど -------------- - -Latte のテンプレートシステムは、独自のフィルタ、関数、タグなどで拡張できます。手早いその場しのぎの解から、アプリケーション全体のためのアーキテクチャ上のパターンまで、3 つの方法があります。 - -**プレゼンターのメソッドでその場しのぎに** - -最も手早いのは、プレゼンターやコンポーネントのコードで直接フィルタや関数を足す方法です。プレゼンターでは `beforeRender()` や `render()` メソッドがこれに向いています。 - -```php -protected function beforeRender(): void -{ - // フィルタの追加 - $this->template->addFilter('money', fn($val) => '$' . number_format($val, 2)); - - // 関数の追加 - $this->template->addFunction('isWeekend', fn($date) => $date->format('N') >= 6); -} -``` - -テンプレートでは次のようにします。 - -```latte -

    価格: {$price|money}

    - -{if isWeekend($now)} ... {/if} -``` - -もっと複雑なロジックが必要なら、`Latte\Engine` オブジェクトを直接設定できます。 - -```php -protected function beforeRender(): void -{ - $latte = $this->template->getLatte(); - $latte->setFeature(Latte\Feature::MigrationWarnings); -} -``` - -**アトリビュートを使う** - -より優雅なのは、フィルタと関数をプレゼンターやコンポーネントの[テンプレートパラメータのクラス|#型安全なテンプレート]のメソッドとして定義し、アトリビュートで印を付ける方法です。 - -```php -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - #[Latte\Attributes\TemplateFilter] - public function money(float $val): string - { - return '$' . number_format($val, 2); - } - - #[Latte\Attributes\TemplateFunction] - public function isWeekend(DateTimeInterface $date): bool - { - return $date->format('N') >= 6; - } -} -``` - -Latte はこれらのアトリビュートが付いたメソッドを自動的に見つけて登録します。テンプレートでのフィルタや関数の名前はメソッド名と同じです。これらのメソッドは public でなければなりません。 - -**拡張を使って全体に** - -ここまでの方法は、特定のプレゼンターやコンポーネントでだけ必要なフィルタや関数に向いていて、アプリケーション全体には向きません。アプリケーション全体には[拡張 |latte:extending-latte#Latte Extension]を作るのが最適です。このクラスが、プロジェクトのすべての Latte の拡張をひとつにまとめます。短い例を挙げます。 - -```php -namespace App\Presentation\Accessory; - -final class LatteExtension extends Latte\Extension -{ - public function __construct( - private App\Model\Facade $facade, - private Nette\Security\User $user, - // ... - ) { - } - - public function getFilters(): array - { - return [ - 'timeAgoInWords' => $this->filterTimeAgoInWords(...), - 'money' => $this->filterMoney(...), - // ... - ]; - } - - public function getFunctions(): array - { - return [ - 'canEditArticle' => - fn($article) => $this->facade->canEditArticle($article, $this->user->getId()), - // ... - ]; - } - - private function filterTimeAgoInWords(DateTimeInterface $time): string - { - // ... - } - - // ... -} -``` - -拡張は[設定 |configuration#Latte テンプレート]で登録します。 - -```neon -latte: - extensions: - - App\Presentation\Accessory\LatteExtension -``` - -拡張にはいくつもの利点があります。依存性注入のサポート、アプリケーションのモデル層へのアクセス、そしてすべての拡張を一元的に管理できることです。独自のタグ、プロバイダ、コンパイラパスなどにも対応しています。 - - -すべてのテンプレートの設定 -------------- - -すべてのテンプレートを作る `TemplateFactory` サービスには、コールバックの公開配列 `$onCreate` があります。テンプレートが作られるたびに呼ばれるので、アプリケーションのすべてのテンプレートに対するフィルタ、関数、変数を 1 か所から設定できます。各コールバックは、新しく作られたテンプレートを受け取ります。`TemplateFactory` サービスを[注入してもらい |dependency-injection:passing-dependencies]、たとえばアプリケーションの起動時にコールバックを登録します。 - -```php -$templateFactory->onCreate[] = function (Nette\Bridges\ApplicationLatte\Template $template): void { - $template->addFilter('money', fn($val) => '$' . number_format($val, 2)); -}; -``` - - -翻訳 ---- - -多言語のアプリケーションを作っているなら、テンプレートのテキストを言語ごとに出し分ける必要が出てくるでしょう。そのために Nette Framework は翻訳のインターフェース [api:Nette\Localization\Translator]を定義していて、`translate()` というメソッドをひとつだけ持ちます。メッセージ `$message`(ふつうは文字列です)とそのほかのパラメータを受け取ります。役目は翻訳された文字列を返すことです。Nette には既定の実装がありません。[Componette |https://componette.org/search/localization]にある既製の解決策から、必要に合うものを選べます。トランスレーターの設定方法は、それぞれのドキュメントで説明されています。 - -テンプレートには、[注入してもらった |dependency-injection:passing-dependencies]トランスレーターを `setTranslator()` メソッドで設定できます。 - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator); -} -``` - -あるいはトランスレーターを[設定 |configuration#Latte テンプレート]で指定することもできます。 - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -そうすればトランスレーターを、たとえばフィルタ `|translate` として使えます。`translate()` メソッドに渡される追加のパラメータも書けます(`foo, bar` を参照)。 - -```latte -
    {='カート'|translate} -{$item|translate} -{$item|translate, foo, bar} -``` - -あるいはアンダースコアのタグとして。 - -```latte -{_'カート'} -{_$item} -{_$item, foo, bar} -``` - -テンプレートの一部を翻訳するには、ペアタグ `{translate}` があります(Latte 2.11 以降。以前は `{_}` タグが使われていました)。 - -```latte -{translate}注文{/translate} -{translate foo, bar}注文{/translate} -``` - -トランスレーターはふつう、テンプレートが描かれる実行時に呼ばれます。ただし Latte のバージョン 3 は、すべての静的なテキストをテンプレートのコンパイル時に翻訳できます。各文字列が一度だけ翻訳され、その結果がコンパイル済みの形に書き込まれるので、性能が上がります。この場合、キャッシュディレクトリには言語ごとに複数のコンパイル済みテンプレートができます。そのためには、第 2 パラメータに言語を指定するだけです。 - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator, $lang); -} -``` - -静的なテキストとは、たとえば `{_'hello'}` や `{translate}hello{/translate}` のことです。`{_$foo}` のような静的でないテキストは、引き続き実行時に翻訳されます。 diff --git a/application/ja/upgrading.texy b/application/ja/upgrading.texy deleted file mode 100644 index e69196b350..0000000000 --- a/application/ja/upgrading.texy +++ /dev/null @@ -1,47 +0,0 @@ -アップグレード -******* - - -バージョン 3.0 へのアップグレード -=================== - -Nette 3.0 では、メソッドのパラメータと戻り値に型宣言が加わりました。Nette を継承したクラス(プレゼンターやコンポーネントなど)でそうしたメソッドを上書きしている場合は、同じ型宣言を足さなければなりません。さもないと PHP が「Declaration must be compatible」のエラーを出します。 - -`Nette\Application\IRouter` インターフェースが変わりました。`match()` メソッドは `Nette\Application\Request` オブジェクトの代わりにパラメータの配列を返し、`constructUrl()` はそれを受け取るようになりました。 - -Nette は、各シグナルが同じオリジン(つまり同じドメインとサブドメイン)から送られたかを確認するようになりました。この同一オリジンポリシーは、起こり得る攻撃の経路を減らす助けになる決定的なセキュリティのしくみです。ほかのオリジンを許したい場合は、ハンドラのメソッドに `@crossOrigin` アノテーションを足してください。 - -```php -/** - * @crossOrigin - */ -public function handleXy(): void -{ -} -``` - -これはフォームの送信にも当てはまります。ほかのオリジンからの送信を許したい場合は、次のようにします。 - -```php -$form = new Nette\Application\UI\Form; -$form->allowCrossOrigin(); -``` - -`Nette\ComponentModel\Component` のコンストラクタは何年も使われておらず、バージョン 3.0 で削除されました。これは BC break です。`Nette\Application\UI\Presenter` を継承したコンポーネントやプレゼンターで親のコンストラクタを呼んでいる場合は、その呼び出しを削除しなければなりません。 - - -バージョン 2.4 へのアップグレード -=================== - -- `Route` と `SimpleRouter` は、サイトへのアクセスに使われたのと同じ HTTP/HTTPS のスキームを生成するようになりました。特定のプロトコルを要求するルートは、スキームを付けて定義できます。たとえば `Route('http://domain.cz/')` です。 -- render/action メソッドの bool 型のパラメータ(つまり既定値が true または false のもの)と永続パラメータについて、`false` と `null` が区別されるようになりました。パラメータが URL にない場合、その値は(以前の `false` ではなく)`null` になります。 -- `Presenter::getReflection()` が返すクラスは、もう `Nette\Reflection\ClassType` の子孫ではなく、`getReflection()->getMethod()` も `Nette\Reflection\Method` の子孫ではありません。 -- `SECURED` フラグと `Route::$defaultFlags` は非推奨です。 - - -バージョン 2.3 へのアップグレード -=================== - -- ルートとプレゼンター名は**大文字小文字を区別します**。プレゼンター名で誤った大文字小文字を使うと Nette が警告しますが、性能上の理由から Route のマスクは確認されないので、手で確かめてください。 -- `Route::addStyle()` と `Route::setStyleProperty()` は非推奨で、`E_USER_DEPRECATED` を出すようになりました。 -- テンプレートの拡張子 `.phtml` と古いリンクの構文はサポートされなくなりました。 diff --git a/application/meta.json b/application/meta.json deleted file mode 100644 index 725762aab4..0000000000 --- a/application/meta.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": "4.x", - "repo": "nette/application", - "composer": "nette/application", - "api": "https://api.nette.org/application/" -} diff --git a/application/pl/@home.texy b/application/pl/@home.texy deleted file mode 100644 index 5f8b714738..0000000000 --- a/application/pl/@home.texy +++ /dev/null @@ -1,85 +0,0 @@ -Nette Application -***************** - -.[perex] -Nette Application to rdzeń frameworka Nette, dostarczający potężne narzędzia do tworzenia nowoczesnych aplikacji webowych. Oferuje szereg wyjątkowych możliwości, które znacząco upraszczają tworzenie aplikacji oraz poprawiają bezpieczeństwo kodu i łatwość jego utrzymania. - - -Instalacja ----------- - -Bibliotekę pobierzesz i zainstalujesz za pomocą [Composera|best-practices:composer]: - -```shell -composer require nette/application -``` - - -Dlaczego wybrać Nette Application? ----------------------------------- - -Nette od zawsze było pionierem technologii webowych. - -**Dwukierunkowy router:** Nette dysponuje zaawansowanym systemem routingu, unikalnym dzięki swojej dwukierunkowości - nie tylko tłumaczy URL na akcje aplikacji, ale potrafi też generować URL w drugą stronę. Oznacza to, że: -- strukturę URL całej aplikacji możesz zmienić w dowolnej chwili bez konieczności modyfikowania szablonów -- URL są automatycznie kanonizowane, co poprawia SEO -- routing definiowany jest w jednym miejscu, a nie rozproszony w adnotacjach - -**Komponenty i sygnały:** Wbudowany system komponentów zainspirowany Delphi i React.js jest unikalny wśród frameworków PHP: -- pozwala tworzyć elementy UI wielokrotnego użytku -- wspiera hierarchiczne składanie komponentów -- oferuje eleganckie obsługiwanie żądań AJAX za pomocą sygnałów -- bogata biblioteka gotowych komponentów na [Componette](https://componette.org) - -**AJAX i snippety:** Nette wprowadziło rewolucyjny sposób pracy z AJAX-em już w 2009 roku, na długo przed podobnymi rozwiązaniami, jak Hotwire dla Ruby on Rails czy Symfony UX Turbo: -- snippety pozwalają aktualizować tylko części strony bez konieczności pisania JavaScriptu -- 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 możliwościami: -- automatyczna ochrona przed XSS z escapowaniem zależnym od kontekstu -- rozszerzalny o własne filtry, funkcje i tagi -- dziedziczenie szablonów i snippety dla AJAX-a -- doskonałe wsparcie dla PHP 8.x wraz z systemem typów - -**Wstrzykiwanie zależności:** Nette w pełni korzysta z wstrzykiwania zależności: -- automatyczne przekazywanie zależności (autowiring) -- konfiguracja w przejrzystym formacie NEON -- wsparcie dla fabryk komponentów - - -Główne korzyści ---------------- - -- **Bezpieczeństwo**: automatyczna ochrona przed [podatnościami|nette:vulnerability-protection], takimi jak XSS, CSRF itd. -- **Produktywność**: mniej pisania, więcej możliwości dzięki przemyślanej konstrukcji -- **Debugowanie**: [debugger Tracy|tracy:] z panelem routingu -- **Wydajność**: inteligentny cache, leniwe ładowanie komponentów -- **Elastyczność**: łatwa zmiana URL nawet po ukończeniu aplikacji -- **Komponenty**: unikalny system elementów UI wielokrotnego użytku -- **Nowoczesność**: pełne wsparcie dla PHP 8.3+ i systemu typów - - -Pierwsze kroki --------------- - -1. [Jak działają aplikacje? |how-it-works] - zrozumienie podstawowej architektury -2. [Presentery |presenters] - praca z presenterami i akcjami -3. [Szablony |templates] - tworzenie szablonów w Latte -4. [Routing |routing] - konfigurowanie adresów URL -5. [Komponenty interaktywne |components] - korzystanie z systemu komponentów - - -Zgodność z PHP --------------- - -| wersja | zgodna z PHP -|-----------------------|------------------- -| Nette Application 3.3 | PHP 8.3 - 8.5 -| Nette Application 3.2 | PHP 8.1 - 8.5 -| 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 - -Dotyczy najnowszych wersji patch. diff --git a/application/pl/@left-menu.texy b/application/pl/@left-menu.texy deleted file mode 100644 index ed22b0f063..0000000000 --- a/application/pl/@left-menu.texy +++ /dev/null @@ -1,24 +0,0 @@ -Nette Application -***************** -- [Przegląd |@home] -- [Jak działają aplikacje? |how-it-works] -- [Bootstrapping|bootstrapping] -- [Presentery|presenters] -- [Szablony|templates] -- [Struktura katalogów |directory-structure] -- [Routing|routing] -- [Tworzenie odnośników URL |creating-links] -- [Komponenty interaktywne |components] -- [AJAX i snippety |ajax] -- [Multiplier |multiplier] -- [Konfiguracja|configuration] -- [Aktualizacja|upgrading] - - -Dalsza lektura -************** -- [Dlaczego warto używać Nette?|www:10-reasons-why-nette] -- [Instalacja |nette:installation] -- [Stwórz swoją pierwszą aplikację! |quickstart:] -- [Dobre praktyki |best-practices:] -- [Rozwiązywanie problemów |nette:troubleshooting] diff --git a/application/pl/@meta.texy b/application/pl/@meta.texy deleted file mode 100644 index 61ac92d1af..0000000000 --- a/application/pl/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Dokumentacja Nette}} diff --git a/application/pl/ajax.texy b/application/pl/ajax.texy deleted file mode 100644 index ba5e987831..0000000000 --- a/application/pl/ajax.texy +++ /dev/null @@ -1,265 +0,0 @@ -AJAX i snippety -*************** - -
    - -W erze nowoczesnych aplikacji webowych, w których funkcjonalność często rozłożona jest między serwer a przeglądarkę, AJAX jest niezbędnym elementem łączącym. Jakie możliwości oferuje w tym obszarze Nette Framework? -- wysyłanie części szablonu, zwanych snippetami -- przekazywanie zmiennych między PHP a JavaScriptem -- narzędzia do debugowania żądań AJAX - -
    - - -Żądanie AJAX -============ - -Żądanie AJAX zasadniczo nie różni się od klasycznego żądania HTTP. Wywoływany jest presenter z określonymi parametrami. To do presentera należy decyzja, jak odpowiedzieć na żądanie - może zwrócić dane w formacie JSON, wysłać fragment kodu HTML, dokument XML itd. - -Po stronie przeglądarki inicjujemy żądanie AJAX funkcją `fetch()`: - -```js -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -.then(response => response.json()) -.then(payload => { - // przetwarzamy odpowiedź -}); -``` - -Po stronie serwera żądanie AJAX rozpoznaje metoda `$httpRequest->isAjax()` usługi [enkapsulującej żądanie HTTP |http:request]. Do wykrycia używa nagłówka HTTP `X-Requested-With`, dlatego kluczowe jest jego wysłanie. Wewnątrz presentera możesz użyć metody `$this->isAjax()`. - -Jeśli chcesz wysłać dane w formacie JSON, użyj metody [`sendJson()` |presenters#Wysyłanie odpowiedzi]. Metoda ta kończy również działanie presentera. - -```php -public function actionExport(): void -{ - $this->sendJson($this->model->getData()); -} -``` - -Jeśli planujesz odpowiedzieć specjalnym szablonem przeznaczonym dla AJAX-a, możesz zrobić to tak: - -```php -public function handleClick($param): void -{ - if ($this->isAjax()) { - $this->template->setFile('path/to/ajax.latte'); - } - // ... -} -``` - - -Snippety -======== - -Najpotężniejszym narzędziem, jakie Nette oferuje do łączenia serwera z klientem, są snippety. Dzięki nim zamienisz zwykłą aplikację w aplikację AJAX-ową minimalnym wysiłkiem i kilkoma wierszami kodu. Jak to wszystko działa, pokazuje przykład Fifteen, którego kod znajdziesz na [GitHubie |https://github.com/nette-examples/fifteen]. - -Snippety pozwalają aktualizować tylko części strony zamiast przeładowywać ją całą. Jest to nie tylko szybsze i wydajniejsze, ale daje też wygodniejsze doświadczenie użytkownika. Snippety mogą przypominać Ci Hotwire dla Ruby on Rails albo Symfony UX Turbo. Co ciekawe, Nette wprowadziło snippety 14 lat wcześniej. - -Jak działają snippety? Przy pierwszym wczytaniu strony (żądanie nie-AJAX) wczytywana jest cała strona wraz ze wszystkimi snippetami. Gdy użytkownik wejdzie w interakcję ze stroną (np. kliknie przycisk, wyśle formularz itd.), zamiast przeładowania całej strony inicjowane jest żądanie AJAX. Kod w presenterze wykonuje akcję i decyduje, które snippety trzeba zaktualizować. Nette renderuje te snippety i wysyła je jako payload JSON zawierający tablicę ze snippetami. Kod obsługujący w przeglądarce wstawia następnie otrzymane snippety z powrotem na stronę. Przesyłany jest więc tylko kod zmienionych snippetów, co oszczędza pasmo i przyspiesza ładowanie w porównaniu z przesyłaniem treści całej strony. Jeśli żaden snippet nie zostanie unieważniony przez `redrawControl()`, Nette zwraca całą stronę nawet przy żądaniu AJAX - snippety wysyłane są dopiero wtedy, gdy coś zostanie unieważnione. - - -Naja ----- - -Do obsługi snippetów po stronie przeglądarki służy [biblioteka Naja |https://naja.js.org]. [Zainstaluj ją |https://naja.js.org/#/guide/01-install-setup-naja] jako pakiet Node.js (do użycia z bundlerami, takimi jak Webpack, Rollup, Vite, Parcel i inne): - -```shell -npm install naja -``` - -…albo wstaw ją bezpośrednio do szablonu strony: - -```latte - -``` - -Najpierw trzeba bibliotekę [zainicjować |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization]: - -```js -naja.initialize(); -``` - -Aby zamienić zwykły odnośnik (sygnał) albo wysyłanie formularza w żądanie AJAX, wystarczy oznaczyć odpowiedni odnośnik, formularz albo przycisk klasą `ajax`: - -```latte -Idź - -
    - -
    - -albo - -
    - -
    -``` - - -Przerysowywanie snippetów -------------------------- - -Każdy obiekt klasy [Control |components] (w tym sam Presenter) pilnuje, czy zaszły zmiany wymagające jego przerysowania. Służy do tego metoda `redrawControl()`: - -```php -public function handleLogin(string $user): void -{ - // po zalogowaniu trzeba przerysować odpowiednią część - $this->redrawControl(); - // ... -} -``` - -Nette pozwala na jeszcze precyzyjniejszą kontrolę tego, co trzeba przerysować. Metoda może przyjąć jako argument nazwę snippetu. Można więc unieważniać (czyli wymuszać przerysowanie) na poziomie części szablonu. Jeśli unieważniony zostanie cały komponent, przerysowany zostanie również każdy snippet w jego wnętrzu: - -```php -// unieważnia snippet 'header' -$this->redrawControl('header'); -``` - -Oczekujące unieważnienie możesz też anulować drugim parametrem `$redraw`: wywołanie `$this->redrawControl('header', redraw: false)` oznacza snippet jako niewymagający przerysowania. Pełna sygnatura to `redrawControl(?string $snippet = null, bool $redraw = true)`. - - -Snippety w Latte ----------------- - -Używanie snippetów w Latte jest wyjątkowo łatwe. Aby zdefiniować część szablonu jako snippet, wystarczy opakować ją tagami `{snippet}` i `{/snippet}`: - -```latte -{snippet header} -

    Cześć ...

    -{/snippet} -``` - -Snippet tworzy na stronie HTML element `
    ` ze specjalnym wygenerowanym `id`. Przy przerysowaniu snippetu aktualizowana jest zawartość tego elementu. Dlatego konieczne jest, aby przy początkowym renderowaniu strony wyrenderowane zostały również wszystkie snippety, nawet jeśli na początku bywają puste. - -Snippet możesz też utworzyć na elemencie innym niż `
    `, używając n:atrybutu: - -```latte -
    -

    Cześć ...

    -
    -``` - - -Obszary snippetów ------------------ - -Nazwy snippetów mogą być również wyrażeniami: - -```latte -{foreach $items as $id => $item} -
  • {$item}
  • -{/foreach} -``` - -Samo w sobie jest to niedziałający krok pośredni: wyrenderowany poza statycznym `{snippet}` albo `{snippetArea}` dynamiczny snippet wywołuje `E_USER_WARNING` z komunikatem *Dynamic snippets are allowed only inside static snippet/snippetArea.* Poprawimy to poniżej. - -Powstaje w ten sposób kilka snippetów w rodzaju `item-0`, `item-1` itd. Gdybyśmy unieważnili bezpośrednio dynamiczny snippet (np. `item-1`), nic nie zostałoby przerysowane. Powodem jest to, że snippety naprawdę działają jak wycinki i renderowane są bezpośrednio tylko one same. W szablonie technicznie nie ma jednak snippetu o nazwie `item-1`. Powstaje on dopiero wtedy, gdy wykona się kod otaczający snippet, czyli pętla foreach. Dlatego część szablonu, która ma zostać wykonana, oznaczamy tagiem `{snippetArea}`: - -```latte -
      - {foreach $items as $id => $item} -
    • {$item}
    • - {/foreach} -
    -``` - -I żądamy przerysowania zarówno pojedynczego snippetu, jak i całego obszaru nadrzędnego: - -```php -$this->redrawControl('itemsContainer'); -$this->redrawControl('item-1'); -``` - -Jednocześnie warto zadbać o to, aby tablica `$items` zawierała tylko te elementy, które mają zostać przerysowane. - -Jeśli do głównego szablonu dołączymy tagiem `{include}` inny szablon zawierający snippety, trzeba dołączenie szablonu ponownie opakować w `snippetArea` i unieważnić je razem ze snippetem: - -```latte -{snippetArea include} - {include 'included.latte'} -{/snippetArea} -``` - -```latte -{* included.latte *} -{snippet item} - ... -{/snippet} -``` - -```php -$this->redrawControl('include'); -$this->redrawControl('item'); -``` - - -Snippety w komponentach ------------------------ - -Snippety możesz tworzyć wewnątrz [komponentów|components], a Nette automatycznie je przerysuje. Jest jednak pewne ograniczenie: aby przerysować snippety, Nette wywołuje metodę `render()` bez żadnych parametrów. Przekazywanie parametrów w szablonie nie zadziała więc: - -```latte -OK -{control productGrid} - -nie zadziała: -{control productGrid, $arg, $arg} -{control productGrid:paginator} -``` - - -Wysyłanie własnych danych -------------------------- - -Razem ze snippetami możesz wysłać do klienta dowolne dodatkowe dane. Wystarczy zapisać je do obiektu `payload`: - -```php -public function actionDelete(int $id): void -{ - // ... - if ($this->isAjax()) { - $this->payload->message = 'Sukces'; - } -} -``` - - -Przekierowywanie ----------------- - -Podczas żądania AJAX metody `redirect()` i `redirectUrl()` nie wysyłają przekierowania HTTP. Zamiast tego zapisują docelowy URL do payloadu (obiektu danych wysyłanego w odpowiedzi AJAX), konkretnie do jego właściwości `payload.redirect`, i wysyłają go; samo przekierowanie wykonuje następnie biblioteka po stronie klienta (Naja). - - -Przekazywanie parametrów -======================== - -Wysyłając do komponentu parametry przez żądanie AJAX, niezależnie od tego, czy są to parametry sygnału, czy parametry trwałe, musimy podać w żądaniu ich globalną nazwę, która zawiera również nazwę komponentu. Pełną nazwę parametru zwraca metoda `getParameterId()`. - -```js -let url = new URL({link //foo!}); -url.searchParams.set({$control->getParameterId('bar')}, bar); - -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -``` - -A metoda handle z odpowiadającymi parametrami w komponencie: - -```php -public function handleFoo(int $bar): void -{ -} -``` - - -Dalsza lektura -============== - -- [Dynamiczne snippety |best-practices:dynamic-snippets] diff --git a/application/pl/bootstrapping.texy b/application/pl/bootstrapping.texy deleted file mode 100644 index 13b0288e7b..0000000000 --- a/application/pl/bootstrapping.texy +++ /dev/null @@ -1,312 +0,0 @@ -Bootstrapping -************* - -
    - -Bootstrapping to proces inicjalizacji środowiska aplikacji, tworzenia kontenera wstrzykiwania zależności (DI) i uruchamiania aplikacji. Omówimy: - -- jak klasa Bootstrap inicjalizuje środowisko -- jak konfiguruje się aplikacje za pomocą plików NEON -- jak odróżnić tryb produkcyjny od deweloperskiego -- jak utworzyć i skonfigurować kontener DI - -
    - - -Aplikacje, zarówno webowe, jak i skrypty uruchamiane z wiersza poleceń, zaczynają swoje działanie od jakiejś formy inicjalizacji środowiska. Dawniej odpowiadał za to plik nazywany może `include.inc.php`, dołączany przez plik początkowy. W nowoczesnych aplikacjach Nette zastąpiła go klasa `Bootstrap`, którą jako część aplikacji znajdziesz w pliku `app/Bootstrap.php`. Może wyglądać na przykład tak: - -```php -namespace App; - -use Nette; -use Nette\Bootstrap\Configurator; - -class Bootstrap -{ - private Configurator $configurator; - private string $rootDir; - - public function __construct() - { - $this->rootDir = dirname(__DIR__); - // konfigurator odpowiada za ustawienie środowiska aplikacji i usług - $this->configurator = new Configurator; - // ustawiamy katalog na pliki tymczasowe generowane przez Nette (np. skompilowane szablony) - $this->configurator->setTempDirectory($this->rootDir . '/temp'); - } - - public function bootWebApplication(): Nette\DI\Container - { - $this->initializeEnvironment(); - $this->setupContainer(); - return $this->configurator->createContainer(); - } - - private function initializeEnvironment(): void - { - // Nette jest sprytne i tryb deweloperski włącza się automatycznie, - // albo możesz włączyć go dla konkretnego adresu IP, odkomentowując poniższy wiersz: - // $this->configurator->setDebugMode('secret@23.75.345.200'); - - // włącza Tracy: najlepszy "scyzoryk szwajcarski" do debugowania - $this->configurator->enableTracy($this->rootDir . '/log'); - - // RobotLoader: automatycznie wczytuje wszystkie klasy z wybranego katalogu - $this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); - } - - private function setupContainer(): void - { - // wczytujemy pliki konfiguracyjne - $this->configurator->addConfig($this->rootDir . '/config/common.neon'); - } -} -``` - - -index.php -========= - -W przypadku aplikacji webowych plikiem początkowym jest `index.php`, leżący w [katalogu publicznym |directory-structure#Katalog publiczny www/] `www/`. Zleca on klasie Bootstrap zainicjowanie środowiska i utworzenie kontenera DI. Następnie pobiera z kontenera usługę `Application`, która uruchamia aplikację webową: - -```php -$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); -// uruchamiamy aplikację Nette i obsługujemy przychodzące żądanie -$application->run(); -``` - -.[note] -Obiekt `$application` podczas obsługi żądania emituje [zdarzenia |nette:glossary#Zdarzenia]: `onStartup`, `onRequest`, `onPresenter`, `onResponse`, `onShutdown` i `onError` (przy nieobsłużonym wyjątku). Możesz podpiąć do nich handlery, co przydaje się do logowania albo monitorowania całej aplikacji. - -Jak widać, w ustawieniu środowiska i utworzeniu kontenera wstrzykiwania zależności (DI) pomaga klasa [api:Nette\Bootstrap\Configurator]. Przedstawimy ją teraz szczegółowo. - - -Tryb deweloperski kontra produkcyjny -==================================== - -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 (zapytania SQL, czas wykonania, zużyta pamięć) - - przy błędzie wyświetla szczegółową stronę błędu z wywołaniami funkcji i zawartością zmiennych - - automatycznie odświeża cache przy zmianie szablonów Latte, plików konfiguracyjnych itd. - - -🚀 Tryb produkcyjny: - - nie wyświetla żadnych informacji debugowych, wszystkie błędy zapisuje do logu - - przy błędzie wyświetla ErrorPresenter albo ogólną stronę "Server Error" - - cache nigdy nie odświeża się automatycznie! - - zoptymalizowany pod kątem szybkości i bezpieczeństwa - - -Tryb wybierany jest przez autodetekcję, więc zwykle nie trzeba niczego konfigurować ani przełączać ręcznie: - -- tryb deweloperski: na localhoście (adres IP `127.0.0.1` albo `::1`), o ile nie ma proxy (czyli nie wykryto jego nagłówka HTTP) -- tryb produkcyjny: wszędzie indziej - -Jeśli chcemy włączyć tryb deweloperski również w innych przypadkach, na przykład dla programistów łączących się z konkretnego adresu IP, użyjemy `setDebugMode()`: - -```php -$this->configurator->setDebugMode('23.75.345.200'); // można podać także tablicę adresów IP -``` - -Zdecydowanie zalecamy połączenie adresu IP z ciasteczkiem. W ciasteczku `nette-debug` zapisz tajny token, np. `secret1234`, i w ten sposób aktywuj tryb deweloperski dla programistów łączących się z konkretnego adresu IP, którzy mają w ciasteczku również wspomniany token: - -```php -$this->configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Tryb deweloperski możemy też całkowicie wyłączyć, nawet dla localhosta: - -```php -$this->configurator->setDebugMode(false); -``` - -Zwróć uwagę, że wartość `true` wymusza włączenie trybu deweloperskiego, co **nigdy** nie powinno wydarzyć się na serwerze produkcyjnym. - -Autodetekcją zajmuje się wewnętrznie statyczna metoda `Configurator::detectDebugMode()`, którą możesz wywołać także samodzielnie, na przykład aby wykryć tryb deweloperski poza konfiguratorem. Przyjmuje opcjonalną białą listę adresów IP albo nazw komputerów i zwraca, czy bieżące żądanie ma działać w trybie deweloperskim: - -```php -$debug = Nette\Bootstrap\Configurator::detectDebugMode('23.75.345.200'); -``` - - -Narzędzie do debugowania Tracy -============================== - -Dla łatwego debugowania włączymy znakomite narzędzie [Tracy |tracy:]. W trybie deweloperskim wizualizuje błędy, a w trybie produkcyjnym loguje je do wskazanego katalogu: - -```php -$this->configurator->enableTracy($this->rootDir . '/log'); -``` - - -Pliki tymczasowe -================ - -Nette używa cache dla kontenera DI, RobotLoadera, szablonów itd. Dlatego trzeba ustawić ścieżkę do katalogu, w którym cache będzie przechowywany: - -```php -$this->configurator->setTempDirectory($this->rootDir . '/temp'); -``` - -Na Linuksie albo macOS ustaw katalogom `log/` i `temp/` [prawa do zapisu |nette:troubleshooting#Ustawienie uprawnień do katalogów]. - - -RobotLoader -=========== - -Zwykle będziemy chcieli automatycznie wczytywać klasy za pomocą [RobotLoadera |robot-loader:], musimy więc go uruchomić i pozwolić mu wczytywać klasy z katalogu, w którym leży `Bootstrap.php` (czyli `__DIR__`), oraz ze wszystkich jego podkatalogów: - -```php -$this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); -``` - -Alternatywnym podejściem jest wczytywanie klas wyłącznie przez [Composera |best-practices:composer] zgodnie z PSR-4. - - -Strefa czasowa -============== - -Za pomocą konfiguratora możesz ustawić domyślną strefę czasową. - -```php -$this->configurator->setTimeZone('Europe/Prague'); -``` - - -Konfiguracja kontenera DI -========================= - -Częścią procesu startowego jest utworzenie kontenera DI, czyli fabryki obiektów, która jest sercem całej aplikacji. To w rzeczywistości klasa PHP generowana przez Nette i zapisywana w katalogu cache. Fabryka produkuje kluczowe obiekty aplikacji, a za pomocą plików konfiguracyjnych instruujemy ją, jak ma je tworzyć i ustawiać, wpływając tym samym na zachowanie całej aplikacji. - -Pliki konfiguracyjne zapisywane są zwykle w [formacie NEON |neon:format]. W osobnym rozdziale przeczytasz, [co można skonfigurować |nette:configuring]. - -.[tip] -W trybie deweloperskim kontener aktualizuje się automatycznie przy każdej zmianie kodu albo plików konfiguracyjnych. W trybie produkcyjnym generowany jest tylko raz, a zmiany nie są sprawdzane, aby zmaksymalizować wydajność. - -Podczas gdy `createContainer()` buduje kontener i zwraca jego instancję, metoda `loadContainer()` zwraca tylko nazwę wygenerowanej klasy kontenera, której instancję możesz potem utworzyć samodzielnie. Przydaje się to w zaawansowanych scenariuszach. - -Pliki konfiguracyjne wczytuje się metodą `addConfig()`: - -```php -$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 -$configDir = $this->rootDir . '/config'; -$this->configurator->addConfig($configDir . '/common.neon'); -$this->configurator->addConfig($configDir . '/services.neon'); -if (PHP_SAPI === 'cli') { - $this->configurator->addConfig($configDir . '/cli.php'); -} -``` - -Nazwa `cli.php` nie jest literówką; konfigurację można zapisać także w pliku PHP, który zwraca ją jako tablicę. - -Kolejne pliki konfiguracyjne możemy dodać również [w sekcji `includes` |dependency-injection:configuration#Dołączanie plików]. - -Jeśli w plikach konfiguracyjnych pojawią się elementy o tych samych kluczach, zostaną nadpisane albo, w przypadku [tablic, scalone |dependency-injection:configuration#Scalanie]. Plik dołączony później ma wyższy priorytet niż poprzedni. Plik, w którym wymieniona jest sekcja `includes`, ma wyższy priorytet niż pliki w nim dołączone. - - -Parametry statyczne -------------------- - -Parametry używane w plikach konfiguracyjnych można definiować [w sekcji `parameters` |dependency-injection:configuration#Parametry], a także przekazywać (albo nadpisywać) metodą `addStaticParameters()` (której starszy, obecnie przestarzały alias to `addParameters()`). Ważne jest, że różne wartości parametrów spowodują wygenerowanie kolejnych kontenerów DI, czyli kolejnych klas. - -```php -$this->configurator->addStaticParameters([ - 'projectId' => 23, -]); -``` - -Do parametru `projectId` można odwołać się w konfiguracji standardowym zapisem `%projectId%`. - - -Parametry dynamiczne --------------------- - -Do kontenera możemy dodać również parametry dynamiczne, których różne wartości, w odróżnieniu od parametrów statycznych, nie spowodują wygenerowania nowych kontenerów DI. - -```php -$this->configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -W ten sposób łatwo dodamy na przykład zmienne środowiskowe, do których w konfiguracji można potem odwołać się zapisem `%env.zmienna%`. - -```php -$this->configurator->addDynamicParameters([ - 'env' => getenv(), -]); -``` - - -Parametry domyślne ------------------- - -W plikach konfiguracyjnych możesz używać tych parametrów: - -- `%appDir%` to bezwzględna ścieżka do katalogu zawierającego plik `Bootstrap.php` -- `%wwwDir%` to bezwzględna ścieżka do katalogu zawierającego plik wejściowy `index.php` -- `%tempDir%` to bezwzględna ścieżka do katalogu na pliki tymczasowe -- `%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 -- `%baseUrl%` to bezwzględny URL katalogu głównego (parametr dynamiczny wyliczany w czasie działania) -- `%debugMode%` wskazuje, czy aplikacja działa w trybie debug -- `%consoleMode%` wskazuje, czy żądanie przyszło z wiersza poleceń - - -Usługi importowane ------------------- - -Teraz zejdziemy głębiej. Choć zadaniem kontenera DI jest tworzenie obiektów, czasem może pojawić się potrzeba wstawienia do kontenera istniejącego obiektu. Robimy to, definiując usługę z flagą `imported: true`. - -```neon -services: - myservice: - type: App\Model\MyCustomService - imported: true -``` - -A w bootstrapie wstawiamy obiekt do kontenera: - -```php -$this->configurator->addServices([ - 'myservice' => new App\Model\MyCustomService('foobar'), -]); -``` - - -Różne środowiska -================ - -Śmiało modyfikuj klasę `Bootstrap` według swoich potrzeb. Do metody `bootWebApplication()` możesz dodać parametry odróżniające projekty webowe. Albo możemy dodać inne metody, na przykład `bootTestEnvironment()`, która inicjalizuje środowisko dla testów jednostkowych, `bootConsoleApplication()` dla skryptów wywoływanych z wiersza poleceń itd. - -```php -public function bootTestEnvironment(): Nette\DI\Container -{ - Tester\Environment::setup(); // inicjalizacja Nette Testera - $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/pl/components.texy b/application/pl/components.texy deleted file mode 100644 index ee22bce2e1..0000000000 --- a/application/pl/components.texy +++ /dev/null @@ -1,504 +0,0 @@ -Komponenty interaktywne -*********************** - -
    - -Komponenty to osobne obiekty wielokrotnego użytku, które osadzamy w stronach. Mogą to być formularze, datagridy, ankiety, w zasadzie wszystko, co warto wykorzystywać wielokrotnie. Pokażemy: - -- jak używać komponentów? -- jak je pisać? -- czym są sygnały? - -
    - -Nette ma wbudowany system komponentów. Coś podobnego mogą kojarzyć weterani z Delphi albo ASP.NET Web Forms; React czy Vue.js zbudowane są na czymś odlegle podobnym. W świecie frameworków PHP jest to jednak funkcja unikalna. - -Jednocześnie komponenty zasadniczo wpływają na podejście do tworzenia aplikacji. Możesz składać strony z wcześniej przygotowanych jednostek. Potrzebujesz datagrida w swojej administracji? Znajdź go na [Componette |https://componette.org/search/component], repozytorium dodatków open source (nie tylko komponentów) do Nette, i po prostu wstaw do presentera. - -Do presentera możesz włączyć dowolną liczbę komponentów. A w niektórych komponentach możesz osadzić kolejne. Powstaje w ten sposób drzewo komponentów, którego korzeniem jest presenter. - - -Metody fabryczne -================ - -Jak komponenty trafiają do presentera i jak się ich potem używa? Zwykle za pomocą metod fabrycznych. - -Fabryka komponentów to elegancki sposób tworzenia komponentów dopiero wtedy, gdy są rzeczywiście potrzebne (lazy / on demand). Cała magia polega na zaimplementowaniu metody o nazwie `createComponent()`, gdzie `` to nazwa tworzonego komponentu, która komponent tworzy i zwraca. - -```php .{file:DefaultPresenter.php} -class DefaultPresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentPoll(): PollControl - { - $poll = new PollControl; - $poll->items = $this->items; - return $poll; - } -} -``` - -Ponieważ wszystkie komponenty tworzone są w osobnych metodach, kod staje się przejrzystszy. - -.[note] -Nazwy komponentów zawsze zaczynają się małą literą, mimo że w nazwie metody pisane są wielką. - -Fabryk nigdy nie wywołujemy bezpośrednio; wywoływane są automatycznie przy pierwszym użyciu komponentu. Dzięki temu komponent tworzony jest we właściwym momencie i tylko wtedy, gdy naprawdę jest potrzebny. Jeśli komponentu nie użyjemy (np. przy żądaniu AJAX, gdzie przesyłana jest tylko część strony, albo przy cachowaniu szablonu), w ogóle nie zostanie utworzony, co oszczędza wydajność serwera. - -```php .{file:DefaultPresenter.php} -// sięgamy po komponent i jeśli był to pierwszy raz, -// wywoła się createComponentPoll(), które go utworzy -$poll = $this->getComponent('poll'); -// alternatywny zapis: $poll = $this['poll']; -``` - -W szablonie komponent można wyrenderować tagiem [{control} |#Renderowanie]. Nie ma więc potrzeby ręcznego przekazywania komponentów do szablonu. - -```latte -

    Zagłosuj

    - -{control poll} -``` - -.[tip] -Do dynamicznego tworzenia zmiennej liczby komponentów użyj [Multipliera |multiplier]. - -Metody fabryczne `createComponent()` działają nie tylko w presenterach. W ten sam sposób możesz zagnieździć komponent w innym komponencie, składając je w drzewo - przydaje się to na przykład przy osobno renderowanym formularzu wewnątrz komponentu. - - -Styl hollywoodzki -================= - -Komponenty powszechnie korzystają ze świeżej techniki, którą lubimy nazywać stylem hollywoodzkim. Na pewno znasz frazes, który często słyszą uczestnicy castingów filmowych: "Nie dzwoń do nas, my zadzwonimy do ciebie". I dokładnie o to chodzi. - -W Nette, zamiast nieustannie zadawać pytania ("czy formularz został wysłany?", "czy był poprawny?", "czy użytkownik nacisnął ten przycisk?"), mówisz frameworkowi "gdy to się stanie, wywołaj tę metodę" i zostawiasz mu dalszą pracę. Jeśli programujesz w JavaScripcie, dobrze znasz ten styl programowania. Piszesz funkcje, które wywoływane są, gdy nastąpi określone zdarzenie. A język przekazuje im odpowiednie parametry. - -Całkowicie zmienia to perspektywę pisania aplikacji. Im więcej zadań możesz zostawić frameworkowi, tym mniej masz pracy. I tym mniej możesz przeoczyć. - - -Pisanie komponentu -================== - -Przez pojęcie komponent rozumiemy zwykle potomka klasy [api:Nette\Application\UI\Control]. (Trafniej byłoby używać terminu "controls", ale w niektórych językach ma on inne znaczenie, a "komponenty" bardziej się przyjęły.) Sam presenter [api:Nette\Application\UI\Presenter] również jest potomkiem klasy `Control`. - -```php .{file:PollControl.php} -use Nette\Application\UI\Control; - -class PollControl extends Control -{ -} -``` - - -Renderowanie -============ - -Wiemy już, że do wyrenderowania komponentu służy tag `{control nazwaKomponentu}`. Wywołuje on w rzeczywistości metodę `render()` komponentu, w której zajmujemy się renderowaniem. Mamy do dyspozycji, tak jak w presenterze, [szablon Latte|templates] w zmiennej `$this->template`, do której przekazujemy parametry. W odróżnieniu od presentera musimy podać plik szablonu i kazać go wyrenderować: - -```php .{file:PollControl.php} -public function render(): void -{ - // wstawiamy do szablonu jakieś parametry - $this->template->param = $value; - // i renderujemy go - $this->template->render(__DIR__ . '/poll.latte'); -} -``` - -Tag `{control}` pozwala przekazać metodzie `render()` parametry: - -```latte -{control poll, $id, $message} -``` - -```php .{file:PollControl.php} -public function render(int $id, string $message): void -{ - // ... -} -``` - -Czasem komponent może składać się z kilku części, które chcemy renderować osobno. Dla każdej z nich tworzymy własną metodę renderującą, tutaj w przykładzie `renderPaginator()`: - -```php .{file:PollControl.php} -public function renderPaginator(): void -{ - // ... -} -``` - -A w szablonie wywołujemy ją potem tak: - -```latte -{control poll:paginator} -``` - -Dla lepszego zrozumienia warto wiedzieć, jak ten tag przekłada się na kod PHP. - -```latte -{control poll} -{control poll:paginator, 123, 'hello'} -``` - -przekłada się na: - -```php -$control->getComponent('poll')->render(); -$control->getComponent('poll')->renderPaginator(123, 'hello'); -``` - -Metoda `getComponent()` zwraca komponent `poll`, a na tym komponencie wywoływana jest metoda `render()` albo `renderPaginator()`, jeśli w tagu po dwukropku podano inną metodę renderującą. - -.[caution] -Uwaga, jeśli w parametrach poza nawiasami kwadratowymi pojawi się **`=>`**, wszystkie parametry zostaną opakowane w tablicę i przekazane jako pierwszy argument: - -```latte -{control poll, id: 123, message: 'hello'} -``` - -przekłada się na: - -```php -$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']); -``` - -Renderowanie subkomponentu: - -```latte -{control cartControl-someForm} -``` - -przekłada się na: - -```php -$control->getComponent("cartControl-someForm")->render(); -``` - -Komponenty, podobnie jak presentery, automatycznie przekazują do szablonów kilka przydatnych zmiennych: - -- `$basePath` to bezwzględna ścieżka URL do katalogu głównego (np. `/eshop`) -- `$baseUrl` to bezwzględny URL katalogu głównego (np. `http://localhost/eshop`) -- `$user` to obiekt [reprezentujący użytkownika |security:authentication] -- `$presenter` to bieżący presenter -- `$control` to bieżący komponent -- `$flashes` to tablica [wiadomości |#Wiadomości flash] wysłanych funkcją `flashMessage()` - - -Sygnał -====== - -Wiemy już, że nawigacja w aplikacji Nette polega na linkowaniu albo przekierowywaniu do par `Presenter:akcja`. A co, jeśli chcemy tylko wykonać akcję na **bieżącej stronie**? Na przykład zmienić sortowanie kolumn w tabeli; usunąć element; przełączyć tryb jasny/ciemny; wysłać formularz; zagłosować w ankiecie itd. - -Ten typ żądania nazywa się sygnałem. I tak jak akcje wywołują metody `action()` albo `render()`, sygnały wywołują metody `handle()`. Podczas gdy pojęcie akcji (albo widoku) dotyczy wyłącznie presenterów, sygnały dotyczą wszystkich komponentów. A więc i presenterów, bo `UI\Presenter` jest potomkiem `UI\Control`. - -```php -public function handleClick(int $x, int $y): void -{ - // ... przetwarzanie sygnału ... -} -``` - -Odnośnik wywołujący sygnał tworzy się w zwykły sposób, czyli w szablonie atrybutem `n:href` albo tagiem `{link}`, a w kodzie metodą `link()`. Więcej w rozdziale [Tworzenie odnośników URL |creating-links#Odnośniki do sygnału]. - -```latte -kliknij tutaj -``` - -Sygnał wywoływany jest zawsze na bieżącym presenterze i akcji; nie da się wywołać go na innym presenterze albo innej akcji. - -Sygnał powoduje więc przeładowanie strony dokładnie jak pierwotne żądanie, ale dodatkowo wywołuje metodę obsługującą sygnał z odpowiednimi parametrami. Jeśli metoda nie istnieje, zgłaszany jest wyjątek [api:Nette\Application\UI\BadSignalException], który wyświetlany jest użytkownikowi jako strona błędu 403 Forbidden. - - -Snippety i AJAX -=============== - -Sygnały mogą trochę przypominać Ci AJAX: handlery wywoływane na bieżącej stronie. I masz rację, sygnały rzeczywiście często wywoływane są przez AJAX, a następnie do przeglądarki przesyłane są tylko zmienione części strony. Nazywamy je snippetami. Więcej informacji znajdziesz na [stronie poświęconej AJAX-owi |ajax]. - - -Wiadomości flash -================ - -Komponent ma własny magazyn wiadomości flash, niezależny od presentera. To wiadomości informujące na przykład o wyniku operacji. Ważną cechą wiadomości flash jest to, że są dostępne w szablonie również po przekierowaniu. Nawet po wyświetleniu pozostają aktywne przez kolejne 30 sekund, na przykład na wypadek, gdyby użytkownik odświeżył stronę z powodu błędu transmisji - wiadomość nie zniknie od razu. - -Wysyłaniem zajmuje się metoda [flashMessage |api:Nette\Application\UI\Control::flashMessage()]. Pierwszym parametrem jest tekst wiadomości (`string`, `Stringable`) albo obiekt `stdClass` reprezentujący wiadomość. Opcjonalnym drugim parametrem jest jej typ (error, warning, info itd.). Metoda `flashMessage()` zwraca instancję wiadomości flash jako obiekt `stdClass`, do którego można dodać dalsze informacje. - -```php -$this->flashMessage('Element został usunięty.'); -$this->redirect(/* ... */); // i przekierowujemy -``` - -Wiadomości te dostępne są w szablonie w zmiennej `$flashes` jako obiekty `stdClass`, które zawierają właściwości `message` (tekst wiadomości), `type` (typ wiadomości) i mogą zawierać wspomniane informacje użytkownika. Renderujemy je na przykład tak: - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -Przekierowanie po przetworzeniu sygnału -======================================= - -Po przetworzeniu sygnału komponentu często następuje przekierowanie. Przypomina to formularze - po ich wysłaniu również przekierowujemy, aby zapobiec ponownemu wysłaniu danych przy odświeżeniu strony w przeglądarce. - -```php -$this->redirect('this'); // przekierowuje na bieżący presenter i akcję -``` - -Ponieważ komponent jest elementem wielokrotnego użytku i zwykle nie powinien mieć bezpośredniego powiązania z konkretnymi presenterami, metody `redirect()` i `link()` automatycznie interpretują parametr jako sygnał komponentu: - -```php -$this->redirect('click'); // przekierowuje na sygnał 'click' tego samego komponentu -``` - -Jeśli potrzebujesz przekierować na inny presenter albo inną akcję, możesz zrobić to przez presenter: - -```php -$this->getPresenter()->redirect('Product:show'); // przekierowuje na inny presenter/akcję -``` - - -Parametry trwałe -================ - -Parametry trwałe służą do utrzymywania stanu w komponentach między różnymi żądaniami. Ich wartość pozostaje taka sama również po kliknięciu w odnośnik. W odróżnieniu od danych w sesji przesyłane są w URL. I dzieje się to całkowicie automatycznie, łącznie z odnośnikami tworzonymi w innych komponentach na tej samej stronie. - -Masz na przykład komponent do stronicowania treści. Takich komponentów może być na stronie kilka. I chcemy, aby po kliknięciu w odnośnik wszystkie komponenty pozostały na swojej bieżącej stronie. Dlatego numer strony (`page`) czynimy parametrem trwałym. - -Utworzenie parametru trwałego w Nette jest wyjątkowo proste. Wystarczy utworzyć właściwość publiczną i oznaczyć ją atrybutem: (wcześniej używano `/** @persistent */`) - -```php -use Nette\Application\Attributes\Persistent; // ten wiersz jest ważny - -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; // musi być public -} -``` - -Zalecamy podanie typu danych właściwości (np. `int`), możesz też podać wartość domyślną. Wartości parametrów można [walidować |#Walidacja parametrów trwałych]. - -Przy tworzeniu odnośnika wartość parametru trwałego można zmienić: - -```latte -dalej -``` - -Albo *zresetować*, czyli usunąć z URL. Przyjmie wtedy swoją wartość domyślną: - -```latte -resetuj -``` - - -Komponenty trwałe -================= - -Trwałe mogą być nie tylko parametry, ale też komponenty. Ich parametry trwałe przenoszone są wtedy również między różnymi akcjami presentera albo między wieloma presenterami. Komponenty trwałe oznaczamy atrybutem na klasie presentera. Na przykład komponenty `calendar` i `poll` oznaczymy tak: - -```php -use Nette\Application\Attributes\Persistent; - -#[Persistent('calendar', 'poll')] -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Subkomponentów wewnątrz tych komponentów nie trzeba oznaczać; również stają się trwałe. - -Starsza adnotacja `@persistent` nadal działa, ale jest przestarzała i wywołuje ostrzeżenie: - -```php -/** - * @persistent(calendar, poll) - */ -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Komponenty z zależnościami -========================== - -Jak tworzyć komponenty z zależnościami, nie "zaśmiecając" presenterów, które będą ich używać? Dzięki sprytnym możliwościom kontenera DI w Nette, podobnie jak przy klasycznych usługach, większość pracy można zostawić frameworkowi. - -Weźmy przykład komponentu, który ma zależność od usługi `PollFacade`: - -```php -class PollControl extends Control -{ - public function __construct( - private int $id, // ID ankiety, dla której tworzymy komponent - private PollFacade $facade, - ) { - } - - public function handleVote(int $voteId): void - { - $this->facade->vote($this->id, $voteId); - // ... - } -} -``` - -Gdybyśmy pisali klasyczną usługę, nie byłoby o czym mówić. Kontener DI niewidocznie zająłby się przekazaniem wszystkich zależności. Przy komponentach zwykle jednak radzimy sobie tak, że tworzymy nową instancję bezpośrednio w presenterze, w [metodach fabrycznych |#Metody fabryczne] `createComponent…()`. Ale przekazywanie wszystkich zależności wszystkich komponentów do presentera tylko po to, aby przekazać je dalej do komponentów, jest uciążliwe. A ile kodu do napisania... - -Logiczne pytanie brzmi: dlaczego po prostu nie zarejestrujemy komponentu jako klasycznej usługi, nie przekażemy go do presentera i nie zwrócimy w metodzie `createComponent…()`? To podejście jest jednak niewłaściwe, bo chcemy mieć możliwość tworzenia komponentu w razie potrzeby wielokrotnie. - -Poprawnym rozwiązaniem jest napisanie fabryki komponentu, czyli klasy, która tworzy komponent za nas: - -```php -class PollControlFactory -{ - public function __construct( - private PollFacade $facade, - ) { - } - - public function create(int $id): PollControl - { - return new PollControl($id, $this->facade); - } -} -``` - -Fabrykę tę rejestrujemy w kontenerze w konfiguracji: - -```neon -services: - - PollControlFactory -``` - -i wreszcie używamy jej w naszym presenterze: - -```php -class PollPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private PollControlFactory $pollControlFactory, - ) { - } - - protected function createComponentPollControl(): PollControl - { - $pollId = 1; // możemy przekazać własny parametr - return $this->pollControlFactory->create($pollId); - } -} -``` - -Świetne jest to, że Nette DI potrafi takie proste fabryki [wygenerować |dependency-injection:factory], więc zamiast pisać cały jej kod, wystarczy napisać jej interfejs: - -```php -interface PollControlFactory -{ - public function create(int $id): PollControl; -} -``` - -I to wszystko. Nette wewnętrznie implementuje ten interfejs i wstrzykuje go do presentera, gdzie możemy go użyć. Magicznie dodaje do naszego komponentu parametr `$id` i instancję klasy `PollFacade`. - - -Komponenty w głąb -================= - -Komponenty w Nette Application reprezentują części aplikacji webowej wielokrotnego użytku, które osadzamy w stronach i którym poświęcony jest cały ten rozdział. Jakie dokładnie są możliwości takiego komponentu? - -1) da się go wyrenderować w szablonie -2) wie, [którą swoją część |ajax#Snippety] wyrenderować przy żądaniu AJAX (snippety) -3) ma możliwość przechowywania swojego stanu w URL (parametry trwałe) -4) ma możliwość reagowania na działania użytkownika (sygnały) -5) tworzy strukturę hierarchiczną (której korzeniem jest presenter) - -Za każdą z tych funkcji odpowiada jedna z klas w linii dziedziczenia. Za renderowanie (1 + 2) odpowiada [api:Nette\Application\UI\Control], za włączenie w [cykl życia |presenters#Cykl życia presentera] (3, 4) klasa [api:Nette\Application\UI\Component], a za utworzenie struktury hierarchicznej (5) klasy [Container i Component |component-model:]. - -``` -Nette\ComponentModel\Component { IComponent } -| -+- Nette\ComponentModel\Container { IContainer } - | - +- Nette\Application\UI\Component { SignalReceiver, StatePersistent } - | - +- Nette\Application\UI\Control { Renderable } - | - +- Nette\Application\UI\Presenter { IPresenter } -``` - - -Cykl życia komponentu ---------------------- - -[* lifecycle-component.svg *] *** *Cykl życia komponentu* .<> - - -Walidacja parametrów trwałych ------------------------------ - -Wartości [parametrów trwałych |#Parametry trwałe] otrzymane z URL zapisywane są do właściwości metodą `loadState()`. Sprawdza ona również, czy typ danych podany dla właściwości się zgadza; w przeciwnym razie odpowiada błędem 404 i strona nie zostaje wyświetlona. - -Nigdy nie ufaj ślepo parametrom trwałym, bo użytkownik może je łatwo nadpisać w URL. Tak sprawdzimy na przykład, czy numer strony `$this->page` jest większy od 0. Odpowiednim sposobem jest nadpisanie wspomnianej metody `loadState()`: - -```php -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; - - public function loadState(array $params): void - { - parent::loadState($params); // tutaj ustawiane jest $this->page - // następuje własne sprawdzenie wartości: - if ($this->page < 1) { - $this->error(); - } - } -} -``` - -Odwrotnym procesem, czyli zebraniem wartości z właściwości trwałych, zajmuje się metoda `saveState()`. - - -Podłączenie do presentera -------------------------- - -W chwili, gdy komponent staje się częścią hierarchii presentera, wywoływane są jego callbacki zapisane w tablicy `$onAnchor`. Od tego momentu komponent ma dostępny presenter, może bezpiecznie tworzyć odnośniki, odczytywać parametry trwałe itd. - -```php -$control->onAnchor[] = function ($control): void { - // komponent ma teraz dostępny presenter -}; -``` - - -Sygnały w głąb --------------- - -Sygnał powoduje przeładowanie strony dokładnie jak pierwotne żądanie (poza wywołaniem przez AJAX) i wywołuje metodę `signalReceived($signal)`, której domyślna implementacja w klasie `Nette\Application\UI\Component` próbuje wywołać metodę złożoną ze słów `handle`. Dalsze przetwarzanie zależy od danego obiektu. Obiekty dziedziczące po `Component` (czyli `Control` i `Presenter`) reagują próbą wywołania metody `handle` z odpowiednimi parametrami. - -Innymi słowy: brana jest definicja funkcji `handle` wraz ze wszystkimi parametrami, które przyszły z żądaniem, a parametry z URL przypisywane są do argumentów po nazwie, po czym następuje próba wywołania metody. Na przykład wartość parametru `id` z URL przekazywana jest jako argument `$id`, `something` z URL jako `$something` itd. A jeśli metoda nie istnieje, metoda `signalReceived` zgłasza [wyjątek |api:Nette\Application\UI\BadSignalException]. - -Poza parametrami z URL sygnał odczytuje również parametry wysłane w **ciele POST żądania**. Przydaje się to, bo sygnały często wywoływane są przez JavaScript, w którym naturalne jest wysyłanie danych metodą POST. Jeśli jednak parametr o tej samej nazwie przyjdzie zarówno z URL, jak i z ciała POST, pierwszeństwo ma wartość **z URL**. Unikaj więc nadawania polu POST tej samej nazwy co parametrowi URL albo trasy, bo wartość z URL po cichu by je nadpisała. Parametry sygnału dzielą wspólną przestrzeń z parametrami akcji i parametrami trwałymi, zobacz [Wspólna przestrzeń parametrów |presenters#Wspólna przestrzeń parametrów]. - -Sygnał może odebrać dowolny komponent, presenter albo obiekt implementujący interfejs `SignalReceiver` i podłączony do drzewa komponentów. - -Głównymi odbiorcami sygnałów będą `Presentery` i komponenty wizualne dziedziczące po `Control`. Sygnał ma służyć jako znak dla obiektu, że powinien coś zrobić: ankieta ma policzyć głos użytkownika, blok z newsami ma się rozwinąć i wyświetlić dwa razy więcej newsów, formularz został wysłany i ma przetworzyć dane itd. - -URL sygnału tworzy się metodą [Component::link() |api:Nette\Application\UI\Component::link()]. Jako parametr `$destination` przekazujemy string `{sygnał}!`, a jako `$args` tablicę argumentów, które chcemy przekazać sygnałowi. Sygnał wywoływany jest zawsze na bieżącym presenterze i akcji z bieżącymi parametrami; parametry sygnału są tylko dodawane. Ponadto dodawany jest **parametr `?do`, który określa sygnał**. - -Jego format to albo `{sygnał}`, albo `{odbiorcaSygnału}-{sygnał}`. `{odbiorcaSygnału}` to nazwa komponentu w presenterze. Dlatego w nazwie komponentu nie można użyć myślnika - służy on do oddzielenia nazwy komponentu i sygnału, choć w ten sposób można zagnieżdżać wiele komponentów. - -Metoda [isSignalReceiver()|api:Nette\Application\UI\Presenter::isSignalReceiver()] sprawdza, czy komponent (pierwszy argument) jest odbiorcą sygnału (drugi argument). Drugi argument można pominąć - sprawdzane jest wtedy, czy komponent jest odbiorcą jakiegokolwiek sygnału. Jeśli drugi parametr ustawimy na `true`, sprawdzane jest, czy odbiorcą jest podany komponent albo któryś z jego potomków. - -Na dowolnym etapie poprzedzającym `handle` możemy wykonać sygnał ręcznie, wywołując metodę [processSignal()|api:Nette\Application\UI\Presenter::processSignal()], która zajmuje się obsługą sygnału - bierze komponent zidentyfikowany jako odbiorca sygnału (jeśli odbiorcy nie podano, jest nim sam presenter) i wysyła mu sygnał. - -Przykład: - -```php -if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) { - $this->processSignal(); -} -``` - -Sygnał zostanie w ten sposób wykonany przedwcześnie i nie zostanie wywołany ponownie. diff --git a/application/pl/configuration.texy b/application/pl/configuration.texy deleted file mode 100644 index 0281e17a4b..0000000000 --- a/application/pl/configuration.texy +++ /dev/null @@ -1,186 +0,0 @@ -Konfiguracja aplikacji -********************** - -.[perex] -Przegląd opcji konfiguracyjnych Nette Application. - - -Application -=========== - -```neon -application: - # pokazywać panel "Nette Application" w Tracy BlueScreen? - debugger: ... # (bool) włączone, jeśli Tracy jest dostępne - - # na produkcji wyjątki zawsze obsługuje error-presenter; - # ta opcja włącza to zachowanie również w trybie deweloperskim - catchExceptions: ... # (bool) domyślnie false - czyli wyłączone w dev, na produkcji zawsze włączone - - # nazwa error-presentera - errorPresenter: Error # (string|array) domyślnie 'Nette:Error' - - # definiuje aliasy presenterów i akcji - aliases: ... - - # definiuje reguły tłumaczenia nazwy presentera na klasę - mapping: ... - - # wyciszać ostrzeżenia o nieprawidłowych odnośnikach? - # działa tylko w trybie deweloperskim - silentLinks: ... # (bool) domyślnie false -``` - -Od wersji 3.2 pakietu `nette/application` można zdefiniować parę error-presenterów: - -```neon -application: - errorPresenter: - 4xx: Error4xx # dla Nette\Application\BadRequestException - 5xx: Error5xx # dla pozostałych wyjątków -``` - -Rozdzielenie ich przydaje się, bo obie sytuacje są zasadniczo różne. `BadRequestException` (kody 4xx) oznacza, że z aplikacją wszystko w porządku, a jedynie odwiedzający poprosił o coś, co nie istnieje. Możesz więc użyć pełnoprawnego presentera, który wyświetli przyjazny komunikat w layoucie Twojej witryny. Odwrotnie, błąd 5xx oznacza, że w aplikacji coś się zepsuło i nie wiadomo co. Presenter dla 5xx trzymaj możliwie minimalny, aby przy jego renderowaniu nie mogło zawieść nic więcej - najlepiej, aby nie dotykał bazy danych, layoutu ani zalogowanego użytkownika. - -Opcja `silentLinks` określa, jak Nette zachowuje się w trybie deweloperskim, gdy generowanie odnośnika się nie powiedzie (na przykład dlatego, że presenter nie istnieje itd.). Wartość domyślna `false` oznacza, że Nette zgłasza błąd `E_USER_WARNING`. Ustawienie na `true` wycisza ten komunikat. W środowisku produkcyjnym `E_USER_WARNING` zgłaszany jest zawsze. Na to zachowanie można wpłynąć również ustawieniem zmiennej presentera [$invalidLinkMode |creating-links#Nieprawidłowe odnośniki]. - -[Aliasy upraszczają odwoływanie się |creating-links#Aliasy] do często używanych presenterów. - -[Mapowanie definiuje reguły |directory-structure#Mapowanie presenterów], według których z nazwy presentera wyprowadzana jest nazwa klasy. - - -Automatyczna rejestracja presenterów ------------------------------------- - -Nette automatycznie dodaje presentery jako usługi do kontenera DI, co znacząco przyspiesza ich tworzenie. To, jak Nette odnajduje presentery, można skonfigurować: - -```neon -application: - # szukać presenterów w class mapie Composera? - scanComposer: ... # (bool) domyślnie true - - # maska, do której musi pasować nazwa klasy i pliku - scanFilter: ... # (string) domyślnie '*Presenter' - - # w których katalogach szukać presenterów? - scanDirs: # (string[]|false) domyślnie '%appDir%' - - %vendorDir%/mymodule -``` - -Katalogi wymienione w `scanDirs` nie nadpisują wartości domyślnej `%appDir%`, lecz ją uzupełniają, więc `scanDirs` będzie zawierać obie ścieżki: `%appDir%` i `%vendorDir%/mymodule`. Jeśli chcemy pominąć katalog domyślny, użyjemy [wykrzyknika |dependency-injection:configuration#Scalanie]: - -```neon -application: - scanDirs!: - - %vendorDir%/mymodule -``` - -Skanowanie katalogów można wyłączyć, ustawiając wartość na `false`. Presentery nie są wtedy rejestrowane jako usługi, więc nie da się ich dostosować przez sekcję [decorator |dependency-injection:configuration#Dekorator], a ich tworzenie jest wolniejsze. Nie zalecamy więc całkowitego wyłączania automatycznej rejestracji, bo obniży to wydajność aplikacji. - - -Szablony Latte -============== - -To ustawienie globalnie wpływa na zachowanie Latte w komponentach i presenterach. - -```neon -latte: - # pokazywać panel Latte w pasku Tracy dla głównego szablonu (true), czy dla wszystkich komponentów (all)? - debugger: ... # (true|false|'all') włączone, jeśli Tracy jest dostępne (tylko w trybie debug) - - # generować szablony z nagłówkiem declare(strict_types=1) - strictTypes: ... # (bool) domyślnie false - - # włącza [tryb ścisłego parsera |latte:develop#strict mode] - strictParsing: ... # (bool) domyślnie false - - # ogranicza zasięg zmiennych do ciała pętli - scopedLoopVariables: ... # (bool) domyślnie false - - # usuwa wcięcia powstałe z zagnieżdżenia w tagach parzystych - dedent: ... # (bool) domyślnie false - - # włącza [kontrolę wygenerowanego kodu |latte:develop#Checking Generated Code] - phpLinter: ... # (string) domyślnie null - - # ustawia locale - locale: cs_CZ # (string) domyślnie null - - # klasa obiektu $this->template - templateClass: App\MyTemplateClass # domyślnie Nette\Bridges\ApplicationLatte\DefaultTemplate -``` - -Nowe [rozszerzenia |latte:extending-latte#Latte Extension] dodasz tak: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - - -Routing -======= - -Podstawowe ustawienia: - -```neon -routing: - # pokazywać panel routingu w pasku Tracy? - debugger: ... # (bool) włączone, jeśli Tracy jest dostępne (tylko w trybie debug) - - # serializować router do kontenera DI - cache: ... # (bool) domyślnie false -``` - -Routing definiuje się zwykle w klasie [RouterFactory |routing#Kolekcja tras]. Alternatywnie trasy można definiować również w konfiguracji za pomocą par `maska: akcja`, ale ten sposób nie daje zbyt dużej elastyczności: - -```neon -routing: - routes: - 'detail/': Admin:Home:default - '/': Front:Home:default -``` - - -Stałe -===== - -Tworzenie stałych PHP. - -```neon -constants: - Foobar: 'baz' -``` - -Stała `Foobar` zostanie utworzona po starcie aplikacji. - -.[note] -Stałe nie powinny służyć jako globalnie dostępne zmienne. Do przekazywania wartości do obiektów używaj [wstrzykiwania zależności |dependency-injection:passing-dependencies]. - - -PHP -=== - -Ustawianie dyrektyw PHP. Przegląd wszystkich dyrektyw znajdziesz na [php.net |https://www.php.net/manual/en/ini.list.php]. - -```neon -php: - date.timezone: Europe/Prague -``` - - -Usługi DI -========= - -Do kontenera DI dodawane są te usługi: - -| Nazwa | Typ | Opis -|----------------------------|---------------------------------------------------|----------------------------------------- -| `application.application` | [api:Nette\Application\Application] | [uruchamiacz aplikacji |how-it-works#Nette Application] -| `application.linkGenerator` | [api:Nette\Application\LinkGenerator] | [LinkGenerator |creating-links#LinkGenerator] -| `application.presenterFactory` | [api:Nette\Application\IPresenterFactory] | fabryka presenterów -| `application.###` | [api:Nette\Application\UI\Presenter] | poszczególne presentery -| `routing.router` | [api:Nette\Routing\Router] | router -| `latte.latteFactory` | [api:Nette\Bridges\ApplicationLatte\LatteFactory] | fabryka obiektu `Latte\Engine` -| `latte.templateFactory` | [api:Nette\Application\UI\TemplateFactory] | fabryka [`$this->template` |templates] diff --git a/application/pl/creating-links.texy b/application/pl/creating-links.texy deleted file mode 100644 index 59d9c3653f..0000000000 --- a/application/pl/creating-links.texy +++ /dev/null @@ -1,319 +0,0 @@ -Tworzenie odnośników URL -************************ - -
    - -Tworzenie odnośników w Nette jest tak proste jak wskazanie palcem. Wystarczy wycelować, a framework wykona za Ciebie całą pracę. Pokażemy: - -- jak tworzyć odnośniki w szablonach i gdzie indziej -- jak odróżnić odnośnik do bieżącej strony -- co zrobić z nieprawidłowymi odnośnikami - -
    - - -Dzięki [dwukierunkowemu routingowi |routing] nigdy nie będziesz musiał zapisywać na sztywno w szablonach ani w kodzie URL swojej aplikacji, które mogą się później zmienić albo których składanie bywa skomplikowane. W odnośniku podajesz jedynie presenter i akcję, przekazujesz ewentualne parametry, a framework sam wygeneruje URL. Właściwie przypomina to bardzo wywołanie funkcji. Spodoba Ci się to. - - -W szablonie presentera -====================== - -Najczęściej tworzymy odnośniki w szablonach i świetnym pomocnikiem jest atrybut `n:href`: - -```latte -szczegóły -``` - -Zwróć uwagę, że zamiast atrybutu HTML `href` użyliśmy [n:atrybutu |latte:syntax#n:atrybuty] `n:href`. Jego wartością nie jest URL, jak byłoby w przypadku atrybutu `href`, lecz nazwa presentera i akcji. - -Kliknięcie w odnośnik to, mówiąc prosto, coś w rodzaju wywołania metody `ProductPresenter::renderShow()`. A jeśli ma ona w sygnaturze parametry, możemy wywołać ją z argumentami: - -```latte -szczegóły produktu -``` - -Można też przekazywać parametry nazwane. Poniższy odnośnik przekazuje parametr `lang` o wartości `en`: - -```latte -szczegóły produktu -``` - -Jeśli metoda `ProductPresenter::renderShow()` nie ma w sygnaturze `$lang`, może pobrać wartość parametru przez `$lang = $this->getParameter('lang')` albo z [właściwości |presenters#Parametry żądania]. - -Jeśli parametry są w tablicy, można je rozwinąć operatorem `...`: - -```latte -{var $args = [$product->id, lang => en]} -szczegóły produktu -``` - -W odnośnikach automatycznie przekazywane są również tak zwane [parametry trwałe |presenters#Parametry trwałe]. - -Atrybut `n:href` jest bardzo poręczny dla tagów HTML ``. Jeśli chcemy wypisać odnośnik gdzie indziej, na przykład w tekście, użyjemy `{link}`: - -```latte -URL to: {link Home:default} -``` - - -W kodzie -======== - -Do utworzenia odnośnika w presenterze służy metoda `link()`: - -```php -$url = $this->link('Product:show', $product->id); -``` - -Parametry można przekazać także jako tablicę, w której można podać również parametry nazwane: - -```php -$url = $this->link('Product:show', [$product->id, 'lang' => 'en']); -``` - -Odnośniki można tworzyć również bez presentera, za pomocą [#LinkGenerator] i jego metody `link()`. - -Czasem potrzebujesz utworzyć odnośnik teraz, ale wygenerować rzeczywisty URL dopiero później. Służy do tego metoda `lazyLink()`, która zwraca obiekt `Nette\Application\UI\Link`. Zaletą jest to, że możesz ten obiekt przekazać dalej, na przykład do szablonu, i przed jego wyrenderowaniem nadal dostosowywać jego parametry metodą `setParameter()`. Sam URL składany jest dopiero wtedy, gdy obiekt zostanie skonwertowany na string: - -```php -$link = $this->lazyLink('Product:show', $id); -// ... -echo $link; // URL generowany jest dopiero tutaj -``` - - -Odnośniki do presentera -======================= - -Jeśli celem odnośnika jest presenter i akcja, ma on taką składnię: - -``` -[//] [[[[:]moduł:]presenter:]akcja | this] [#fragment] -``` - -Format ten obsługują wszystkie tagi Latte i wszystkie metody presentera pracujące z odnośnikami, czyli `n:href`, `{link}`, `{plink}`, `link()`, `lazyLink()`, `isLinkCurrent()`, `redirect()`, `redirectPermanent()`, `forward()`, `canonicalize()`, a także [#LinkGenerator]. Choć więc w przykładach używane jest `n:href`, mogłaby tam stać dowolna z tych funkcji. - -Podstawową postacią jest więc `Presenter:akcja`: - -```latte -strona główna -``` - -Jeśli linkujemy do akcji bieżącego presentera, możemy pominąć jego nazwę: - -```latte -strona główna -``` - -Jeśli akcją docelową jest `default`, możemy ją pominąć, ale dwukropek musi zostać: - -```latte -strona główna -``` - -Odnośniki mogą wskazywać także na inne [moduły |directory-structure#Presentery i szablony]. Rozróżniamy tu odnośniki względne wobec zagnieżdżonego submodułu albo bezwzględne. Zasada jest analogiczna do ścieżek dyskowych, tyle że zamiast ukośników używa się dwukropków. Zakładając, że bieżący presenter jest częścią modułu `Front`, zapisalibyśmy: - -```latte -odnośnik do Front:Shop:Product:show -odnośnik do Admin:Product:show -``` - -Szczególnym przypadkiem jest odnośnik [do samego siebie |#Odnośnik do bieżącej strony], gdzie jako cel podajemy `this`. - -```latte -odśwież -``` - -Do konkretnej części strony możemy linkować przez tak zwany fragment po znaku kratki `#`: - -```latte -odnośnik do Home:default i fragmentu #main -``` - -.{data-version:3.3.0} -Fragment można też ustawić dynamicznie jako argument z kluczem `#`. Jego wartość jest automatycznie kodowana i ma pierwszeństwo przed fragmentem podanym w celu: - -```php -$this->link('Home:default', ['#' => $fragment]); -``` - - -Ścieżki bezwzględne -=================== - -Odnośniki generowane przez `link()` albo `n:href` są zawsze ścieżkami bezwzględnymi (czyli zaczynają się od `/`), ale nie bezwzględnymi URL z protokołem i domeną, jak `https://domain`. - -Aby wygenerować bezwzględny URL, dodaj na początku dwa ukośniki (np. `n:href="//Home:"`). Alternatywnie możesz przełączyć presenter tak, aby generował wyłącznie odnośniki bezwzględne, ustawiając `$this->absoluteUrls = true`. - -Do zamiany ścieżki względnej na bezwzględną można też użyć w szablonie filtra `|absoluteUrl`. - - -Odnośnik do bieżącej strony -=========================== - -Cel `this` tworzy odnośnik do bieżącej strony: - -```latte -odśwież -``` - -Jednocześnie przekazywane są wszystkie parametry podane w sygnaturze metody `action()` albo `render()` (jeśli `action()` nie jest zdefiniowana). Jeśli więc jesteśmy na stronie `Product:show` z `id: 123`, odnośnik do `this` przekaże również ten parametr. - -Oczywiście parametry można podać bezpośrednio: - -```latte -odśwież -``` - -Funkcja `isLinkCurrent()` sprawdza, czy cel odnośnika jest identyczny z bieżącą stroną. Można tego użyć na przykład w szablonie do odróżniania odnośników itd. - -Parametry są takie same jak dla metody `link()`, ale zamiast konkretnej akcji można użyć symbolu wieloznacznego `*`, który oznacza dowolną akcję danego presentera. - -```latte -{if !isLinkCurrent('Admin:login')} - Zaloguj się -{/if} - -
  • - ... -
  • -``` - -W połączeniu z `n:href` w jednym elemencie można użyć postaci skróconej: - -```latte -... -``` - -Symbolu wieloznacznego `*` można użyć tylko zamiast akcji, nie presentera. - -Aby ustalić, czy jesteśmy w konkretnym module albo jego submodule, użyj metody `isModuleCurrent(moduleName)`. - -```latte -
  • - ... -
  • -``` - - -Zmiana bazy odnośników .{data-version:3.2.7} -============================================ - -Domyślnie odnośniki względne wyprowadzane są z bieżącego presentera. Można to zmienić za pomocą `{linkBase}`: - -```latte -{linkBase Admin:Dashboard} -szczegóły produktu -``` - -Odnośnik poprowadzi do `Admin:Dashboard:Product:show`. Dotyczy to wyłącznie odnośników względnych - odnośniki bezwzględne zaczynające się dwukropkiem oraz odnośniki do bieżącego presentera (`this`, `show`) pozostają bez zmian. - -`{linkBase}` obowiązuje w całym szablonie i przydaje się zwłaszcza w szablonach layoutu, gdzie zapewnia spójne odnośniki niezależnie od wywołującego presentera. -Tag musi znaleźć się na początku szablonu, w przeciwnym razie zgłasza `CompileException`. - - -Odnośniki do sygnału -==================== - -Celem odnośnika nie musi być tylko presenter i akcja, ale też [sygnał |components#Sygnał] (wywołuje wtedy metodę `handle()`). Składnia wygląda wtedy tak: - -``` -[//] [subkomponent:]sygnał! [#fragment] -``` - -Sygnał odróżnia więc wykrzyknik: - -```latte -sygnał -``` - -Możesz też utworzyć odnośnik do sygnału subkomponentu (albo sub-subkomponentu): - -```latte -sygnał -``` - - -Odnośniki w komponencie -======================= - -Ponieważ [komponenty|components] są osobnymi jednostkami wielokrotnego użytku, które nie powinny mieć żadnych powiązań z otaczającymi presenterami, odnośniki działają tu nieco inaczej. Atrybut Latte `n:href` i tag `{link}`, a także metody komponentu, takie jak `link()` i inne, **zawsze traktują cel odnośnika jako nazwę sygnału**. Nie trzeba więc nawet dodawać wykrzyknika: - -```latte -sygnał, nie akcja -``` - -Gdybyśmy chcieli w szablonie komponentu linkować do presenterów, użyjemy tagu `{plink}`: - -```latte -strona główna -``` - -albo w kodzie - -```php -$this->getPresenter()->link('Home:default') -``` - - -Aliasy .{data-version:3.2.3} -============================ - -Czasem przydatne bywa przypisanie parze Presenter:akcja łatwego do zapamiętania aliasu. Na przykład nazwanie strony głównej `Front:Home:default` po prostu `home`, a `Admin:Dashboard:default` jako `admin`. - -Aliasy definiuje się w [konfiguracji|configuration] pod kluczem `application › aliases`: - -```neon -application: - aliases: - home: Front:Home:default - admin: Admin:Dashboard:default - sign: Front:Sign:in -``` - -W odnośnikach zapisuje się je potem za pomocą małpy, na przykład: - -```latte -administracja -``` - -Obsługiwane są też we wszystkich metodach pracujących z odnośnikami, takich jak `redirect()` i podobne. - - -Nieprawidłowe odnośniki -======================= - -Może się zdarzyć, że utworzymy nieprawidłowy odnośnik: albo dlatego, że prowadzi do nieistniejącego presentera, albo dlatego, że przekazuje więcej parametrów, niż przyjmuje w sygnaturze metoda docelowa, albo gdy dla docelowej akcji nie da się wygenerować URL. Sposób obsługi nieprawidłowych odnośników ustawia się w presenterze przez `$this->invalidLinkMode`. Może przyjmować kombinację tych wartości (stałych): - -- `Presenter::InvalidLinkSilent` - tryb cichy, jako URL zwraca znak # -- `Presenter::InvalidLinkWarning` - zgłaszane jest ostrzeżenie E_USER_WARNING, które w trybie produkcyjnym zostanie zalogowane, ale nie przerwie wykonywania skryptu -- `Presenter::InvalidLinkTextual` - ostrzeżenie wizualne, wypisuje błąd bezpośrednio w odnośniku -- `Presenter::InvalidLinkException` - zgłasza InvalidLinkException - -Ustawieniem domyślnym jest `InvalidLinkWarning` w trybie produkcyjnym i `InvalidLinkWarning | InvalidLinkTextual` w trybie deweloperskim. `InvalidLinkWarning` w środowisku produkcyjnym nie powoduje przerwania skryptu, ale ostrzeżenie zostanie zalogowane. W środowisku deweloperskim przechwytuje je [Tracy |tracy:] i wyświetla bluescreen. `InvalidLinkTextual` działa tak, że jako URL zwraca komunikat o błędzie zaczynający się od znaków `#error:`. Aby takie odnośniki rzucały się w oczy na pierwszy rzut oka, dodaj do swojego CSS: - -```css -a[href^="#error:"] { - background: red; - color: white; -} -``` - -Jeśli nie chcemy, aby w środowisku deweloperskim powstawały ostrzeżenia, możemy wyciszyć je bezpośrednio w [konfiguracji|configuration]. - -```neon -application: - silentLinks: true -``` - - -LinkGenerator -============= - -Jak tworzyć odnośniki z podobną wygodą jak metodą `link()`, ale bez obecności presentera? Od tego jest [api:Nette\Application\LinkGenerator]. - -LinkGenerator to usługa, którą możesz otrzymać przez konstruktor, a następnie tworzyć odnośniki jej metodą `link()`. - -W porównaniu z presenterami jest różnica. LinkGenerator tworzy wszystkie odnośniki bezpośrednio jako bezwzględne URL. Ponadto nie istnieje "bieżący presenter", nie da się więc podać jako celu samej nazwy akcji `link('default')` ani używać ścieżek względnych do modułów. - -Nieprawidłowe odnośniki zawsze zgłaszają `Nette\Application\UI\InvalidLinkException`. diff --git a/application/pl/directory-structure.texy b/application/pl/directory-structure.texy deleted file mode 100644 index 4df3dbebdd..0000000000 --- a/application/pl/directory-structure.texy +++ /dev/null @@ -1,526 +0,0 @@ -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ą uporządkować kod. Dowiesz się: - -- jak **logicznie podzielić** aplikację na katalogi -- jak zaprojektować strukturę, aby **dobrze się skalowała** wraz z rozwojem projektu -- jakie są **możliwe alternatywy** oraz ich zalety i wady - -
    - - -Warto wspomnieć, że sam Nette Framework nie wymusza żadnej konkretnej struktury. Zaprojektowano go tak, aby dawał się łatwo dostosować do dowolnych potrzeb i preferencji. - - -Podstawowa struktura projektu -============================= - -Choć Nette Framework nie narzuca żadnej sztywnej struktury katalogów, istnieje sprawdzony układ domyślny w postaci [Web Project|https://github.com/nette/web-project]: - -/--pre -web-project/ -├── app/ ← katalog aplikacji -├── assets/ ← pliki SCSS, JS, obrazy..., alternatywnie resources/ -├── bin/ ← skrypty do wiersza poleceń -├── config/ ← konfiguracja -├── log/ ← zalogowane błędy -├── temp/ ← pliki tymczasowe, cache -├── tests/ ← testy -├── vendor/ ← biblioteki zainstalowane przez Composera -└── www/ ← katalog publiczny (document-root) -\-- - -Strukturę tę możesz swobodnie modyfikować według swoich potrzeb: zmieniać nazwy folderów albo je przenosić. Wystarczy wtedy dostosować ścieżki względne do katalogów w `Bootstrap.php` i ewentualnie `composer.json`. Nic więcej nie jest potrzebne, żadnej skomplikowanej rekonfiguracji, żadnych zmian stałych. Nette ma sprytną autodetekcję i samo rozpoznaje położenie aplikacji wraz z jej bazowym URL. - - -Zasady organizacji kodu -======================= - -Gdy po raz pierwszy poznajesz nowy projekt, powinieneś móc szybko się w nim odnaleźć. Wyobraź sobie, że klikasz w katalog `app/Model/` i widzisz taką strukturę: - -/--pre -app/Model/ -├── Services/ -├── Repositories/ -└── Entities/ -\-- - -Dowiadujesz się z tego tylko tyle, że projekt używa jakichś usług, repozytoriów i encji. O rzeczywistym przeznaczeniu aplikacji nie dowiadujesz się niczego. - -Spójrzmy na inne podejście - **organizację według domen**: - -/--pre -app/Model/ -├── Cart/ -├── Payment/ -├── Order/ -└── Product/ -\-- - -Tutaj jest inaczej - na pierwszy rzut oka widać, że chodzi o sklep internetowy. Same nazwy katalogów zdradzają, co aplikacja potrafi: pracuje z płatnościami, zamówieniami i produktami. - -Pierwsze podejście (organizacja według typu klas) przynosi w praktyce kilka problemów: kod logicznie powiązany jest rozproszony po różnych folderach i trzeba między nimi skakać. Dlatego będziemy organizować według domen. - - -Przestrzenie nazw ------------------ - -Zwyczajowo struktura katalogów odpowiada przestrzeniom nazw w aplikacji. Oznacza to, że fizyczne położenie plików zgadza się z ich przestrzenią nazw. Na przykład klasa leżąca w `app/Model/Product/ProductRepository.php` powinna mieć przestrzeń nazw `App\Model\Product`. Zasada ta pomaga w poruszaniu się po kodzie i upraszcza autoloading. - - -Liczba pojedyncza kontra mnoga w nazwach ----------------------------------------- - -Zauważ, że dla głównych katalogów aplikacji używamy liczby pojedynczej: `app`, `config`, `log`, `temp`, `www`. To samo dotyczy wnętrza aplikacji: `Model`, `Core`, `Presentation`. Dzieje się tak dlatego, że każdy z nich reprezentuje jedno spójne pojęcie. - -Podobnie `app/Model/Product` reprezentuje wszystko, co dotyczy produktów. Nie nazywamy go `Products`, bo to nie jest folder pełen produktów (zawierałby pliki w rodzaju `nokia.php`, `samsung.php`). To przestrzeń nazw zawierająca klasy do pracy z produktami: `ProductRepository.php`, `ProductService.php`. - -Folder `app/Tasks` jest w liczbie mnogiej, bo zawiera zestaw osobnych wykonywalnych skryptów: `CleanupTask.php`, `ImportTask.php`. Każdy z nich jest niezależną jednostką. - -Dla spójności zalecamy używać: -- liczby pojedynczej dla przestrzeni nazw reprezentujących jednostkę funkcjonalną (nawet jeśli pracuje z wieloma encjami) -- liczby mnogiej dla zbiorów niezależnych jednostek -- w razie wątpliwości albo gdy nie chcesz się nad tym zastanawiać, wybierz liczbę pojedynczą - - -Katalog publiczny `www/` -======================== - -Ten katalog jako jedyny jest dostępny z sieci (document-root). Często możesz spotkać się z nazwą `public/` zamiast `www/` - to tylko kwestia konwencji i nie wpływa na działanie aplikacji. Katalog zawiera: -- [punkt wejścia |bootstrapping#index.php] aplikacji `index.php` -- plik `.htaccess` z regułami mod_rewrite (dla Apache) -- pliki statyczne (CSS, JavaScript, obrazy) -- przesłane pliki - -Dla właściwego bezpieczeństwa aplikacji kluczowe jest [poprawne skonfigurowanie document-root |nette:troubleshooting#Jak zmienić albo usunąć katalog www z URL?]. - -.[note] -Nigdy nie umieszczaj w tym katalogu folderu `node_modules/` - zawiera tysiące plików, które mogą być wykonywalne i nie powinny być publicznie dostępne. - - -Katalog aplikacji `app/` -======================== - -To główny katalog zawierający kod aplikacji. Podstawowa struktura: - -/--pre -app/ -├── Core/ ← sprawy infrastrukturalne -├── Model/ ← logika biznesowa -├── Presentation/ ← presentery i szablony -├── Tasks/ ← skrypty poleceń -└── Bootstrap.php ← klasa startowa aplikacji -\-- - -`Bootstrap.php` to [klasa startowa aplikacji|bootstrapping], która inicjalizuje środowisko, wczytuje konfigurację i tworzy kontener DI. - -Przyjrzyjmy się teraz poszczególnym podkatalogom szczegółowo. - - -Presentery i szablony -===================== - -Warstwa prezentacji aplikacji leży w katalogu `app/Presentation`. Alternatywą jest krótsze `app/UI`. To miejsce na wszystkie presentery, ich szablony i ewentualne powiązane klasy pomocnicze. - -Warstwę tę organizujemy według domen. W złożonym projekcie łączącym sklep internetowy, blog i API struktura wyglądałaby tak: - -/--pre -app/Presentation/ -├── Shop/ ← frontend sklepu -│ ├── Product/ -│ ├── Cart/ -│ └── Order/ -├── Blog/ ← blog -│ ├── Home/ -│ └── Post/ -├── Admin/ ← administracja -│ ├── Dashboard/ -│ └── Products/ -└── Api/ ← endpointy API - └── V1/ -\-- - -Odwrotnie, dla prostego bloga użylibyśmy takiej struktury: - -/--pre -app/Presentation/ -├── Front/ ← frontend witryny -│ ├── Home/ -│ └── Post/ -├── Admin/ ← administracja -│ ├── Dashboard/ -│ └── Posts/ -├── Error/ -└── Export/ ← RSS, mapy strony itd. -\-- - -Foldery w rodzaju `Home/` czy `Dashboard/` zawierają presentery i szablony. Foldery w rodzaju `Front/`, `Admin/` czy `Api/` nazywamy **modułami**. Technicznie to zwykłe katalogi służące do logicznego podziału aplikacji. - -Każdy folder zawierający presenter obejmuje sam plik presentera i jego szablony. Na przykład folder `Dashboard/` zawiera: - -/--pre -Dashboard/ -├── DashboardPresenter.php ← presenter -└── default.latte ← szablon -\-- - -Ta struktura katalogów odzwierciedla się w przestrzeniach nazw klas. Na przykład `DashboardPresenter` leży w przestrzeni nazw `App\Presentation\Admin\Dashboard` (zobacz [#Mapowanie presenterów]): - -```php -namespace App\Presentation\Admin\Dashboard; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -Do presentera `Dashboard` w module `Admin` odwołujemy się w aplikacji zapisem z dwukropkiem jako `Admin:Dashboard`. Do jego akcji `default` odwołujemy się potem jako `Admin:Dashboard:default`. Przy zagnieżdżonych modułach używamy większej liczby dwukropków, na przykład `Shop:Order:Detail:default`. - - -Elastyczny rozwój struktury ---------------------------- - -Jedną z wielkich zalet tej struktury jest to, jak elegancko dostosowuje się do rosnących potrzeb projektu. Jako przykład weźmy część generującą feedy XML. Na początku mamy prostą postać: - -/--pre -Export/ -├── ExportPresenter.php ← jeden presenter dla wszystkich eksportów -├── sitemap.latte ← szablon mapy strony -└── feed.latte ← szablon feedu RSS -\-- - -Z czasem dochodzą kolejne typy feedów i potrzebujemy dla nich więcej logiki... Żaden problem! Folder `Export/` po prostu staje się modułem: - -/--pre -Export/ -├── Sitemap/ -│ ├── SitemapPresenter.php -│ └── sitemap.latte -└── Feed/ - ├── FeedPresenter.php - ├── amazon.latte ← feed dla Amazona - └── ebay.latte ← feed dla eBaya -\-- - -Ta przemiana jest całkowicie płynna - wystarczy utworzyć nowe podfoldery, podzielić między nie kod i zaktualizować odnośniki (np. z `Export:feed` na `Export:Feed:amazon`). Dzięki temu możemy stopniowo rozbudowywać strukturę według potrzeb, a poziom zagnieżdżenia nie jest w żaden sposób ograniczony. - -Jeśli na przykład w administracji masz wiele presenterów związanych z zarządzaniem zamówieniami, takich jak `OrderDetail`, `OrderEdit`, `OrderDispatch` itd., możesz dla lepszej organizacji utworzyć moduł (folder) o nazwie `Order`, który będzie zawierać (foldery dla) presentery `Detail`, `Edit`, `Dispatch` i inne. - - -Położenie szablonów -------------------- - -W poprzednich przykładach widzieliśmy, że szablony leżą bezpośrednio w folderze z presenterem: - -/--pre -Dashboard/ -├── DashboardPresenter.php ← presenter -├── DashboardTemplate.php ← opcjonalna klasa szablonu -└── default.latte ← szablon -\-- - -To położenie okazuje się w praktyce najwygodniejsze - masz wszystkie powiązane pliki pod ręką. - -Alternatywnie możesz umieścić szablony w podfolderze `templates/`. Nette obsługuje oba warianty. Możesz nawet umieścić szablony całkowicie poza folderem `Presentation/`. Wszystko o możliwościach położenia szablonów znajdziesz w rozdziale [Wyszukiwanie szablonów |templates#Wyszukiwanie szablonów]. - - -Klasy pomocnicze i komponenty ------------------------------ - -Presenterom i szablonom często towarzyszą inne pliki pomocnicze. Umieszczamy je logicznie według ich zasięgu: - -1. **Bezpośrednio przy presenterze** w przypadku komponentów specyficznych dla tego presentera: - -/--pre -Product/ -├── ProductPresenter.php -├── ProductGrid.php ← komponent do listowania produktów -└── FilterForm.php ← formularz do filtrowania -\-- - -2. **Dla modułu** - zalecamy folder `Accessory`, który wygodnie ląduje na początku alfabetu: - -/--pre -Front/ -├── Accessory/ -│ ├── NavbarControl.php ← komponenty dla frontendu -│ └── TemplateFilters.php -├── Product/ -└── Cart/ -\-- - -3. **Dla całej aplikacji** - w `Presentation/Accessory/`: -/--pre -app/Presentation/ -├── Accessory/ -│ ├── LatteExtension.php -│ └── TemplateFilters.php -├── Front/ -└── Admin/ -\-- - -Alternatywnie klasy pomocnicze, takie jak `LatteExtension.php` czy `TemplateFilters.php`, możesz umieścić w folderze infrastrukturalnym `app/Core/Latte/`. A komponenty w `app/Components`. Wybór zależy od konwencji zespołu. - - -Model - serce aplikacji -======================= - -Model zawiera całą logikę biznesową aplikacji. Zasada jego organizacji jest znów taka sama: strukturyzuj według domen: - -/--pre -app/Model/ -├── Payment/ ← wszystko o płatnościach -│ ├── PaymentFacade.php ← główny punkt wejścia -│ ├── PaymentRepository.php -│ ├── Payment.php ← encja -├── Order/ ← wszystko o zamówieniach -│ ├── OrderFacade.php -│ ├── OrderRepository.php -│ ├── Order.php -└── Shipping/ ← wszystko o wysyłce -\-- - -W modelu typowo spotkasz te rodzaje klas: - -**Fasady**: reprezentują główny punkt wejścia do konkretnej domeny w aplikacji. Działają jak orkiestrator, koordynując współpracę różnych usług w celu zrealizowania kompletnych przypadków użycia (jak "utwórz zamówienie" czy "przetwórz płatność"). Pod swoją warstwą orkiestracji fasada ukrywa przed resztą aplikacji szczegóły implementacyjne, dostarczając tym samym czysty interfejs do pracy z daną domeną. - -```php -class OrderFacade -{ - public function createOrder(Cart $cart): Order - { - // walidacja - // utworzenie zamówienia - // wysłanie e-maila - // zapis do statystyk - } -} -``` - -**Usługi**: skupiają się na konkretnych operacjach biznesowych w obrębie domeny. W odróżnieniu od fasad, które orkiestrują całe przypadki użycia, usługa implementuje konkretną logikę biznesową (jak obliczanie cen czy przetwarzanie płatności). Usługi są zwykle bezstanowe i mogą być używane albo przez fasady jako klocki do bardziej złożonych operacji, albo bezpośrednio przez inne części aplikacji do prostszych zadań. - -```php -class PricingService -{ - public function calculateTotal(Order $order): Money - { - // obliczenie ceny - } -} -``` - -**Repozytoria**: zajmują się całą komunikacją z magazynem danych, zwykle z bazą danych. Ich zadaniem jest wczytywanie i zapisywanie encji oraz implementowanie metod do ich wyszukiwania. Repozytorium osłania resztę aplikacji przed szczegółami implementacyjnymi bazy danych i dostarcza obiektowy interfejs do pracy z danymi. - -```php -class OrderRepository -{ - public function find(int $id): ?Order - { - } - - public function findByCustomer(int $customerId): array - { - } -} -``` - -**Encje**: obiekty reprezentujące główne pojęcia biznesowe w aplikacji, które mają własną tożsamość i zmieniają się w czasie. Zwykle są to klasy mapowane na tabele bazy danych za pomocą ORM (jak Nette Database Explorer albo Doctrine). Encje mogą zawierać reguły biznesowe związane ze swoimi danymi i logikę walidacji. - -```php -// encja mapowana na tabelę bazy danych '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**: niezmienne obiekty reprezentujące wartości bez własnej tożsamości, na przykład kwotę pieniężną albo adres e-mail. Dwie instancje value objectu o tych samych wartościach uznawane są za identyczne. - - -Kod infrastrukturalny -===================== - -Folder `Core/` (albo alternatywnie `Infrastructure/`) jest domem technicznego fundamentu aplikacji. Kod infrastrukturalny obejmuje zwykle: - -/--pre -app/Core/ -├── Router/ ← routing i zarządzanie URL -│ └── RouterFactory.php -├── Security/ ← uwierzytelnianie i autoryzacja -│ ├── Authenticator.php -│ └── Authorizator.php -├── Logging/ ← logowanie i monitorowanie -│ ├── SentryLogger.php -│ └── FileLogger.php -├── Cache/ ← warstwa cache -│ └── FullPageCache.php -└── Integration/ ← integracja z usługami zewnętrznymi - ├── Slack/ - └── Stripe/ -\-- - -Przy mniejszych projektach naturalnie wystarczy struktura płaska: - -/--pre -Core/ -├── RouterFactory.php -├── Authenticator.php -└── QueueMailer.php -\-- - -To kod, który: - -- zajmuje się infrastrukturą techniczną (routing, logowanie, cache) -- integruje usługi zewnętrzne (Sentry, Elasticsearch, Redis) -- dostarcza podstawowe usługi dla całej aplikacji (poczta, baza danych) -- jest w większości niezależny od konkretnej domeny - cache albo logger działa tak samo dla sklepu internetowego i dla bloga - -Zastanawiasz się, czy dana klasa należy tutaj, czy do modelu? Kluczowa różnica jest taka, że kod w `Core/`: - -- nie wie nic o domenie (produkty, zamówienia, artykuły) -- zwykle da się przenieść do innego projektu -- rozwiązuje "jak to działa" (jak wysłać e-mail), a nie "co robi" (jaki e-mail wysłać) - -Przykład dla lepszego zrozumienia: - -- `App\Core\MailerFactory` - tworzy instancje klasy do wysyłania e-maili, zajmuje się ustawieniami SMTP -- `App\Model\OrderMailer` - używa `MailerFactory` do wysyłania e-maili o zamówieniach, zna ich szablony i wie, kiedy mają być wysyłane - - -Skrypty poleceń -=============== - -Aplikacje często potrzebują wykonywać czynności poza zwykłymi żądaniami HTTP, czy to przetwarzanie danych w tle, czy konserwację, czy zadania okresowe. Do uruchamiania służą proste skrypty w katalogu `bin/`, a właściwa logika implementacji umieszczana jest w `app/Tasks/` (albo `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/ ← zadania regularne - ├── 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 jednego e-maila jest częścią modelu, natomiast masowe wysyłanie tysięcy e-maili należy do `Tasks/`. - -Zadania uruchamiane są zwykle z wiersza poleceń albo przez cron: skrypt w `bin/` tworzy kontener DI metodą [bootConsoleApplication() |bootstrapping#Różne środowiska] i pobiera z niego potrzebną usługę. Można je uruchamiać również przez żądanie HTTP, ale trzeba pomyśleć o bezpieczeństwie. Presenter uruchamiający zadanie trzeba zabezpieczyć, na przykład tylko dla zalogowanych użytkowników albo silnym tokenem i dostępem z dozwolonych adresów IP. Przy długo działających zadaniach trzeba zwiększyć limit czasu skryptu i użyć `session_write_close()`, aby nie blokować sesji. - - -Inne możliwe katalogi -===================== - -Poza wymienionymi podstawowymi katalogami możesz dodać kolejne, wyspecjalizowane foldery według potrzeb projektu. Spójrzmy na najczęstsze i ich zastosowanie: - -/--pre -app/ -├── Api/ ← logika API niezależna od warstwy prezentacji -├── Database/ ← skrypty migracyjne i seedery danych testowych -├── Components/ ← współdzielone komponenty wizualne w całej aplikacji -├── Event/ ← przydatne przy architekturze zdarzeniowej -├── Mail/ ← szablony e-maili i powiązana logika -└── Utils/ ← klasy pomocnicze -\-- - -Na współdzielone komponenty wizualne używane w presenterach w całej aplikacji możesz przeznaczyć folder `app/Components` albo `app/Controls`: - -/--pre -app/Components/ -├── Form/ ← współdzielone komponenty formularzy -│ ├── SignInForm.php -│ └── UserForm.php -├── Grid/ ← komponenty do listowania danych -│ └── DataGrid.php -└── Navigation/ ← elementy nawigacji - ├── Breadcrumbs.php - └── Menu.php -\-- - -To miejsce dla komponentów o bardziej złożonej logice. Jeśli chcesz współdzielić komponenty między wieloma projektami, warto wydzielić je do osobnego pakietu Composera. - -W katalogu `app/Mail` możesz umieścić zarządzanie komunikacją e-mailową: - -/--pre -app/Mail/ -├── templates/ ← szablony e-maili -│ ├── order-confirmation.latte -│ └── welcome.latte -└── OrderMailer.php -\-- - - -Mapowanie presenterów -===================== - -Mapowanie definiuje reguły wyprowadzania nazwy klasy z nazwy presentera. Podajemy je w [konfiguracji|configuration] pod kluczem `application › mapping`. - -Na tej stronie pokazaliśmy, że presentery umieszczamy w folderze `app/Presentation` (albo `app/UI`). Od Nette Application 3.3 jest to konwencja domyślna, której nie trzeba konfigurować. Jeśli używasz innej struktury albo chcesz podać mapowanie jawnie, ustawienie domyślne odpowiada temu wierszowi: - -```neon -application: - mapping: App\Presentation\*\**Presenter -``` - -Jak działa mapowanie? Dla lepszego zrozumienia wyobraźmy sobie najpierw aplikację bez modułów. Chcemy, aby klasy presenterów należały do przestrzeni nazw `App\Presentation`, tak aby presenter `Home` mapował się na klasę `App\Presentation\HomePresenter`. Osiągniemy to taką konfiguracją: - -```neon -application: - mapping: App\Presentation\*Presenter -``` - -Mapowanie działa tak, że gwiazdka w masce `App\Presentation\*Presenter` zastępowana jest nazwą presentera `Home`, co daje ostateczną nazwę klasy `App\Presentation\HomePresenter`. Proste! - -Jak jednak widzisz w przykładach w tym i innych rozdziałach, klasy presenterów umieszczamy w jednoimiennych podkatalogach, na przykład presenter `Home` mapuje się na klasę `App\Presentation\Home\HomePresenter`. Osiągniemy to, używając podwójnej gwiazdki `**` (wymaga Nette Application 3.2.3): - -```neon -application: - mapping: App\Presentation\**Presenter -``` - -Przejdźmy teraz do mapowania presenterów w modułach. Dla każdego modułu możemy zdefiniować własne mapowanie: - -```neon -application: - mapping: - Front: App\Presentation\Front\**Presenter - Admin: App\Presentation\Admin\**Presenter - Api: App\Api\*Presenter -``` - -Zgodnie z tą konfiguracją presenter `Front:Home` mapuje się na klasę `App\Presentation\Front\Home\HomePresenter`, a presenter `Api:OAuth` na klasę `App\Api\OAuthPresenter`. - -Ponieważ moduły `Front` i `Admin` mają podobny wzorzec mapowania, a takich modułów będzie zapewne więcej, można utworzyć ogólną regułę, która je zastąpi. Do maski klasy dochodzi nowa gwiazdka dla modułu: - -```neon -application: - mapping: - *: App\Presentation\*\**Presenter - Api: App\Api\*Presenter -``` - -Działa to również dla głębiej zagnieżdżonych struktur katalogów, jak presenter `Admin:User:Edit`, gdzie segment z gwiazdką powtarza się dla każdego poziomu modułu, co daje klasę `App\Presentation\Admin\User\Edit\EditPresenter`. - -Alternatywnym zapisem jest użycie zamiast stringa tablicy złożonej z trzech segmentów. Dla pokazanych wyżej przykładów zapis ten 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 deleted file mode 100644 index f8d24ef605..0000000000 --- a/application/pl/how-it-works.texy +++ /dev/null @@ -1,200 +0,0 @@ -Jak działają aplikacje? -*********************** - -
    - -Czytasz właśnie fundamentalny rozdział dokumentacji Nette. Poznasz kompletne zasady działania aplikacji webowych, od A do Z, od chwili narodzin żądania aż po zakończenie skryptu PHP. Po lekturze będziesz wiedzieć: - -- jak to wszystko działa -- czym są Bootstrap, Presenter i kontener DI -- jak wygląda struktura katalogów - -
    - - -Struktura katalogów -=================== - -Otwórz przykładowy szkielet aplikacji webowej o nazwie [WebProject|https://github.com/nette/web-project]. Czytając, możesz zaglądać do omawianych plików. - -Struktura katalogów wygląda mniej więcej tak: - -/--pre -web-project/ -├── app/ ← katalog aplikacji -│ ├── Core/ ← klasy rdzenia niezbędne do działania -│ │ └── RouterFactory.php ← konfiguracja adresów URL -│ ├── Presentation/ ← presentery, szablony i spółka -│ │ ├── @layout.latte ← szablon layoutu -│ │ └── Home/ ← katalog presentera Home -│ │ ├── HomePresenter.php ← klasa presentera Home -│ │ └── default.latte ← szablon akcji default -│ └── Bootstrap.php ← klasa startowa Bootstrap -├── assets/ ← zasoby (SCSS, TypeScript, obrazy źródłowe) -├── bin/ ← skrypty uruchamiane z wiersza poleceń -├── config/ ← pliki konfiguracyjne -│ ├── common.neon -│ └── services.neon -├── log/ ← zalogowane błędy -├── temp/ ← pliki tymczasowe, cache, … -├── vendor/ ← biblioteki zainstalowane przez Composera -│ ├── ... -│ └── autoload.php ← autoloading wszystkich zainstalowanych pakietów -├── www/ ← katalog publiczny, document root projektu -│ ├── assets/ ← skompilowane pliki statyczne (CSS, JS, obrazy, ...) -│ ├── .htaccess ← reguły mod_rewrite -│ └── index.php ← plik początkowy uruchamiający aplikację -└── .htaccess ← zabrania dostępu do wszystkich katalogów poza www -\-- - -Strukturę katalogów możesz dowolnie zmieniać, zmieniać nazwy folderów albo je przenosić; jest całkowicie elastyczna. Nette dysponuje też sprytną autodetekcją i samo rozpoznaje położenie aplikacji wraz z jej bazowym URL. - -W nieco większych aplikacjach możemy uporządkować foldery presenterów i szablonów w [podkatalogi |directory-structure#Presentery i szablony], a klasy pogrupować w przestrzenie nazw, które nazywamy modułami. - -Katalog `www/` reprezentuje katalog publiczny, czyli document-root projektu. Możesz zmienić jego nazwę bez konieczności konfigurowania czegokolwiek po stronie aplikacji. Wystarczy [skonfigurować hosting |nette:troubleshooting#Jak zmienić albo usunąć katalog www z URL?] tak, aby document-root wskazywał na ten katalog. - -WebProject możesz też pobrać bezpośrednio, wraz z Nette, za pomocą [Composera |best-practices:composer]: - -```shell -composer create-project nette/web-project -``` - -Na Linuksie albo macOS ustaw katalogom `log/` i `temp/` [prawa do zapisu |nette:troubleshooting#Ustawienie uprawnień do katalogów]. - -Aplikacja WebProject jest gotowa do uruchomienia; nie trzeba konfigurować absolutnie niczego i możesz obejrzeć ją od razu w przeglądarce, wchodząc do folderu `www/`. - - -Żądanie HTTP -============ - -Wszystko zaczyna się, gdy użytkownik otworzy w przeglądarce stronę. Przeglądarka wysyła do serwera żądanie HTTP. Żądanie to trafia do jednego pliku PHP leżącego w katalogu publicznym `www/`, czyli do `index.php`. Załóżmy, że żądanie dotyczy adresu `https://example.com/product/123`. Dzięki odpowiedniej [konfiguracji serwera |nette:troubleshooting#Jak skonfigurować serwer dla przyjaznych URL-i?] również ten URL mapowany jest na plik `index.php`, który następnie się wykonuje. - -Jego zadaniem jest: - -1) zainicjować środowisko -2) pozyskać fabrykę -3) uruchomić aplikację Nette, która obsłuży żądanie - -Jaką fabrykę? Przecież nie produkujemy traktorów, tylko robimy strony! Zaraz, zaraz, zaraz to wyjaśnimy. - -Przez "inicjalizację środowiska" rozumiemy na przykład aktywowanie [Tracy|tracy:], czyli znakomitego narzędzia do logowania i wizualizowania błędów. Na serwerze produkcyjnym błędy loguje, a w środowisku deweloperskim wyświetla je bezpośrednio. Inicjalizacja obejmuje więc również ustalenie, czy witryna działa w trybie produkcyjnym, czy deweloperskim. Nette używa do tego [sprytnej autodetekcji |bootstrapping#Tryb deweloperski kontra produkcyjny]: jeśli uruchomisz witrynę na localhoście, działa ona w trybie deweloperskim. Nie musisz niczego konfigurować, a aplikacja jest od razu gotowa zarówno do tworzenia, jak i do wdrożenia na żywo. Te kroki wykonywane są i szczegółowo opisane w rozdziale o [klasie Bootstrap|bootstrapping]. - -Punkt trzeci (tak, drugi pominęliśmy, ale do niego wrócimy) to uruchomienie aplikacji. Za obsługę żądań HTTP w Nette odpowiada klasa `Nette\Application\Application` (dalej `Application`). Gdy więc mówimy "uruchom aplikację", mamy konkretnie na myśli wywołanie na obiekcie tej klasy trafnie nazwanej metody `run()`. - -Nette jest mentorem, który prowadzi Cię do pisania czystych aplikacji według sprawdzonych metodologii. Jedną z najbardziej ugruntowanych jest **wstrzykiwanie zależności**, w skrócie DI. Nie chcemy Cię teraz obciążać wyjaśnianiem DI, jest od tego [osobny rozdział|dependency-injection:introduction]. Istotną konsekwencją jest to, że kluczowe obiekty tworzy zwykle fabryka obiektów zwana **kontenerem DI** (albo DIC). Tak, to właśnie ta fabryka, o której była mowa wcześniej. Produkuje ona również obiekt `Application`, dlatego najpierw potrzebujemy kontenera. Pozyskujemy go za pomocą klasy `Configurator`, każemy mu utworzyć obiekt `Application`, wywołujemy na nim metodę `run()` i tym samym aplikacja Nette rusza. Dokładnie to dzieje się w pliku [index.php |bootstrapping#index.php]. - - -Nette Application -================= - -Klasa `Application` ma jedno zadanie: odpowiedzieć na żądanie HTTP. - -Aplikacje pisane w Nette dzielą się na wiele tak zwanych presenterów (w innych frameworkach możesz spotkać się z terminem "kontroler", który w istocie oznacza to samo). To klasy, z których każda reprezentuje konkretną stronę witryny: np. stronę główną, produkt w sklepie internetowym, formularz logowania, feed z mapą strony itd. Aplikacja może mieć od jednego do tysięcy presenterów. - -`Application` zaczyna od zapytania tak zwanego routera, który presenter ma obsłużyć bieżące żądanie. Router rozstrzyga o odpowiedzialności. Analizuje wejściowy URL `https://example.com/product/123` i na podstawie swojej konfiguracji decyduje, że to zadanie należy na przykład do **presentera** `Product`, który ma wykonać **akcję** `show` dla produktu o `id: 123`. Dobrą praktyką jest zapisywanie pary presenter + akcja oddzielonych dwukropkiem, czyli `Product:show`. - -Router zamienił więc URL na parę `Presenter:akcja` + parametry, w naszym przypadku `Product:show` + `id: 123`. Jak taki router wygląda, zobaczysz w pliku `app/Core/RouterFactory.php`, a szczegółowo opisujemy go w rozdziale [Routing |Routing]. - -Idźmy dalej. `Application` zna już nazwę presentera i może działać dalej. Robi to, tworząc instancję klasy `ProductPresenter`, która zawiera kod presentera `Product`. Ściślej mówiąc, prosi o utworzenie presentera kontener DI, bo tworzenie obiektów to jego odpowiedzialność. - -Presenter może wyglądać tak: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ProductRepository $repository, - ) { - } - - public function renderShow(int $id): void - { - // pobieramy dane z modelu i przekazujemy do szablonu - $this->template->product = $this->repository->getProduct($id); - } -} -``` - -Presenter przejmuje obsługę żądania. Zadanie jest jasne: wykonać akcję `show` z `id: 123`. W terminologii presenterów oznacza to, że wywoływana jest metoda `renderShow()`, otrzymująca `123` w parametrze `$id`. - -Presenter może obsługiwać wiele akcji, czyli mieć wiele metod `render()`. Zalecamy jednak projektować presentery z jedną albo możliwie niewieloma akcjami. - -Wywołana została więc metoda `renderShow(123)`. Jej kod to fikcyjny przykład, ale pokazuje, jak dane przekazywane są do szablonu, konkretnie przez zapis do `$this->template`. - -Następnie presenter zwraca odpowiedź. Może nią być strona HTML, obraz, dokument XML, wysłanie pliku z dysku, JSON albo choćby przekierowanie na inną stronę. Co ważne, jeśli nie wskażemy jawnie, jak odpowiedzieć (a tak jest w przypadku `ProductPresenter`), odpowiedzią będzie wyrenderowanie szablonu do strony HTML. Dlaczego? Bo w 99% przypadków chcemy wyrenderować szablon. Presenter przyjmuje więc to zachowanie jako domyślne, aby ułatwić nam pracę. Na tym polega istota Nette. - -Nie musimy nawet wskazywać, który szablon wyrenderować; framework sam wywnioskuje ścieżkę. W przypadku akcji `show` po prostu próbuje wczytać szablon `show.latte` leżący w tym samym katalogu co klasa `ProductPresenter`. Próbuje też odnaleźć layout w pliku `@layout.latte` (więcej szczegółów w [wyszukiwaniu szablonów |templates#Wyszukiwanie szablonów]). - -Następnie szablony są renderowane. To kończy zadanie presentera i całej aplikacji. Jeśli szablon nie istnieje, zwracana jest strona błędu 404. Więcej o presenterach dowiesz się na stronie [Presentery|presenters]. - -[* request-flow.svg *] - -Dla pewności podsumujmy cały proces na nieco innym URL: - -1) URL to `https://example.com` -2) aplikacja startuje, tworzony jest kontener DI i wykonywane `Application::run()` -3) router dekoduje URL na parę `Home:default` -4) tworzona jest instancja klasy `HomePresenter` -5) wywoływana jest metoda `renderDefault()` (jeśli istnieje) -6) renderowany jest szablon, np. `default.latte`, wraz z layoutem, np. `@layout.latte` - - -Mogłeś właśnie spotkać się z wieloma nowymi pojęciami, ale wierzymy, że mają sens. Tworzenie aplikacji w Nette jest zdumiewająco proste. - - -Szablony -======== - -Skoro mowa o szablonach, Nette używa systemu szablonów [Latte |latte:]. Dlatego pliki szablonów mają rozszerzenie `.latte`. Latte używane jest przede wszystkim dlatego, że to najbezpieczniejszy system szablonów dla PHP, a zarazem najbardziej intuicyjny. Nie musisz uczyć się wiele nowego; wystarczy znajomość PHP i kilku tagów. Wszystko, czego potrzebujesz, znajdziesz [w dokumentacji |templates]. - -W szablonie [tworzysz odnośniki |creating-links] do innych presenterów i akcji tak: - -```latte -szczegóły produktu -``` - -Zamiast prawdziwego URL po prostu zapisujesz znajomą parę `Presenter:akcja` i dodajesz ewentualne parametry. Sztuczka tkwi w `n:href`, które mówi Nette, aby ten atrybut przetworzyło. Wygeneruje wtedy: - -```latte -szczegóły produktu -``` - -Generowaniem URL zajmuje się wspomniany wcześniej router. Routery w Nette są wyjątkowe, bo potrafią wykonać nie tylko przekształcenie z URL na parę `Presenter:akcja`, ale też odwrotne: wygenerować URL z nazwy presentera, akcji i parametrów. Dzięki temu w Nette możesz całkowicie zmienić postać URL w całej gotowej aplikacji bez zmieniania jednego znaku w szablonach czy presenterach, po prostu modyfikując router. Umożliwia to również tak zwaną kanonizację, kolejną unikalną cechę Nette, która poprawia SEO (Search Engine Optimization) przez automatyczne zapobieganie istnieniu zduplikowanej treści pod różnymi URL. Wielu programistów uznaje tę możliwość za zdumiewającą. - - -Komponenty interaktywne -======================= - -Musimy powiedzieć Ci o presenterach jeszcze jedno: mają wbudowany system komponentów. Osoby z większym doświadczeniem mogą przypomnieć sobie coś podobnego z Delphi albo ASP.NET Web Forms; React czy Vue.js zbudowane są na nieco pokrewnych koncepcjach. W świecie frameworków PHP jest to funkcja całkowicie unikalna. - -Komponenty to niezależne jednostki wielokrotnego użytku, które osadzamy w stronach (czyli w presenterach). Mogą to być [formularze |forms:in-presenter], [datagridy |https://componette.org/contributte/datagrid/], menu, ankiety, w zasadzie wszystko, co warto wykorzystywać wielokrotnie. Możemy tworzyć własne komponenty albo skorzystać z [ogromnego wyboru |https://componette.org] komponentów open source. - -Komponenty zasadniczo wpływają na podejście do tworzenia aplikacji. Otwierają nowe możliwości składania stron z wcześniej przygotowanych jednostek. I mają też coś wspólnego z [Hollywood |components#Styl hollywoodzki]. - - -Kontener DI i konfiguracja -========================== - -Kontener DI, czyli fabryka obiektów, jest sercem całej aplikacji. - -Bez obaw, nie jest to żadna magiczna czarna skrzynka, choć poprzednie wiersze mogłyby tak sugerować. W rzeczywistości to całkiem przyziemna klasa PHP, którą generuje Nette i zapisuje w katalogu cache. Zawiera wiele metod nazwanych w rodzaju `createServiceAbcd()`, z których każda potrafi utworzyć i zwrócić konkretny obiekt. Tak, jest tam również metoda `createServiceApplication__application()`, produkująca instancję `Nette\Application\Application`, której potrzebowaliśmy w `index.php` do uruchomienia aplikacji. Są też metody tworzące poszczególne presentery itd. - -Obiekty tworzone przez kontener DI z jakiegoś powodu nazywane są usługami. - -Prawdziwie szczególne w tej klasie jest to, że jej nie programujesz - robi to framework. Faktycznie generuje kod PHP i zapisuje go na dysk. Ty jedynie podajesz instrukcje, jakie obiekty kontener ma umieć tworzyć i dokładnie jak. Instrukcje te zapisywane są w [plikach konfiguracyjnych |bootstrapping#Konfiguracja kontenera DI], które używają formatu [NEON|neon:format] i dlatego mają rozszerzenie `.neon`. - -Pliki konfiguracyjne służą wyłącznie do instruowania kontenera DI. Jeśli więc na przykład podasz w sekcji [session |http:configuration#Sesja] opcję `expiration: 14 days`, kontener DI przy tworzeniu obiektu `Nette\Http\Session` reprezentującego sesję wywoła jego metodę `setExpiration('14 days')`, urzeczywistniając tym samym konfigurację. - -Czeka na Ciebie cały rozdział opisujący, co można [skonfigurować |nette:configuring] i jak [zdefiniować własne usługi |dependency-injection:services]. - -Gdy tylko zagłębisz się nieco w tworzenie usług, natkniesz się na pojęcie [autowiring |dependency-injection:autowiring]. To funkcja, która niesamowicie uprości Ci życie. Potrafi automatycznie przekazywać obiekty tam, gdzie ich potrzebujesz (na przykład do konstruktorów Twoich klas), bez konieczności robienia czegokolwiek. Odkryjesz, że kontener DI w Nette to mały cud. - - -Co dalej? -========= - -Omówiliśmy podstawowe zasady działania aplikacji Nette. Jak dotąd był to przegląd powierzchowny, ale wkrótce zagłębisz się bardziej i z czasem stworzysz wspaniałe aplikacje webowe. Gdzie dalej? Wypróbowałeś już samouczek [Stwórz swoją pierwszą aplikację|quickstart:]? - -Poza tym, co opisano powyżej, Nette oferuje cały arsenał [przydatnych klas|utils:], [warstwę bazodanową|database:] itd. Poklikaj po dokumentacji. Albo odwiedź [blog|https://blog.nette.org]. Odkryjesz wiele ciekawych rzeczy. - -Niech framework przyniesie Ci wiele radości 💙 diff --git a/application/pl/multiplier.texy b/application/pl/multiplier.texy deleted file mode 100644 index dcb7e97ab9..0000000000 --- a/application/pl/multiplier.texy +++ /dev/null @@ -1,63 +0,0 @@ -Multiplier: dynamiczne komponenty -********************************* - -.[perex] -Narzędzie do dynamicznego tworzenia komponentów interaktywnych. - -Zacznijmy od typowego przykładu: wyobraź sobie listę produktów w sklepie internetowym, przy której chcesz wyświetlić dla każdego artykułu formularz "Do koszyka". Jedną z możliwości jest opakowanie całej listy w jeden formularz. O wiele wygodniejszy sposób oferuje jednak [api:Nette\Application\UI\Multiplier]. - -Multiplier pozwala wygodnie zdefiniować fabrykę dla wielu komponentów. Działa na zasadzie komponentów zagnieżdżonych - każdy komponent dziedziczący po [api:Nette\ComponentModel\Container] może zawierać inne komponenty. - -.[tip] -Zobacz w dokumentacji rozdział o [modelu komponentów |components#Komponenty w głąb]. - -Istota Multipliera polega na tym, że działa jak rodzic, który potrafi dynamicznie tworzyć swoich potomków za pomocą callbacku przekazanego w konstruktorze. Zobacz przykład: - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function () { - $form = new Nette\Application\UI\Form; - $form->addInteger('amount', 'Ilość:') - ->setRequired(); - $form->addSubmit('send', 'Do koszyka'); - return $form; - }); -} -``` - -Teraz w szablonie możemy po prostu wyrenderować formularz dla każdego produktu - i każdy naprawdę będzie osobnym komponentem. - -```latte -{foreach $items as $item} -

    {$item->title}

    - {$item->description} - - {control "shopForm-$item->id"} -{/foreach} -``` - -Argument przekazany w tagu `{control}` ma format, który oznacza: - -1. Pobierz komponent `shopForm`. -2. Z niego pobierz potomka o nazwie `$item->id`. - -Przy pierwszym wywołaniu punktu **1** komponent `shopForm` jeszcze nie istnieje, więc wywoływana jest jego fabryka `createComponentShopForm`. Następnie na uzyskanym komponencie (instancji Multipliera) wywoływana jest fabryka konkretnego formularza, czyli anonimowa funkcja, którą przekazaliśmy do konstruktora Multipliera. - -W kolejnej iteracji pętli foreach metoda `createComponentShopForm` nie zostanie wywołana ponownie (bo komponent już istnieje). Ponieważ jednak szukamy innego potomka (`$item->id` będzie w każdej iteracji inne), anonimowa funkcja zostanie wywołana ponownie i zwróci nowy formularz. - -Pozostaje już tylko zadbać o to, aby formularz dodawał do koszyka właściwy produkt - obecnie formularz jest identyczny dla każdego produktu. Pomaga nam w tym cecha Multipliera (i ogólnie każdej fabryki komponentów w Nette Framework): każda fabryka otrzymuje jako pierwszy argument nazwę tworzonego komponentu. Ponadto fabryka Multipliera otrzymuje jako drugi argument samą instancję Multipliera. W naszym przypadku pierwszym argumentem będzie `$item->id`, czyli dokładnie ta informacja, której potrzebujemy. Wystarczy więc nieznacznie zmodyfikować tworzenie formularza: - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function (string $itemId) { - $form = new Nette\Application\UI\Form; - $form->addInteger('amount', 'Ilość:') - ->setRequired(); - $form->addHidden('itemId', $itemId); - $form->addSubmit('send', 'Do koszyka'); - return $form; - }); -} -``` diff --git a/application/pl/presenters.texy b/application/pl/presenters.texy deleted file mode 100644 index 7caffa063b..0000000000 --- a/application/pl/presenters.texy +++ /dev/null @@ -1,595 +0,0 @@ -Presentery -********** - -
    - -Przyjrzymy się temu, jak w Nette pisze się presentery i szablony. Po lekturze będziesz rozumieć: - -- jak działają presentery -- czym są parametry trwałe -- jak renderowane są szablony - -
    - -[Wiemy już |how-it-works#Nette Application], że presenter to klasa reprezentująca konkretną stronę aplikacji webowej, na przykład stronę główną, produkt w sklepie internetowym, formularz logowania, feed z mapą strony itd. Aplikacja może mieć od jednego do tysięcy presenterów. W innych frameworkach znane są też jako kontrolery. - -Zwykle terminem presenter określamy potomka klasy [api:Nette\Application\UI\Presenter], która nadaje się do generowania interfejsów webowych i której poświęcona będzie reszta tego rozdziału. W ogólnym sensie presenterem jest dowolny obiekt implementujący interfejs [api:Nette\Application\IPresenter]. - - -Cykl życia presentera -===================== - -Zadaniem presentera jest obsłużenie żądania i zwrócenie odpowiedzi (którą może być strona HTML, obraz, przekierowanie itd.). - -Na początku przekazywane jest mu więc żądanie. Nie jest to bezpośrednio żądanie HTTP, lecz obiekt [api:Nette\Application\Request], w który żądanie HTTP zostało przekształcone z pomocą routera. Zwykle nie pracujemy z tym obiektem bezpośrednio, bo presenter sprytnie deleguje obsługę żądania do innych metod, którym teraz się przyjrzymy. - -[* lifecycle.svg *] *** Cykl życia presentera .<> - -Diagram pokazuje listę metod wywoływanych kolejno od góry do dołu, jeśli istnieją. Żadna z nich nie jest obowiązkowa; możesz mieć całkowicie pusty presenter bez jednej metody i zbudować na nim prostą statyczną witrynę. - - -`__construct()` ---------------- - -Konstruktor nie należy ściśle do cyklu życia presentera, bo wywoływany jest w chwili tworzenia obiektu. Wspominamy o nim jednak ze względu na jego znaczenie. Konstruktor (wraz z [metodą inject|best-practices:inject-method-attribute]) służy do przekazywania zależności. - -Presenter nie powinien zajmować się logiką biznesową aplikacji, zapisywać do bazy danych ani z niej czytać, wykonywać obliczeń itd. To odpowiedzialność klas w warstwie, którą nazywamy modelem. Na przykład klasa `ArticleRepository` może odpowiadać za wczytywanie i zapisywanie artykułów. Aby presenter mógł z niej korzystać, musi mieć ją [przekazaną przez wstrzykiwanie zależności |dependency-injection:passing-dependencies]: - - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articles, - ) { - } -} -``` - - -`startup()` ------------ - -Natychmiast po otrzymaniu żądania wywoływana jest metoda `startup()`. Możesz jej użyć do zainicjowania właściwości, sprawdzenia uprawnień użytkownika itd. Wymagane jest, aby metoda ta zawsze wywoływała swojego rodzica: `parent::startup()`. - - -`action(args...)` .{toc: action()} -------------------------------------------------- - -Podobna do metody `render()`. Podczas gdy `render()` ma przygotować dane dla konkretnego szablonu, który następnie zostanie wyrenderowany, `action()` przetwarza żądanie bez konieczności renderowania potem szablonu. Może na przykład przetworzyć dane, zalogować albo wylogować użytkownika itd., a następnie [przekierować gdzie indziej |#Przekierowanie]. - -Ważne jest, że `action()` wywoływana jest *przed* `render()`. Pozwala nam to ewentualnie zmienić przebieg żądania w metodzie akcji, na przykład zmieniając szablon, który zostanie wyrenderowany, albo nawet metodę `render()`, która zostanie wywołana, za pomocą `setView('otherView')`. - -.{data-version:3.2.3} -Możesz nawet przełączyć się na zupełnie inną akcję metodą `switch('otherAction')`. Przerywa ona bieżącą metodę i zamiast niej uruchamia metody `action()` i `render()` nowej akcji (oraz wyłącza automatyczną [kanonizację|#Kanonizacja]). Samo żądanie trwa dalej; przerywana jest tylko aktualnie działająca metoda. - -Do metody przekazywane są parametry z żądania. Można i zalecamy podać dla nich typy, np. `actionShow(int $id, ?string $slug = null)`. Jeśli parametru `id` brakuje albo nie jest liczbą całkowitą, presenter zwraca [błąd 404 |#Błąd 404 itd.] i kończy działanie. - - -`handle(args...)` .{toc: handle()} --------------------------------------------------- - -Ta metoda przetwarza tak zwane sygnały, o których dowiemy się w rozdziale poświęconym [komponentom |components#Sygnał]. Przeznaczona jest przede wszystkim dla komponentów i obsługi żądań AJAX. - -Do metody przekazywane są parametry z żądania, tak jak przy `action()`, wraz z kontrolą typów. - - -`beforeRender()` ----------------- - -Metoda `beforeRender`, jak sama nazwa wskazuje, wywoływana jest przed każdą metodą `render()`. Służy do wspólnej konfiguracji szablonu, przekazywania zmiennych do layoutu i podobnych zadań. - - -`render(args...)` .{toc: render()} ------------------------------------------------ - -Tutaj przygotowujemy szablon do późniejszego renderowania, przekazujemy mu dane itd. - -Do metody przekazywane są parametry z żądania, tak jak przy `action()`, wraz z kontrolą typów. - -```php -public function renderShow(int $id): void -{ - // pobieramy dane z modelu i przekazujemy do szablonu - $this->template->article = $this->articles->getById($id); -} -``` - - -`afterRender()` ---------------- - -Metoda `afterRender`, jak znów wskazuje nazwa, wywoływana jest po każdej metodzie `render()`. Używana jest raczej rzadko. - - -`shutdown()` ------------- - -Wywoływana na końcu cyklu życia presentera. - - -Zdarzenia ---------- - -Poza metodami `startup()`, `beforeRender()` i `shutdown()`, wywoływanymi w ramach cyklu życia presentera, można zdefiniować inne funkcje, które będą wywoływane automatycznie. Presenter definiuje tak zwane [zdarzenia |nette:glossary#Zdarzenia], a ich handlery dodajesz do tablic `$onStartup`, `$onRender` i `$onShutdown`. - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct() - { - $this->onStartup[] = function () { - // ... - }; - } -} -``` - -Handlery z tablicy `$onStartup` wywoływane są tuż przed metodą `startup()`, handlery `$onRender` między `beforeRender()` a `render()`, a wreszcie handlery `$onShutdown` tuż przed `shutdown()`. - - -**Rada, zanim ruszymy dalej:** Jak widzisz, presenter może obsługiwać wiele akcji/widoków, czyli mieć wiele metod `render()`. Zalecamy jednak projektować presentery z jedną albo możliwie niewieloma akcjami. - - -Wysyłanie odpowiedzi -==================== - -Odpowiedzią presentera jest zwykle [wyrenderowanie szablonu do strony HTML|templates], ale może to być również wysłanie pliku, JSON-a albo choćby przekierowanie na inną stronę. - -W dowolnym momencie cyklu życia możemy użyć jednej z poniższych metod, aby wysłać odpowiedź i jednocześnie zakończyć działanie presentera: - -- `redirect()`, `redirectPermanent()`, `redirectUrl()` i `forward()` wykonują [przekierowanie |#Przekierowanie] -- `error()` kończy presenter [z powodu błędu |#Błąd 404 itd.] -- `sendJson($data)` kończy presenter i [wysyła dane |#Wysyłanie JSON] w formacie JSON -- `sendTemplate()` kończy presenter i natychmiast [renderuje szablon |templates] -- `sendResponse($response)` kończy presenter i wysyła [własną odpowiedź |#Odpowiedzi] -- `terminate()` kończy presenter bez odpowiedzi - -Każda z tych metod natychmiast kończy presenter, zgłaszając cichy wyjątek zakończenia `Nette\Application\AbortException`. - -Jeśli nie wywołasz żadnej z tych metod, presenter automatycznie przechodzi do wyrenderowania szablonu. Dlaczego? Bo w 99% przypadków chcemy wyrenderować szablon, więc presenter przyjmuje to zachowanie jako domyślne, aby ułatwić nam pracę. - - -Tworzenie odnośników -==================== - -Presenter ma metodę `link()`, która służy do tworzenia odnośników URL do innych presenterów. Pierwszym parametrem jest docelowy presenter i akcja, po nim argumenty, które można przekazać jako tablicę: - -```php -$url = $this->link('Product:show', $id); - -$url = $this->link('Product:show', [$id, 'lang' => 'en']); -``` - -W szablonie odnośniki do innych presenterów i akcji tworzy się tak: - -```latte -szczegóły produktu -``` - -Zamiast prawdziwego URL po prostu zapisujesz znajomą parę `Presenter:akcja` i dodajesz ewentualne parametry. Sztuczka tkwi w `n:href`, które mówi Latte, aby ten atrybut przetworzyło i wygenerowało prawdziwy URL. W Nette w ogóle nie musisz myśleć o URL, tylko o presenterach i akcjach. - -Więcej informacji znajdziesz w rozdziale [Tworzenie odnośników URL|creating-links]. - - -Przekierowanie -============== - -Do przejścia na inny presenter służą metody `redirect()` i `forward()`. Mają bardzo podobną składnię do metody [link() |#Tworzenie odnośników]. - -Metoda `forward()` przechodzi na nowy presenter natychmiast, bez przekierowania HTTP: - -```php -$this->forward('Product:show'); -``` - -Przykład tymczasowego przekierowania z kodem HTTP 302 (albo 303, jeśli metodą bieżącego żądania jest POST): - -```php -$this->redirect('Product:show', $id); -``` - -Aby uzyskać trwałe przekierowanie z kodem HTTP 301, użyj tego: - -```php -$this->redirectPermanent('Product:show', $id); -``` - -Na inny URL poza aplikacją możesz przekierować metodą `redirectUrl()`. Kod HTTP można podać jako drugi parametr; domyślnie jest to 302 (albo 303, jeśli metodą bieżącego żądania jest POST): - -```php -$this->redirectUrl('https://nette.org'); -``` - -Przekierowanie natychmiast kończy działanie presentera, zgłaszając tak zwany cichy wyjątek zakończenia `Nette\Application\AbortException`. - -Przed przekierowaniem można wysłać [wiadomości flash |#Wiadomości flash], czyli wiadomości, które wyświetlą się w szablonie po przekierowaniu. - - -Wiadomości flash -================ - -To wiadomości informujące zwykle o wyniku jakiejś operacji. Ważną cechą wiadomości flash jest to, że pozostają dostępne w szablonie również po przekierowaniu. Po wyświetleniu pozostają aktywne przez kolejne 30 sekund, na przykład gdyby użytkownik odświeżył stronę z powodu błędu transmisji - wiadomość nie zniknie od razu. - -Wystarczy wywołać metodę [flashMessage() |api:Nette\Application\UI\Control::flashMessage()], a presenter zajmie się przekazaniem jej do szablonu. Pierwszym parametrem jest tekst wiadomości, a opcjonalnym drugim jej typ (np. error, warning, info). Metoda `flashMessage()` zwraca instancję wiadomości flash, do której można dodać dalsze informacje. - -```php -$this->flashMessage('Element został usunięty.'); -$this->redirect(/* ... */); // i przekierowujemy -``` - -W szablonie wiadomości te dostępne są w zmiennej `$flashes` jako obiekty `stdClass` zawierające właściwości `message` (tekst wiadomości), `type` (typ wiadomości) i ewentualnie wspomniane wcześniej informacje dodane przez użytkownika. Renderujemy je tak: - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -Błąd 404 itd. -============= - -Jeśli żądania nie da się spełnić, na przykład dlatego, że artykuł, który chcemy wyświetlić, nie istnieje w bazie danych, zgłaszamy błąd 404 metodą `error(string $message = '', int $httpCode = 404)`. - -```php -public function renderShow(int $id): void -{ - $article = $this->articles->getById($id); - if (!$article) { - $this->error(); - } - // ... -} -``` - -Kod błędu HTTP można przekazać jako drugi parametr; domyślnie jest to 404. Metoda działa tak, że zgłasza `Nette\Application\BadRequestException`, po czym `Application` przekazuje sterowanie do error-presentera. To presenter, którego zadaniem jest wyświetlenie strony informującej o zaistniałym błędzie. Error-presenter konfiguruje się w [konfiguracji aplikacji|configuration]. - - -Wysyłanie JSON -============== - -Metoda `sendJson($data)` koduje podane dane do JSON, wysyła je jako odpowiedź HTTP i kończy presenter. Przykład: - -```php -public function actionData(): void -{ - $data = ['hello' => 'nette']; - $this->sendJson($data); -} -``` - - -Parametry żądania .{data-version:3.1.14} -======================================== - -Presenter, a także każdy komponent, pozyskuje swoje parametry z żądania HTTP. Ich wartości możesz pobrać metodami `getParameter($name)` albo `getParameters()`. Wartościami są stringi albo tablice stringów, czyli w istocie surowe dane pozyskane bezpośrednio z URL. - -Dla większej wygody zalecamy sięganie po parametry przez właściwości. Wystarczy oznaczyć je atrybutem `#[Parameter]`: - -```php -use Nette\Application\Attributes\Parameter; // ten wiersz jest ważny - -class HomePresenter extends Nette\Application\UI\Presenter -{ - #[Parameter] - public string $theme; // musi być public -} -``` - -Dla właściwości zalecamy podanie typu danych (np. `string`), a Nette automatycznie odpowiednio rzutuje wartość. Wartości parametrów można też [walidować |#Walidacja parametrów]. - -Przy tworzeniu odnośnika możesz ustawić wartość parametru bezpośrednio: - -```latte -kliknij -``` - - -Parametry trwałe -================ - -Parametry trwałe służą do utrzymywania stanu między różnymi żądaniami. Ich wartość pozostaje taka sama również po kliknięciu w odnośnik. W odróżnieniu od danych w sesji przesyłane są w URL. I dzieje się to całkowicie automatycznie, nie ma więc potrzeby podawania ich jawnie w `link()` czy `n:href`. - -Przykładowe zastosowanie? Wyobraź sobie, że masz aplikację wielojęzyczną. Bieżący język to parametr, który musi zawsze być częścią URL. Ale dodawanie go do każdego odnośnika byłoby niesamowicie żmudne. Czynisz więc z niego parametr trwały `lang` i będzie przenoszony automatycznie. Zgrabne! - -Utworzenie parametru trwałego w Nette jest wyjątkowo proste. Wystarczy utworzyć właściwość publiczną i oznaczyć ją atrybutem: (wcześniej używano `/** @persistent */`) - -```php -use Nette\Application\Attributes\Persistent; // ten wiersz jest ważny - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; // musi być public -} -``` - -Jeśli `$this->lang` ma wartość w rodzaju `'en'`, to odnośniki tworzone przez `link()` albo `n:href` będą zawierać również parametr `lang=en`. A po kliknięciu w odnośnik `$this->lang` znów będzie `'en'`. - -Dla właściwości zalecamy podanie typu danych (np. `string`), możesz też podać wartość domyślną. Wartości parametrów można [walidować |#Walidacja parametrów]. - -Parametry trwałe przenoszone są zwykle między wszystkimi akcjami danego presentera. Aby przenosić je również między wieloma presenterami, trzeba zdefiniować je albo: - -- we wspólnym przodku, po którym presentery dziedziczą -- albo w traicie, którego presentery używają: - -```php -trait LanguageAware -{ - #[Persistent] - public string $lang; -} - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - use LanguageAware; -} -``` - -Przy tworzeniu odnośnika wartość parametru trwałego można zmienić: - -```latte -szczegóły po czesku -``` - -Albo *zresetować*, czyli usunąć z URL. Przyjmie wtedy swoją wartość domyślną: - -```latte -kliknij -``` - - -Wspólna przestrzeń parametrów -============================= - -Parametry żądania, [parametry trwałe |#Parametry trwałe] oraz parametry metod `action`, `render` i `handle` (sygnału) dzielą jedną przestrzeń, w której każdy identyfikowany jest swoją nazwą. Jeśli ta sama nazwa pojawi się w kilku z nich, odnoszą się do jednej i tej samej wartości. - -Często wykorzystuje się to z korzyścią. Na przykład parametr trwały `lang` i argument `$lang` metody akcji albo sygnału to jedno i to samo - bieżącą wartość parametru trwałego odczytasz, po prostu wymieniając go w sygnaturze metody: - -```php -#[Persistent] -public string $lang; - -public function handleSearch(string $query, string $lang): void -{ - // $lang zawiera bieżącą wartość parametru trwałego lang -} -``` - -Ponieważ przestrzeń ta jest wspólna, dbaj o unikalność nazw parametrów, chyba że celowo chcesz, aby dzieliły wartość. Dotyczy to również sygnałów, które dodatkowo odczytują parametry z ciała POST żądania, zobacz [Sygnały w głąb |components#Sygnały w głąb]. - - -Komponenty interaktywne -======================= - -Presentery mają wbudowany system komponentów. Komponenty to osobne jednostki wielokrotnego użytku, które osadzamy w presenterach. Mogą to być [formularze |forms:in-presenter], datagridy, menu, w zasadzie wszystko, co warto wykorzystywać wielokrotnie. - -Jak komponenty trafiają do presenterów i jak się ich potem używa? Dowiesz się tego w rozdziale [Komponenty |components]. Dowiesz się nawet, co mają wspólnego z Hollywood. - -A skąd wziąć komponenty? Na [Componette |https://componette.org/search/component] znajdziesz komponenty open source i wiele innych dodatków do Nette, przekazanych przez ochotników ze społeczności frameworka. - - -Idziemy głębiej -=============== - -.[tip] -To, co omówiliśmy dotąd w tym rozdziale, wystarczy zapewne do większości zastosowań. Kolejne sekcje przeznaczone są dla tych, których interesuje głębsze zanurzenie w presentery i którzy chcą wiedzieć absolutnie wszystko. - - -Walidacja parametrów --------------------- - -Wartości [parametrów żądania |#Parametry żądania] i [parametrów trwałych |#Parametry trwałe] otrzymane z URL zapisywane są do właściwości metodą `loadState()`. Sprawdza ona również, czy typ danych podany we właściwości się zgadza, w przeciwnym razie odpowie błędem 404 i strona nie zostanie wyświetlona. - -Nigdy nie ufaj ślepo parametrom otrzymanym z URL, bo użytkownik może je łatwo nadpisać. Tak na przykład sprawdzimy, czy język `$this->lang` należy do obsługiwanych. Odpowiednim sposobem jest nadpisanie wspomnianej metody `loadState()`: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; - - public function loadState(array $params): void - { - parent::loadState($params); // tutaj ustawiane jest $this->lang - // następuje własne sprawdzenie wartości: - if (!in_array($this->lang, ['en', 'cs'])) { - $this->error(); - } - } -} -``` - - -Zapis i przywrócenie żądania ----------------------------- - -Żądanie obsługiwane przez presenter to obiekt [api:Nette\Application\Request], zwracany metodą presentera `getRequest()`. - -Bieżące żądanie można zapisać do sesji albo, odwrotnie, przywrócić je z niej i kazać presenterowi wykonać je ponownie. Przydaje się to na przykład wtedy, gdy użytkownik wypełnia formularz, a jego sesja logowania wygasa. Aby nie utracić danych, przed przekierowaniem na stronę logowania zapisujemy bieżące żądanie do sesji przez `$reqId = $this->storeRequest()`. Zwraca to jego identyfikator w postaci krótkiego stringa, który następnie przekazujemy jako parametr do presentera logowania. - -Po zalogowaniu wywołujemy metodę `$this->restoreRequest($reqId)`, która pobiera żądanie z sesji. Żądania POST są do niego przekazywane, a pozostałe (GET) przekierowywane na URL żądania. Metoda sprawdza, czy żądanie zostało utworzone przez tego samego użytkownika, który jest teraz zalogowany. Jeśli zaloguje się inny użytkownik albo klucz jest nieprawidłowy, nie robi nic, a program działa dalej jak zwykle. - -Zobacz przewodnik [Jak wrócić do poprzedniej strony |best-practices:restore-request]. - - -Kanonizacja ------------ - -Presentery mają naprawdę znakomitą funkcję, która przyczynia się do lepszego SEO (Search Engine Optimization). Automatycznie zapobiegają istnieniu zduplikowanej treści pod różnymi URL. Jeśli do konkretnego celu prowadzi kilka URL, np. `/index` i `/index?page=1`, framework wyznacza jeden z nich jako podstawowy (kanoniczny) i przekierowuje na niego pozostałe kodem HTTP 301. Dzięki temu wyszukiwarki nie indeksują Twoich stron dwa razy i nie rozmywają ich page ranku. - -Proces ten nazywa się kanonizacją. Kanonicznym URL jest ten wygenerowany przez [router|routing], zwykle pierwsza pasująca trasa w kolekcji. - -Kanonizacja jest domyślnie włączona i można ją wyłączyć przez `$this->autoCanonicalize = false`. - -Przekierowanie nie następuje przy żądaniach AJAX ani POST, bo mogłoby to prowadzić do utraty danych albo nie dawałoby żadnej dodatkowej wartości dla SEO. - -Kanonizację możesz też wywołać ręcznie metodą `canonicalize()`. Podobnie jak metodzie `link()`, przekazujesz jej presenter, akcję i parametry. Generuje odnośnik i porównuje go z bieżącym adresem URL. Jeśli się różnią, przekierowuje na wygenerowany odnośnik. - -```php -public function actionShow(int $id, ?string $slug = null): void -{ - $realSlug = $this->facade->getSlugForId($id); - // przekierowuje, jeśli $slug różni się od $realSlug - $this->canonicalize('Product:show', [$id, $realSlug]); -} -``` - -Kompletny wzorzec łączący filtry tras z `canonicalize()` w celu tworzenia przyjaznych SEO adresów URL znajdziesz w [Ładne URL ze slugami |best-practices:pretty-urls]. - - -Odpowiedzi ----------- - -Odpowiedź zwracana przez presenter to obiekt implementujący interfejs [api:Nette\Application\Response]. Dostępnych jest kilka gotowych odpowiedzi: - -- [api:Nette\Application\Responses\CallbackResponse] - wysyła callback -- [api:Nette\Application\Responses\FileResponse] - wysyła plik -- [api:Nette\Application\Responses\ForwardResponse] - forward() -- [api:Nette\Application\Responses\JsonResponse] - wysyła JSON -- [api:Nette\Application\Responses\RedirectResponse] - przekierowanie -- [api:Nette\Application\Responses\TextResponse] - wysyła tekst -- [api:Nette\Application\Responses\VoidResponse] - pusta odpowiedź - -Odpowiedzi wysyła się metodą `sendResponse()`: - -```php -use Nette\Application\Responses; - -// zwykły tekst -$this->sendResponse(new Responses\TextResponse('Hello Nette!')); - -// wysyła plik -$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf')); - -// wysyła callback -$callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) { - if ($httpResponse->getHeader('Content-Type') === 'text/html') { - echo '

    Cześć

    '; - } -}; -$this->sendResponse(new Responses\CallbackResponse($callback)); -``` - -Możesz też napisać własną odpowiedź. Wystarczy zaimplementować interfejs `Nette\Application\Response`, który ma jedną metodę `send()` otrzymującą żądanie i odpowiedź HTTP. Przydaje się to na przykład przy strumieniowaniu danych, których nie chcesz trzymać w pamięci: - -```php -class CsvResponse implements Nette\Application\Response -{ - public function __construct( - private string $fileName, - private iterable $rows, - ) { - } - - public function send(Nette\Http\IRequest $request, Nette\Http\IResponse $response): void - { - $response->setContentType('text/csv', 'utf-8'); - $response->sendAsFile($this->fileName); - - $handle = fopen('php://output', 'w'); - foreach ($this->rows as $row) { - fputcsv($handle, $row); - } - - fclose($handle); - } -} -``` - -Wysyłasz ją potem w presenterze jak zwykle: `$this->sendResponse(new CsvResponse('export.csv', $rows));` - - -Cache HTTP ----------- - -Metoda `lastModified()` ułatwia korzystanie z cache HTTP. Przekazujesz jej datę i czas ostatniej modyfikacji treści (jako timestamp, string albo obiekt `DateTimeInterface`), a opcjonalnie walidator ETag (krótki string identyfikujący bieżącą wersję treści, na przykład jej hash) i czas wygaśnięcia. Jeśli przeglądarka ma już pasującą wersję, presenter wysyła odpowiedź `304 Not Modified` i kończy działanie, dzięki czemu strona nie jest niepotrzebnie renderowana ani przesyłana: - -```php -public function renderArticle(int $id): void -{ - $article = $this->articles->getById($id); - $this->lastModified($article->updatedAt); - // ... -} -``` - - -Finalizacja szablonu .{data-version:3.3.0} ------------------------------------------- - -Gdy presenter renderuje szablon, metoda `sendTemplate()` tuż przed renderowaniem wywołuje `completeTemplate()`. Metoda ta uzupełnia zmienne oznaczone atrybutem `#[TemplateVariable]` i odnajduje plik szablonu (zmienne domyślne ustawia już `TemplateFactory` przy tworzeniu szablonu). Możesz nadpisać tę metodę chronioną, aby dodać zmienne wspólne dla wszystkich widoków albo ustawić inny plik: - -```php -protected function completeTemplate(Nette\Application\UI\Template $template): void -{ - parent::completeTemplate($template); - $template->siteName = 'Moja aplikacja'; -} -``` - - -Ograniczanie dostępu przez `#[Requires]` .{data-version:3.2.3} --------------------------------------------------------------- - -Atrybut `#[Requires]` daje zaawansowane możliwości ograniczania dostępu do presenterów i ich metod. Można nim określić metody HTTP, wymagać żądania AJAX, ograniczyć do tego samego pochodzenia i dopuścić dostęp tylko przez forward. Atrybut można zastosować zarówno do klas presenterów, jak i do poszczególnych metod, takich jak `action()`, `render()`, `handle()` i `createComponent()`. - -Możesz określić te ograniczenia: -- na metody HTTP: `#[Requires(methods: ['GET', 'POST'])]` -- wymaganie żądania AJAX: `#[Requires(ajax: true)]` -- dostęp tylko z tego samego pochodzenia: `#[Requires(sameOrigin: true)]` -- dostęp tylko przez forward: `#[Requires(forward: true)]` -- ograniczenia na konkretne akcje: `#[Requires(actions: 'default')]` - -.[note] -Od wersji 3.3 zgodność pochodzenia weryfikowana jest nagłówkiem przeglądarki `Sec-Fetch-Site` (wcześniej przez ciasteczko SameSite), co jest bardziej niezawodne i sprawdza dokładną zgodność schematu, domeny i portu. - -Szczegóły znajdziesz w przewodniku [Jak używać atrybutu Requires |best-practices:attribute-requires]. - - -Kontrola metody HTTP --------------------- - -Presentery w Nette automatycznie weryfikują metodę HTTP każdego przychodzącego żądania, przede wszystkim ze względów bezpieczeństwa. Domyślnie dozwolone są metody `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH`. - -Jeśli chcesz dodatkowo dopuścić na przykład metodę `OPTIONS`, użyj atrybutu `#[Requires]` (od Nette Application v3.2.3): - -```php -#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])] -class MyPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Od wersji 3.1.13 weryfikacja wykonywana jest w `checkHttpMethod()`, która sprawdza, czy metoda podana w żądaniu znajduje się w tablicy `$presenter->allowedMethods`. Od wersji 3.2.3 podejście to jest przestarzałe na rzecz `#[Requires]`. Metodę możesz nadpisać tak: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - protected function checkHttpMethod(): void - { - $this->allowedMethods[] = 'OPTIONS'; - parent::checkHttpMethod(); - } -} -``` - -Warto podkreślić, że jeśli włączysz metodę `OPTIONS`, musisz następnie odpowiednio ją obsłużyć w swoim presenterze. Metoda ta bywa używana jako tak zwane żądanie preflight, które przeglądarka wysyła automatycznie przed właściwym żądaniem, gdy trzeba ustalić, czy żądanie jest dopuszczalne według polityki CORS (Cross-Origin Resource Sharing). Jeśli włączysz metodę, ale nie zaimplementujesz poprawnej odpowiedzi, może to prowadzić do niespójności i potencjalnych problemów z bezpieczeństwem. - - -Oznaczanie przestarzałych akcji .{data-version:3.2.3} ------------------------------------------------------ - -Atrybut `#[Deprecated]` oznacza akcje, sygnały albo całe presentery jako przestarzałe i przeznaczone do usunięcia w przyszłości. Przy generowaniu odnośników do przestarzałych części aplikacji Nette zgłasza ostrzeżenie, aby zwrócić uwagę programistów. - -Atrybut możesz zastosować albo do całej klasy presentera, albo do poszczególnych metod `action()`, `render()` i `handle()`. - - -Dalsza lektura -============== - -- [Metody i atrybuty inject |best-practices:inject-method-attribute] -- [Składanie presenterów z traitów |best-practices:presenter-traits] -- [Przekazywanie ustawień do presenterów |best-practices:passing-settings-to-presenters] -- [Jak wrócić do poprzedniej strony |best-practices:restore-request] diff --git a/application/pl/routing.texy b/application/pl/routing.texy deleted file mode 100644 index b93081a766..0000000000 --- a/application/pl/routing.texy +++ /dev/null @@ -1,749 +0,0 @@ -Routing -******* - -
    - -Router zajmuje się wszystkim, co dotyczy adresów URL, dzięki czemu nie musisz o nich myśleć. Pokażemy Ci: - -- jak skonfigurować router, aby URL wyglądały tak, jak chcesz -- omówimy SEO i przekierowania -- i pokażemy, jak napisać własny router - -
    - - -Bardziej przyjazne dla ludzi URL (znane też jako cool albo pretty URL) są użyteczniejsze, łatwiejsze do zapamiętania i pozytywnie wpływają na SEO. Nette ma to na uwadze i w pełni wychodzi naprzeciw potrzebom programistów. Możesz zaprojektować dokładnie taką strukturę URL, jakiej chcesz dla swojej aplikacji. Możesz zaprojektować ją nawet wtedy, gdy aplikacja jest już gotowa, bo nie wymaga to zmian w kodzie ani szablonach. Definiuje się ją elegancko w [jednym miejscu |#Integracja], w routerze, zamiast rozpraszać ją jako adnotacje po wszystkich presenterach. - -Router w Nette jest wyjątkowy, bo jest **dwukierunkowy**. Potrafi zarówno dekodować URL z żądań HTTP, jak i tworzyć odnośniki. Odgrywa więc kluczową rolę w [Nette Application |how-it-works#Nette Application], bo nie tylko decyduje, który presenter i akcja obsłużą bieżące żądanie, ale służy też do [generowania URL |creating-links] w szablonach itd. - -Router nie ogranicza się jednak tylko do tego zastosowania; możesz używać go w aplikacjach, w których presentery w ogóle nie występują, do REST API itd. Więcej szczegółów w sekcji [#Użycie samodzielne]. - - -Kolekcja tras -============= - -Najprzyjemniejszy sposób definiowania struktury adresów URL w aplikacji oferuje klasa [api:Nette\Application\Routers\RouteList]. Definicja składa się z listy tak zwanych tras, czyli masek adresów URL oraz powiązanych z nimi presenterów i akcji, zapisanych prostym API. Tras nie musimy w żaden sposób nazywać. - -```php -$router = new Nette\Application\Routers\RouteList; -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('article/', 'Article:view'); -// ... -``` - -Przykład pokazuje, że jeśli otworzymy w przeglądarce `https://domain.com/rss.xml`, wyświetli się presenter `Feed` z akcją `rss`. Jeśli `https://domain.com/article/12`, wyświetli się presenter `Article` z akcją `view` itd. Jeśli nie znajdzie się odpowiednia trasa, Nette Application odpowiada, zgłaszając [BadRequestException |api:Nette\Application\BadRequestException], który wyświetlany jest użytkownikowi jako strona błędu 404 Not Found. - - -Kolejność tras --------------- - -**Kolejność**, w jakiej wymieniane są poszczególne trasy, jest absolutnie **kluczowa**, bo są one obliczane kolejno od góry do dołu. Zasada brzmi: deklarujemy trasy **od konkretnych do ogólnych**: - -```php -// ŹLE: 'rss.xml' przechwycone zostanie przez pierwszą trasę i zrozumiane jako -$router->addRoute('', 'Article:view'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// DOBRZE -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('', 'Article:view'); -``` - -Trasy obliczane są od góry do dołu również przy generowaniu odnośników: - -```php -// ŹLE: odnośnik do 'Feed:rss' wygeneruje się jako 'admin/feed/rss' -$router->addRoute('admin//', 'Admin:default'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// DOBRZE -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('admin//', 'Admin:default'); -``` - -Nie będziemy ukrywać, że poprawne złożenie tras wymaga pewnej wprawy. Zanim ją opanujesz, przydatnym narzędziem będzie [panel routingu |#Debugowanie routera]. - - -Maska i parametry ------------------ - -Maska opisuje ścieżkę względną od katalogu głównego witryny. Najprostszą maską jest statyczny URL: - -```php -$router->addRoute('products', 'Products:default'); -``` - -Często maski zawierają tak zwane **parametry**. Ujmowane są one w nawiasy ostre (np. ``) i przekazywane do docelowego presentera, na przykład do metody `renderShow(int $year)` albo do parametru trwałego `$year`: - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -Przykład pokazuje, że jeśli otworzymy w przeglądarce `https://example.com/chronicle/2020`, wyświetli się presenter `History` z akcją `show` i parametrem `year: 2020`. - -Wartość domyślną parametrów możemy podać bezpośrednio w masce, czyniąc je opcjonalnymi: - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -Trasa przyjmie teraz również URL `https://example.com/chronicle/`, który znów wyświetli `History:show` z parametrem `year: 2020`. - -Parametrami mogą być oczywiście również nazwy presentera i akcji. Na przykład: - -```php -$router->addRoute('/', 'Home:default'); -``` - -Podana trasa przyjmuje na przykład URL w postaci `/article/edit` albo `/catalog/list` i rozumie je odpowiednio jako presentery i akcje `Article:edit` oraz `Catalog:list`. - -Jednocześnie nadaje parametrom `presenter` i `action` wartości domyślne `Home` i `default`, czyniąc również je opcjonalnymi. Trasa przyjmuje więc także URL `/article` i rozumie go jako `Article:default`. Albo odwrotnie, odnośnik do `Product:default` generuje ścieżkę `/product`, a odnośnik do domyślnego `Home:default` generuje ścieżkę `/`. - -Maska może opisywać nie tylko ścieżkę względną od katalogu głównego witryny, ale też ścieżkę bezwzględną, jeśli zaczyna się ukośnikiem, albo nawet cały bezwzględny URL, jeśli zaczyna się dwoma ukośnikami: - -```php -// względem document root -$router->addRoute('/', /* ... */); - -// ścieżka bezwzględna (względem domeny) -$router->addRoute('//', /* ... */); - -// bezwzględny URL wraz z domeną (względem schematu) -$router->addRoute('//.example.com//', /* ... */); - -// bezwzględny URL wraz ze schematem -$router->addRoute('https://.example.com//', /* ... */); -``` - - -Wyrażenia walidacyjne ---------------------- - -Dla każdego parametru można podać warunek walidacyjny za pomocą [wyrażenia regularnego|https://www.php.net/manual/en/reference.pcre.pattern.syntax.php]. Na przykład dla parametru `id` określamy wyrażeniem `\d+`, że może zawierać wyłącznie cyfry: - -```php -$router->addRoute('/[/]', /* ... */); -``` - -Domyślnym wyrażeniem regularnym dla wszystkich parametrów jest `[^/]+`, czyli wszystko poza ukośnikiem. Jeśli parametr ma przyjmować również ukośniki, ustawiamy wyrażenie na `.+`: - -```php -// przyjmuje https://example.com/a/b/c, path będzie 'a/b/c' -$router->addRoute('', /* ... */); -``` - - -Sekwencje opcjonalne --------------------- - -W masce można oznaczyć części opcjonalne nawiasami kwadratowymi. Opcjonalna może być dowolna część maski i może zawierać parametry: - -```php -$router->addRoute('[/]', /* ... */); - -// przyjmuje ścieżki: -// /en/download => lang => en, name => download -// /download => lang => null, name => download -``` - -Gdy parametr jest częścią sekwencji opcjonalnej, naturalnie również staje się opcjonalny. Jeśli nie ma podanej wartości domyślnej, będzie miał wartość null. - -Części opcjonalne mogą znajdować się także w domenie: - -```php -$router->addRoute('//[.]example.com//', /* ... */); -``` - -Sekwencje można dowolnie zagnieżdżać i łączyć: - -```php -$router->addRoute( - '[[-]/][/page-]', - 'Home:default', -); - -// przyjmuje ścieżki: -// /en/hello -// /en-us/hello -// /hello -// /hello/page-12 -``` - -Przy generowaniu URL preferowany jest wariant najkrótszy, więc wszystko, co można pominąć, zostaje pominięte. Dlatego na przykład trasa `index[.html]` generuje ścieżkę `/index`. To zachowanie można odwrócić, umieszczając wykrzyknik za lewym nawiasem kwadratowym: - -```php -// przyjmuje /hello i /hello.html, generuje /hello -$router->addRoute('[.html]', /* ... */); - -// przyjmuje /hello i /hello.html, generuje /hello.html -$router->addRoute('[!.html]', /* ... */); -``` - -Parametry opcjonalne (czyli parametry z wartością domyślną) bez nawiasów kwadratowych zachowują się w istocie tak, jakby były ujęte w następujący sposób: - -```php -$router->addRoute('//', /* ... */); - -// odpowiada temu: -$router->addRoute('[/[/[]]]', /* ... */); -``` - -Jeśli chcemy wpłynąć na zachowanie końcowego ukośnika, aby na przykład zamiast `/home/` generowało się `/home`, można to osiągnąć tak: - -```php -$router->addRoute('[[/[/]]]', /* ... */); -``` - - -Symbole wieloznaczne --------------------- - -W masce bezwzględnego URL możemy użyć poniższych symboli wieloznacznych, aby nie musieć na przykład wpisywać do maski domeny, która może różnić się między środowiskiem deweloperskim a produkcyjnym: - -- `%tld%` = domena najwyższego poziomu, np. `com` albo `org` -- `%sld%` = domena drugiego poziomu, np. `example` -- `%domain%` = domena bez subdomen, np. `example.com` -- `%host%` = cały host, np. `www.example.com` -- `%basePath%` = ścieżka do katalogu głównego - -```php -$router->addRoute('//www.%domain%/%basePath%//', /* ... */); -$router->addRoute('//www.%sld%.%tld%/%basePath%//', /* ... */); -``` - - -Zapis zaawansowany ------------------- - -Cel trasy, zapisywany zwykle w formacie `Presenter:akcja`, można zapisać również za pomocą tablicy definiującej poszczególne parametry i ich wartości domyślne: - -```php -$router->addRoute('/[/]', [ - 'presenter' => 'Home', - 'action' => 'default', -]); -``` - -Do dokładniejszego określenia można użyć jeszcze bardziej rozbudowanej postaci, w której poza wartościami domyślnymi możemy ustawić inne właściwości parametrów, na przykład walidacyjne wyrażenie regularne (zobacz parametr `id`): - -```php -use Nette\Routing\Route; - -$router->addRoute('/[/]', [ - 'presenter' => [ - Route::Value => 'Home', - ], - 'action' => [ - Route::Value => 'default', - ], - 'id' => [ - Route::Pattern => '\d+', - ], -]); -``` - -Ważne, aby zauważyć, że jeśli parametry zdefiniowane w tablicy nie są wymienione w masce ścieżki, ich wartości nie da się zmienić, nawet za pomocą parametrów zapytania podanych w URL po znaku zapytania. - -Przydaje się to przy **parametrach stałych** - aby nadać konkretnej stronie krótki, łatwy do zapamiętania URL. Na przykład aby `/tos` zawsze otwierało `Article:view` z `id: 123`: - -```php -$router->addRoute('tos', [ - 'presenter' => 'Article', - 'action' => 'view', - 'id' => 123, -]); -``` - - -Filtry i tłumaczenia --------------------- - -Kod źródłowy aplikacji piszemy po angielsku, ale jeśli witryna ma mieć czeskie URL, to proste routowanie w rodzaju: - -```php -$router->addRoute('/', 'Home:default'); -``` - -wygeneruje angielskie URL, takie jak `/product/123` czy `/cart`. Jeśli chcemy, aby presentery i akcje w URL reprezentowane były czeskimi słowami (np. `/produkt/123` albo `/kosik`), możemy użyć słownika tłumaczeń. Aby go zapisać, potrzebujemy już "gadatliwszego" wariantu drugiego parametru: - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterTable => [ - // string w URL => presenter - 'produkt' => 'Product', - 'kosik' => 'Cart', - 'katalog' => 'Catalog', - ], - ], - 'action' => [ - Route::Value => 'default', - Route::FilterTable => [ - 'seznam' => 'list', - ], - ], -]); -``` - -Kilka kluczy w słowniku tłumaczeń może prowadzić do tego samego presentera. Powstają w ten sposób jego rozmaite aliasy. Ostatni klucz uznawany jest za wariant kanoniczny (czyli ten, który znajdzie się w wygenerowanym URL). - -Tablicy tłumaczeń można w ten sposób użyć dla dowolnego parametru. Jeśli tłumaczenie nie istnieje, brana jest wartość pierwotna. To zachowanie możemy zmienić, dodając `Route::FilterStrict => true`, a trasa odrzuci wtedy URL, jeśli wartości nie ma w słowniku. - -Poza słownikiem tłumaczeń w postaci tablicy można zastosować własne funkcje tłumaczące. - -```php -use Nette\Routing\Route; - -$router->addRoute('//', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterIn => function (string $s): string { /* ... */ }, - Route::FilterOut => function (string $s): string { /* ... */ }, - ], - 'action' => 'default', - 'id' => null, -]); -``` - -Funkcja `Route::FilterIn` konwertuje między parametrem w URL a stringiem, który przekazywany jest następnie do presentera; funkcja `FilterOut` zapewnia konwersję w kierunku odwrotnym. - -Parametry `presenter`, `action` i `module` mają już predefiniowane filtry konwertujące między stylem PascalCase czy camelCase a kebab-case używanym w URL. Wartość domyślna parametrów zapisywana jest w postaci, w jakiej przekazywana jest do aplikacji (PascalCase dla presentera i modułu, camelCase dla akcji), więc na przykład w przypadku presentera piszemy ``, a nie ``. - - -Filtry ogólne -------------- - -Poza filtrami przeznaczonymi dla konkretnych parametrów możemy zdefiniować również filtry ogólne, które otrzymują tablicę asocjacyjną wszystkich parametrów, mogą ją dowolnie modyfikować i następnie zwrócić. Filtry ogólne definiuje się pod pustym kluczem. - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => 'Home', - 'action' => 'default', - '' => [ - Route::FilterIn => function (array $params): array { /* ... */ }, - Route::FilterOut => function (array $params): array { /* ... */ }, - ], -]); -``` - -Filtry ogólne dają możliwość modyfikowania zachowania trasy w absolutnie dowolny sposób. Możemy użyć ich na przykład do modyfikowania parametrów na podstawie innych parametrów. Choćby do tłumaczenia `` i `` na podstawie bieżącej wartości parametru ``. - -Jeśli parametr ma zdefiniowany własny filtr i istnieje również filtr ogólny, własny `FilterIn` wykonywany jest przed ogólnym, a odwrotnie, ogólny `FilterOut` wykonywany jest przed własnym. Wewnątrz filtra ogólnego wartości parametrów `presenter` i `action` zapisane są więc odpowiednio w stylu PascalCase i camelCase. - -Praktyczne zastosowanie tych filtrów, czyli generowanie przyjaznych SEO adresów URL, takich jak `/article/123-how-to-bake-bread`, bez modyfikowania jakichkolwiek szablonów, znajdziesz w [Ładne URL ze slugami |best-practices:pretty-urls]. - - -Flaga OneWay ------------- - -Trasy jednokierunkowe służą do utrzymania działania starych URL, których aplikacja już nie generuje, ale nadal je przyjmuje. Oznaczamy je flagą `OneWay`: - -```php -// stary URL /product-info?id=123 -$router->addRoute('product-info', 'Product:detail', oneWay: true); -// nowy URL /product/123 -$router->addRoute('product/', 'Product:detail'); -``` - -Przy wejściu na stary URL presenter automatycznie przekierowuje na nowy URL, dzięki czemu wyszukiwarki nie zaindeksują tych stron dwa razy (zobacz [#SEO i kanonizacja]). - - -Routing dynamiczny z callbackami --------------------------------- - -Routing dynamiczny z callbackami pozwala przypisywać trasom bezpośrednio funkcje (callbacki), które wykonują się przy wejściu na daną ścieżkę. Ta elastyczna funkcjonalność pozwala szybko i sprawnie tworzyć rozmaite endpointy dla Twojej aplikacji: - -```php -$router->addRoute('test', function () { - echo 'Jesteś pod adresem /test'; -}); -``` - -W masce możesz też zdefiniować parametry, które automatycznie przekazywane są do Twojego callbacku: - -```php -$router->addRoute('', function (string $lang) { - echo match ($lang) { - 'cs' => 'Witamy w czeskiej wersji naszej witryny!', - 'en' => 'Witamy w angielskiej wersji naszej witryny!', - }; -}); -``` - -Poza parametrami z maski callback może otrzymać również usługi z kontenera DI. Przekazywane są na podstawie typu parametru. Ponadto parametr `$presenter` otrzymuje instancję [MicroPresenter |api:NetteModule\MicroPresenter], która obsługuje trasę: - -```php -$router->addRoute('', function (string $lang, Nette\Http\Request $httpRequest, NetteModule\MicroPresenter $presenter) { - // ... -}); -``` - - -Moduły ------- - -Jeśli mamy kilka tras należących do wspólnego [modułu |directory-structure#Presentery i szablony], używamy `withModule()`. Podany moduł dodawany jest automatycznie przed presenterem każdej trasy w grupie i całkowicie znika z URL: - -```php -$router = new RouteList; -$router->withModule('Forum') // kolejne trasy są częścią modułu Forum - ->addRoute('rss', 'Feed:rss') // presenterem będzie Forum:Feed - ->addRoute('/') - - ->withModule('Admin') // kolejne trasy są częścią modułu Forum:Admin - ->addRoute('sign:in', 'Sign:in'); -``` - -Alternatywą jest parametr `module`, który podobnie ustawia stały moduł i trzyma go poza URL: - -```php -// URL manage/dashboard/default mapuje się na presenter Admin:Dashboard -$router->addRoute('manage//', [ - 'module' => 'Admin', -]); -``` - -Każda nazwa presentera jest kompletna dopiero wraz ze swoim modułem, np. `Front:Admin:ProductList`. Ilekroć taka pełna nazwa trafi do parametru URL, router koduje ją według dwóch prostych reguł: każdy dwukropek `:` (separator modułu) staje się **kropką**, a każda granica słowa w nazwie PascalCase staje się **myślnikiem**. `Front:Admin:ProductList` pojawia się więc w URL jako `front.admin.product-list` i w ten sam sposób jest dekodowana z powrotem. Dlatego aplikacja modułowa, bez żadnego z powyższych narzędzi, produkuje URL pełne kropek. - -Zarówno `withModule()`, jak i parametr `module` unikają tego właśnie dlatego, że odcinają znany prefiks modułu od nazwy presentera, zanim trafi ona do URL: skoro moduł jest stały, w ogóle nie trzeba go kodować. - -Czasem chcemy, aby sam moduł się zmieniał i pojawiał w URL, sięgamy więc po `` bezpośrednio w masce. Uwaga na jeden kluczowy szczegół: **`` przechwytuje całą ścieżkę modułu** - wszystko aż do ostatniego dwukropka w nazwie presentera. Dla presentera `Shop:Admin:Product` oznacza to moduł `Shop:Admin` i presenter `Product`, a ponieważ dwukropki stają się kropkami, otrzymamy: - - -Subdomeny ---------- - -Kolekcje tras można podzielić według subdomen: - -```php -$router = new RouteList; -$router->withDomain('example.com') - ->addRoute('rss', 'Feed:rss') - ->addRoute('/'); -``` - -W nazwie domeny można użyć również [#Symbole wieloznaczne]: - -```php -$router = new RouteList; -$router->withDomain('example.%tld%') - // ... -``` - - -Prefiks ścieżki ---------------- - -Kolekcje tras można podzielić według ścieżki w URL: - -```php -$router = new RouteList; -$router->withPath('eshop') - ->addRoute('rss', 'Feed:rss') // pasuje do URL /eshop/rss - ->addRoute('/'); // pasuje do URL /eshop// -``` - - -Kombinacje ----------- - -Powyższe grupowania można ze sobą łączyć: - -```php -$router = (new RouteList) - ->withDomain('admin.example.com') - ->withModule('Admin') - ->addRoute(/* ... */) - ->addRoute(/* ... */) - ->end() - ->withModule('Images') - ->addRoute(/* ... */) - ->end() - ->end() - ->withDomain('example.com') - ->withPath('export') - ->addRoute(/* ... */) - // ... -``` - - -Parametry zapytania -------------------- - -Maski mogą zawierać również parametry zapytania (parametry po znaku zapytania w URL). Nie da się dla nich zdefiniować wyrażenia walidacyjnego, ale można zmienić nazwę, pod jaką przekazywane są do presentera: - -```php -// chcemy używać parametru zapytania 'cat' w aplikacji pod nazwą 'categoryId' -$router->addRoute('product ? id= & cat=', /* ... */); -``` - - -Parametry foo -------------- - -Teraz schodzimy głębiej. Parametry foo to w istocie parametry nienazwane, pozwalające dopasować wyrażenie regularne. Przykładem jest trasa przyjmująca `/index`, `/index.html`, `/index.htm` i `/index.php`: - -```php -$router->addRoute('index', /* ... */); -``` - -Można też jawnie zdefiniować string, który zostanie użyty przy generowaniu URL. String trzeba umieścić bezpośrednio za znakiem zapytania. Poniższa trasa jest podobna do poprzedniej, ale generuje `/index.html` zamiast `/index`, bo jako wartość do generowania ustawiono string `.html`: - -```php -$router->addRoute('index', /* ... */); -``` - - -Integracja -========== - -Aby zintegrować utworzony router z aplikacją, musimy powiedzieć o nim kontenerowi DI. Najprościej przygotować fabrykę, która utworzy obiekt routera, i powiedzieć kontenerowi w konfiguracji, aby jej użył. Powiedzmy, że napiszemy w tym celu metodę `App\Core\RouterFactory::createRouter()`: - -```php -namespace App\Core; - -use Nette\Application\Routers\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute(/* ... */); - return $router; - } -} -``` - -Następnie zapisujemy w [konfiguracji |dependency-injection:services]: - -```neon -services: - - App\Core\RouterFactory::createRouter -``` - -Wszelkie zależności, na przykład od bazy danych itd., przekazywane są metodzie fabrycznej jako jej parametry za pomocą [autowiringu|dependency-injection:autowiring]: - -```php -public static function createRouter(Nette\Database\Connection $db): RouteList -{ - // ... -} -``` - - -SimpleRouter -============ - -O wiele prostszym routerem niż kolekcja tras jest [SimpleRouter |api:Nette\Application\Routers\SimpleRouter]. Używamy go, gdy nie mamy szczególnych wymagań co do postaci URL, gdy `mod_rewrite` (albo jego alternatywy) nie jest dostępny albo gdy nie chcemy jeszcze zajmować się ładnymi URL. - -Generuje adresy mniej więcej w takiej postaci: - -``` -http://example.com/?presenter=Product&action=detail&id=123 -``` - -Parametrem konstruktora `SimpleRouter` jest domyślny presenter i akcja, czyli akcja, która ma się wykonać, jeśli otworzymy np. `http://example.com/` bez dodatkowych parametrów. - -```php -// domyślnym presenterem będzie 'Home', a akcją 'default' -$router = new Nette\Application\Routers\SimpleRouter('Home:default'); -``` - -Zalecamy definiowanie SimpleRoutera bezpośrednio w [konfiguracji |dependency-injection:services]: - -```neon -services: - - Nette\Application\Routers\SimpleRouter('Home:default') -``` - - -SEO i kanonizacja -================= - -Framework przyczynia się do SEO (Search Engine Optimization), zapobiegając zduplikowanej treści pod różnymi URL. Jeśli do określonego celu prowadzi kilka adresów, np. `/index` i `/index.html`, framework wyznacza pierwszy z nich jako podstawowy (kanoniczny) i przekierowuje na niego pozostałe kodem HTTP 301. Dzięki temu wyszukiwarki nie indeksują stron dwa razy i nie rozmywają ich page ranku. - -Proces ten nazywa się kanonizacją. Kanonicznym URL jest ten wygenerowany przez router, czyli przez pierwszą pasującą trasę w kolekcji bez flagi OneWay. Dlatego w kolekcji wymieniamy **najpierw trasy podstawowe**. - -Kanonizację wykonuje presenter, więcej w rozdziale [kanonizacja |presenters#Kanonizacja]. - - -HTTPS -===== - -Aby używać protokołu HTTPS, trzeba włączyć go na hostingu i poprawnie skonfigurować serwer. - -Przekierowanie całej witryny na HTTPS trzeba ustawić na poziomie serwera, na przykład za pomocą pliku `.htaccess` w katalogu głównym naszej aplikacji, z kodem HTTP 301. Ustawienia mogą różnić się w zależności od hostingu i wyglądają mniej więcej tak: - -``` - - RewriteEngine On - ... - RewriteCond %{HTTPS} off - RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] - ... - -``` - -Router generuje URL z tym samym protokołem, z jakim wczytano stronę, więc nic więcej nie trzeba ustawiać. - -Jeśli jednak wyjątkowo potrzebujemy, aby różne trasy działały pod różnymi protokołami, podajemy to w masce trasy: - -```php -// wygeneruje adres HTTP -$router->addRoute('http://%host%//', /* ... */); - -// wygeneruje adres HTTPS -$router->addRoute('https://%host%//', /* ... */); -``` - - -Debugowanie routera -=================== - -Panel routingu wyświetlany w [pasku Tracy |tracy:] to przydatny pomocnik, który pokazuje listę tras, a także parametry, które router pozyskał z URL. - -Zielony pasek z symbolem ✓ reprezentuje trasę, która obsłużyła bieżący URL; kolor niebieski i symbol ≈ oznaczają trasy, które również obsłużyłyby URL, gdyby zielona ich nie wyprzedziła. Dalej widzimy bieżący presenter i akcję. - -[* routing-debugger.webp *] - -Jednocześnie, jeśli dojdzie do nieoczekiwanego przekierowania z powodu [kanonizacji |#SEO i kanonizacja], warto zajrzeć do panelu w pasek *redirect*, gdzie dowiesz się, jak router pierwotnie zrozumiał URL i dlaczego przekierował. - -.[note] -Przy debugowaniu routera zalecamy otworzyć w przeglądarce Developer Tools (Ctrl+Shift+I albo Cmd+Option+I) i wyłączyć cache w panelu Network, aby przekierowania nie były w nim przechowywane. - - -Wydajność -========= - -Liczba tras wpływa na szybkość routera. Ich liczba zdecydowanie nie powinna przekraczać kilkudziesięciu. Jeśli Twoja witryna ma zbyt skomplikowaną strukturę URL, możesz napisać [własny router |#Własny router]. - -Jeśli router nie ma zależności, na przykład od bazy danych, a jego fabryka nie przyjmuje argumentów, możemy zserializować jego skompilowaną postać bezpośrednio do kontenera DI i tym samym nieco przyspieszyć aplikację. - -```neon -routing: - cache: true -``` - - -Własny router -============= - -Poniższe wiersze przeznaczone są dla bardzo zaawansowanych użytkowników. Możesz utworzyć własny router i naturalnie włączyć go do kolekcji tras. Router to implementacja interfejsu [api:Nette\Routing\Router] z dwiema metodami: - -```php -use Nette\Http\IRequest as HttpRequest; -use Nette\Http\UrlScript; - -class MyRouter implements Nette\Routing\Router -{ - public function match(HttpRequest $httpRequest): ?array - { - // ... - } - - public function constructUrl(array $params, UrlScript $refUrl): ?string - { - // ... - } -} -``` - -Metoda `match` przetwarza bieżące żądanie [$httpRequest |http:request], z którego można pozyskać nie tylko URL, ale też nagłówki itd., w tablicę zawierającą nazwę presentera i jego parametry. Jeśli nie potrafi obsłużyć żądania, zwraca null. Przy obsłudze żądania musimy zwrócić przynajmniej presenter; akcja jest opcjonalna i przy braku podania przyjmuje wartość `default`. Nazwa presentera jest kompletna i zawiera ewentualne moduły: - -```php -[ - 'presenter' => 'Front:Home', - 'action' => 'default', -] -``` - -Metoda `constructUrl` odwrotnie składa z tablicy parametrów wynikowy bezwzględny URL. Może wykorzystać informacje z parametru [`$refUrl`|api:Nette\Http\UrlScript], którym jest bieżący URL. - -Dodasz go do kolekcji tras metodą `add()`: - -```php -$router = new Nette\Application\Routers\RouteList; -$router->add($myRouter); -$router->addRoute(/* ... */); -// ... -``` - - -Użycie samodzielne -================== - -Przez użycie samodzielne rozumiemy wykorzystanie możliwości routera w aplikacji, która nie używa Nette Application ani presenterów. Dotyczy jej niemal wszystko, co pokazaliśmy w tym rozdziale, z tymi różnicami: - -- dla kolekcji tras używamy klasy [api:Nette\Routing\RouteList] -- jako prostego routera klasy [api:Nette\Routing\SimpleRouter] -- ponieważ para `Presenter:akcja` nie istnieje, używamy [#Zapis zaawansowany] - -Znów tworzymy więc metodę, która złoży nam router, np.: - -```php -namespace App\Core; - -use Nette\Routing\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute('rss.xml', [ - 'controller' => 'RssFeedController', - ]); - $router->addRoute('article/', [ - 'controller' => 'ArticleController', - ]); - // ... - return $router; - } -} -``` - -Jeśli używasz kontenera DI, co zalecamy, dodaj metodę ponownie do konfiguracji, a następnie pozyskaj router wraz z żądaniem HTTP z kontenera: - -```php -$router = $container->getByType(Nette\Routing\Router::class); -$httpRequest = $container->getByType(Nette\Http\IRequest::class); -``` - -Albo utwórz obiekty bezpośrednio: - -```php -$router = App\Core\RouterFactory::createRouter(); -$httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); -``` - -Teraz pozostaje już tylko pozwolić routerowi wykonać swoją pracę: - -```php -$params = $router->match($httpRequest); -if ($params === null) { - // nie znaleziono pasującej trasy, wysyłamy błąd 404 - exit; -} - -// przetwarzamy pozyskane parametry -$controller = $params['controller']; -// ... -``` - -I odwrotnie, użyj routera do złożenia odnośnika: - -```php -$params = ['controller' => 'ArticleController', 'id' => 123]; -$url = $router->constructUrl($params, $httpRequest->getUrl()); -``` - - -{{composer: nette/routing}} -{{repo: nette/routing}} -{{api: https://api.nette.org/routing/}} diff --git a/application/pl/templates.texy b/application/pl/templates.texy deleted file mode 100644 index 790c08acc3..0000000000 --- a/application/pl/templates.texy +++ /dev/null @@ -1,419 +0,0 @@ -Szablony -******** - -.[perex] -Nette używa systemu szablonów [Latte |latte:]. Latte jest używane, bo to najbezpieczniejszy system szablonów dla PHP, a zarazem najbardziej intuicyjny. Nie musisz uczyć się wiele nowego; wystarczy znajomość PHP i kilku tagów. - -Powszechnie strona składa się z szablonu layoutu i szablonu konkretnej akcji. Tak może wyglądać szablon layoutu; zwróć uwagę na bloki `{block}` i tag `{include}`: - -```latte - - - - {block title}Moja aplikacja{/block} - - -
    ...
    - {include content} -
    ...
    - - -``` - -A tak wyglądałby szablon akcji: - -```latte -{block title}Strona główna{/block} - -{block content} -

    Strona główna

    -... -{/block} -``` - -Definiuje on blok `content`, który wstawiany jest w layoucie w miejsce `{include content}`, a także na nowo definiuje blok `title`, który nadpisuje `{block title}` w layoucie. Spróbuj wyobrazić sobie wynik. - - -Wyszukiwanie szablonów ----------------------- - -W presenterach nie musisz podawać, który szablon ma zostać wyrenderowany; framework sam wywnioskuje ścieżkę, oszczędzając Ci pisania. - -Jeśli używasz struktury katalogów, w której każdy presenter ma własny katalog, po prostu umieść szablon w tym katalogu pod nazwą akcji (czyli widoku). Na przykład dla akcji `default` użyj szablonu `default.latte`: - -/--pre -app/ -└── Presentation/ - └── Home/ - ├── HomePresenter.php - └── default.latte -\-- - -Jeśli używasz struktury, w której presentery leżą razem w jednym katalogu, a szablony w folderze `templates`, zapisz go albo w pliku `..latte`, albo `/.latte`: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── Home/ - │ └── default.latte ← 1. wariant - └── Home.default.latte ← 2. wariant -\-- - -Katalog `templates` może leżeć też o poziom wyżej, czyli na tym samym poziomie co katalog z klasami presenterów. - -Jeśli szablon się nie znajdzie, presenter odpowiada [błędem 404 - strona nie znaleziona |presenters#Błąd 404 itd.]. - -Widok możesz zmienić przez `$this->setView('otherView')`. Można też bezpośrednio wskazać plik szablonu przez `$this->template->setFile('/path/to/template.latte')`. - -.[note] -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 layoutu ------------------------------ - -Nette automatycznie wyszukuje również plik layoutu. - -Jeśli używasz struktury katalogów, w której każdy presenter ma własny katalog, umieść layout albo w folderze z presenterem, jeśli jest przeznaczony tylko dla niego, albo o poziom wyżej, jeśli jest wspólny dla kilku presenterów: - -/--pre -app/ -└── Presentation/ - ├── @layout.latte ← wspólny layout - └── Home/ - ├── @layout.latte ← tylko dla presentera Home - ├── HomePresenter.php - └── default.latte -\-- - -Jeśli używasz struktury, w której presentery zgrupowane są w jednym katalogu, a szablony w folderze `templates`, layout będzie oczekiwany w tych miejscach: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── @layout.latte ← wspólny layout - ├── Home/ - │ └── @layout.latte ← tylko dla Home, 1. wariant - └── Home.@layout.latte ← tylko dla Home, 2. wariant -\-- - -Jeśli presenter leży w module, wyszukiwanie postępuje też wyżej po poziomach katalogów, zgodnie z zagnieżdżeniem modułów. - -Nazwę layoutu można zmienić przez `$this->setLayout('layoutAdmin')`, a wtedy będzie oczekiwany w pliku `@layoutAdmin.latte`. Możesz też bezpośrednio wskazać plik szablonu layoutu przez `$this->setLayout('/path/to/template.latte')`. - -Użycie `$this->setLayout(false)` albo tagu `{layout none}` wewnątrz szablonu wyłącza wyszukiwanie layoutu. - -.[note] -Pliki, w których wyszukiwane są szablony layoutu, można zmienić, nadpisując metodę [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], która zwraca tablicę możliwych nazw plików. - - -Zmienne szablonu ----------------- - -Zmienne przekazuje się do szablonów, zapisując je do `$this->template`. Stają się wtedy dostępne w szablonie jako zmienne lokalne: - -```php -$this->template->article = $this->articles->getById($id); -``` - -Aby wartość właściwości była automatycznie przekazywana do szablonu jako zmienna, oznacz ją atrybutem `#[TemplateVariable]` i widocznością public: .{data-version:3.2.9} - -```php -use Nette\Application\Attributes\TemplateVariable; - -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - #[TemplateVariable] - public string $siteName = 'Mój blog'; -} -``` - -Jeśli przekażesz do szablonu zmienną o tej samej nazwie, `#[TemplateVariable]` jej nie nadpisze. - - -Zmienne domyślne ----------------- - -Presentery i komponenty automatycznie przekazują do szablonów kilka przydatnych zmiennych: - -- `$basePath` to bezwzględna ścieżka URL do katalogu głównego (np. `/eshop`) -- `$baseUrl` to bezwzględny URL katalogu głównego (np. `http://localhost/eshop`) -- `$user` to obiekt [reprezentujący użytkownika |security:authentication] -- `$presenter` to bieżący presenter -- `$control` to bieżący komponent albo presenter -- `$flashes` to tablica [wiadomości |presenters#Wiadomości flash] wysłanych funkcją `flashMessage()` - -Jeśli używasz własnej klasy szablonu, zmienne te są przekazywane, o ile utworzysz dla nich właściwość. - - -Szablony bezpieczne typowo --------------------------- - -Przy tworzeniu solidnych aplikacji przydaje się jawne określenie, jakich zmiennych oczekuje szablon i jakich są typów. Daje to kontrolę typów w PHP, inteligentne podpowiedzi w IDE i pozwala analizie statycznej wyłapywać błędy. - -Jak zdefiniować taką listę? Po prostu jako klasę z właściwościami reprezentującymi zmienne szablonu. Nazwij ją podobnie jak presenter, tylko z `Template` na końcu: - -```php -/** - * @property-read ArticleTemplate $template - */ -class ArticlePresenter extends Nette\Application\UI\Presenter -{ -} - -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - public Model\Article $article; - public Nette\Security\User $user; - - // i inne zmienne -} -``` - -Obiekt `$this->template` w presenterze będzie teraz instancją klasy `ArticleTemplate`. PHP będzie więc przy zapisie sprawdzać zadeklarowane typy. - -Nette wybiera klasę szablonu automatycznie. Najpierw szuka klasy o nazwie `Template`, np. `ArticleEditTemplate` dla akcji `edit`, i dopiero gdy jej nie ma, sięga po `Template`. - -Adnotacja `@property-read` przeznaczona jest dla IDE i analizy statycznej, umożliwia uzupełnianie kodu, zobacz "PhpStorm i uzupełnianie kodu dla $this⁠-⁠>⁠template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template. - -[* phpstorm-completion.webp *] - -Uzupełniania kodu możesz używać również bezpośrednio w szablonach. Wystarczy zainstalować plugin Latte do PhpStorm i podać na początku szablonu nazwę klasy parametrów szablonu, więcej w rozdziale [Latte: system typów |latte:type-system]: - -```latte -{templateType App\Presentation\Article\ArticleTemplate} -... -``` - -To samo dotyczy komponentów. Wystarczy trzymać się konwencji nazewniczej i dla komponentu w rodzaju `FifteenControl` utworzyć klasę parametrów `FifteenTemplate`. - -Jeśli potrzebujesz użyć innej klasy parametrów, skorzystaj z metody `createTemplate()`: - -```php -public function renderDefault(): void -{ - $template = $this->createTemplate(SpecialTemplate::class); - $template->foo = 123; - // ... - $this->sendTemplate($template); -} -``` - -.{data-version:3.3.0} -Jeśli potrzebujesz wpłynąć na to, jak szablon jest finalizowany przed renderowaniem, na przykład aby dodać zmienne wspólne dla wszystkich akcji, możesz nadpisać w presenterze metodę `completeTemplate()`. Wywoływana jest tuż przed wyrenderowaniem szablonu: - -```php -protected function completeTemplate(Nette\Application\UI\Template $template): void -{ - parent::completeTemplate($template); - $template->siteName = 'Mój blog'; -} -``` - - -Tworzenie odnośników --------------------- - -W szablonie odnośniki do innych presenterów i akcji tworzy się tak: - -```latte -szczegóły produktu -``` - -Atrybut `n:href` jest bardzo poręczny dla tagów HTML ``. Jeśli chcemy wypisać odnośnik gdzie indziej, na przykład w tekście, użyjemy `{link}`: - -```latte -URL to: {link Home:default} -``` - -Więcej informacji znajdziesz w rozdziale [Tworzenie odnośników URL|creating-links]. - - -Własne filtry, tagi itd. ------------------------- - -System szablonów Latte można rozszerzać o własne filtry, funkcje, tagi i inne elementy. Dostępne są trzy podejścia, od szybkich rozwiązań doraźnych po wzorce architektoniczne dla całych aplikacji. - -**Doraźnie w metodach presentera** - -Najszybszym podejściem jest dodawanie filtrów albo funkcji bezpośrednio w kodzie presentera lub komponentu. W presenterach dobrze nadają się do tego metody `beforeRender()` albo `render()`: - -```php -protected function beforeRender(): void -{ - // dodanie filtra - $this->template->addFilter('money', fn($val) => '$' . number_format($val, 2)); - - // dodanie funkcji - $this->template->addFunction('isWeekend', fn($date) => $date->format('N') >= 6); -} -``` - -W szablonie: - -```latte -

    Cena: {$price|money}

    - -{if isWeekend($now)} ... {/if} -``` - -Przy bardziej złożonej logice możesz skonfigurować bezpośrednio obiekt `Latte\Engine`: - -```php -protected function beforeRender(): void -{ - $latte = $this->template->getLatte(); - $latte->setFeature(Latte\Feature::MigrationWarnings); -} -``` - -**Za pomocą atrybutów** - -Eleganckim podejściem jest zdefiniowanie filtrów i funkcji jako metod bezpośrednio w [klasie parametrów szablonu|#Szablony bezpieczne typowo] presentera albo komponentu, oznaczonych atrybutami: - -```php -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - #[Latte\Attributes\TemplateFilter] - public function money(float $val): string - { - return '$' . number_format($val, 2); - } - - #[Latte\Attributes\TemplateFunction] - public function isWeekend(DateTimeInterface $date): bool - { - return $date->format('N') >= 6; - } -} -``` - -Latte automatycznie odnajduje i rejestruje metody oznaczone tymi atrybutami. Nazwa filtra albo funkcji w szablonach odpowiada nazwie metody. Metody te muszą być publiczne. - -**Globalnie za pomocą rozszerzeń** - -Poprzednie podejścia nadają się do filtrów i funkcji potrzebnych tylko w konkretnych presenterach albo komponentach, a nie w całej aplikacji. Dla całej aplikacji najlepiej sprawdza się utworzenie [rozszerzenia |latte:extending-latte#Latte Extension]. Klasa ta centralizuje wszystkie rozszerzenia Latte w Twoim projekcie. Krótki przykład: - -```php -namespace App\Presentation\Accessory; - -final class LatteExtension extends Latte\Extension -{ - public function __construct( - private App\Model\Facade $facade, - private Nette\Security\User $user, - // ... - ) { - } - - public function getFilters(): array - { - return [ - 'timeAgoInWords' => $this->filterTimeAgoInWords(...), - 'money' => $this->filterMoney(...), - // ... - ]; - } - - public function getFunctions(): array - { - return [ - 'canEditArticle' => - fn($article) => $this->facade->canEditArticle($article, $this->user->getId()), - // ... - ]; - } - - private function filterTimeAgoInWords(DateTimeInterface $time): string - { - // ... - } - - // ... -} -``` - -Rozszerzenie zarejestrujesz przez [konfigurację |configuration#Szablony Latte]: - -```neon -latte: - extensions: - - App\Presentation\Accessory\LatteExtension -``` - -Rozszerzenia dają kilka korzyści: wsparcie dla wstrzykiwania zależności, dostęp do warstwy modelu Twojej aplikacji i centralne zarządzanie wszystkimi rozszerzeniami. Obsługują też własne tagi, providery, compiler passy i inne rzeczy. - - -Ustawienie wszystkich szablonów -------------------------------- - -Usługa `TemplateFactory`, która tworzy wszystkie szablony, oferuje publiczną tablicę callbacków `$onCreate`. Wywoływane są one przy każdym utworzeniu dowolnego szablonu, dzięki czemu z jednego miejsca ustawisz filtry, funkcje albo zmienne dla wszystkich szablonów w aplikacji. Każdy callback otrzymuje nowo utworzony szablon. Każ sobie [wstrzyknąć |dependency-injection:passing-dependencies] usługę `TemplateFactory` i zarejestruj callbacki, np. przy starcie aplikacji: - -```php -$templateFactory->onCreate[] = function (Nette\Bridges\ApplicationLatte\Template $template): void { - $template->addFilter('money', fn($val) => '$' . number_format($val, 2)); -}; -``` - - -Tłumaczenie ------------ - -Jeśli programujesz aplikację wielojęzyczną, prawdopodobnie będziesz potrzebować wypisywać w szablonie niektóre teksty w różnych językach. Nette Framework definiuje w tym celu interfejs tłumaczenia [api:Nette\Localization\Translator], który ma jedną metodę `translate()`. Przyjmuje ona komunikat `$message`, którym zwykle jest string, oraz dowolne inne parametry. Zadaniem jest zwrócenie przetłumaczonego stringa. Nette nie ma domyślnej implementacji; możesz wybrać z kilku gotowych rozwiązań dostępnych na [Componette |https://componette.org/search/localization] według swoich potrzeb. Ich dokumentacja wyjaśnia, jak skonfigurować translator. - -Szablonom można ustawić translator, który [otrzymamy wstrzyknięty |dependency-injection:passing-dependencies], metodą `setTranslator()`: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator); -} -``` - -Alternatywnie translator można ustawić przez [konfigurację |configuration#Szablony Latte]: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -Translatora można potem używać na przykład jako filtra `|translate`, wraz z dodatkowymi parametrami przekazywanymi do metody `translate()` (zobacz `foo, bar`): - -```latte -
    {='Koszyk'|translate} -{$item|translate} -{$item|translate, foo, bar} -``` - -Albo jako tagu z podkreśleniem: - -```latte -{_'Koszyk'} -{_$item} -{_$item, foo, bar} -``` - -Do przetłumaczenia fragmentu szablonu służy tag parzysty `{translate}` (od Latte 2.11, wcześniej używano tagu `{_}`): - -```latte -{translate}Zamów{/translate} -{translate foo, bar}Zamów{/translate} -``` - -Translator wywoływany jest normalnie w czasie działania, przy renderowaniu szablonu. Latte w wersji 3 potrafi jednak przetłumaczyć wszystkie teksty statyczne już podczas kompilacji szablonu. Oszczędza to wydajność, bo każdy string tłumaczony jest tylko raz, a powstałe tłumaczenie zapisywane jest do skompilowanej postaci. W katalogu cache powstaje wtedy kilka skompilowanych wersji szablonu, po jednej na język. Wystarczy do tego podać język jako drugi parametr: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator, $lang); -} -``` - -Przez tekst statyczny rozumiemy na przykład `{_'hello'}` albo `{translate}hello{/translate}`. Teksty niestatyczne, jak `{_$foo}`, nadal będą tłumaczone w czasie działania. diff --git a/application/pl/upgrading.texy b/application/pl/upgrading.texy deleted file mode 100644 index e7ea4851fa..0000000000 --- a/application/pl/upgrading.texy +++ /dev/null @@ -1,47 +0,0 @@ -Aktualizacja -************ - - -Aktualizacja do wersji 3.0 -========================== - -Nette 3.0 dodaje deklaracje typów do parametrów metod i wartości zwracanych. Jeśli nadpisujesz taką metodę w klasie dziedziczącej po Nette (na przykład w presenterze albo komponencie), musisz dodać te same deklaracje typów, w przeciwnym razie PHP zgłosi błąd "Declaration must be compatible". - -Interfejs `Nette\Application\IRouter` uległ zmianie. Metoda `match()` zwraca teraz tablicę parametrów zamiast obiektu `Nette\Application\Request`, a `constructUrl()` taką tablicę przyjmuje. - -Nette sprawdza teraz, czy każdy sygnał wysyłany jest z tego samego pochodzenia (czyli z tej samej domeny i subdomeny). Ta polityka same-origin to kluczowy mechanizm bezpieczeństwa, który pomaga ograniczyć możliwe wektory ataku. Jeśli chcesz dopuścić inne pochodzenie, dodaj do metody obsługującej adnotację `@crossOrigin`: - -```php -/** - * @crossOrigin - */ -public function handleXy(): void -{ -} -``` - -Dotyczy to również wysyłania formularzy. Jeśli chcesz dopuścić wysyłanie z innego pochodzenia, zrób to tak: - -```php -$form = new Nette\Application\UI\Form; -$form->allowCrossOrigin(); -``` - -Konstruktor `Nette\ComponentModel\Component` nie był używany od lat i w wersji 3.0 został usunięty. To złamanie zgodności wstecznej: jeśli w komponencie albo presenterze dziedziczącym po `Nette\Application\UI\Presenter` wywołujesz konstruktor rodzica, musisz to wywołanie usunąć. - - -Aktualizacja do wersji 2.4 -========================== - -- `Route` i `SimpleRouter` generują teraz ten sam schemat HTTP/HTTPS, którym otwarto stronę. Trasę wymagającą konkretnego protokołu można zdefiniować wraz ze schematem, np. `Route('http://domain.cz/')`. -- Dla parametrów metod render/action typu bool (czyli z wartością domyślną true albo false) oraz dla parametrów trwałych rozróżniane są teraz `false` i `null`. Jeśli parametru nie ma w URL, jego wartością jest teraz `null` (wcześniej `false`). -- Klasa zwracana przez `Presenter::getReflection()` nie jest już potomkiem `Nette\Reflection\ClassType`, a `getReflection()->getMethod()` nie jest już potomkiem `Nette\Reflection\Method`. -- Flaga `SECURED` i `Route::$defaultFlags` są przestarzałe. - - -Aktualizacja do wersji 2.3 -========================== - -- trasy i nazwy presenterów **rozróżniają wielkość liter**. Nette ostrzega, gdy użyjesz błędnej wielkości liter w nazwie presentera; ze względów wydajnościowych maska Route nie jest sprawdzana, więc zweryfikuj ją ręcznie. -- `Route::addStyle()` i `Route::setStyleProperty()` są przestarzałe i wywołują teraz `E_USER_DEPRECATED`. -- rozszerzenie szablonów `.phtml` i stara składnia odnośników nie są już obsługiwane. diff --git a/application/ru/@home.texy b/application/ru/@home.texy deleted file mode 100644 index 701ddd7ee1..0000000000 --- a/application/ru/@home.texy +++ /dev/null @@ -1,85 +0,0 @@ -Nette Application -***************** - -.[perex] -Nette Application - ядро фреймворка Nette, дающее мощные инструменты для создания современных веб-приложений. Оно предлагает ряд исключительных возможностей, которые заметно упрощают разработку и повышают безопасность и поддерживаемость кода. - - -Установка ---------- - -Скачайте и установите библиотеку с помощью [Composer|best-practices:composer]: - -```shell -composer require nette/application -``` - - -Почему стоит выбрать Nette Application? ---------------------------------------- - -Nette всегда был первопроходцем в веб-технологиях. - -**Двунаправленный маршрутизатор:** в Nette есть продвинутая система маршрутизации, уникальная своей двунаправленностью: она не только преобразует URL в действия приложения, но и умеет порождать URL в обратную сторону. Это значит: -- Вы можете в любой момент изменить структуру URL всего приложения, не трогая шаблоны -- URL автоматически канонизируются, что улучшает SEO -- Маршрутизация задаётся в одном месте, а не разбросана по аннотациям - -**Компоненты и сигналы:** встроенная система компонентов, вдохновлённая Delphi и React.js, уникальна среди PHP-фреймворков: -- Позволяет создавать переиспользуемые элементы интерфейса -- Поддерживает иерархическую композицию компонентов -- Предлагает изящную обработку AJAX-запросов через сигналы -- Богатая библиотека готовых компонентов на [Componette](https://componette.org) - -**AJAX и сниппеты:** Nette представил революционный способ работы с AJAX ещё в 2009 году, задолго до похожих решений вроде Hotwire для Ruby on Rails или Symfony UX Turbo: -- Сниппеты позволяют обновлять только части страницы, не требуя писать JavaScript -- Автоматическая интеграция с системой компонентов -- Умная инвалидация участков страницы -- Минимальный объём передаваемых данных - -**Интуитивные шаблоны [Latte|latte:]:** самая безопасная система шаблонов для PHP с продвинутыми возможностями: -- Автоматическая защита от XSS с контекстно-зависимым экранированием -- Расширяемость собственными фильтрами, функциями и тегами -- Наследование шаблонов и сниппеты для AJAX -- Отличная поддержка PHP 8.x с системой типов - -**Dependency Injection:** Nette полностью использует Dependency Injection: -- Автоматическая передача зависимостей (autowiring) -- Настройка в наглядном формате NEON -- Поддержка фабрик компонентов - - -Главные преимущества --------------------- - -- **Безопасность**: автоматическая защита от [уязвимостей|nette:vulnerability-protection] вроде XSS, CSRF и других -- **Продуктивность**: меньше писать, больше возможностей благодаря продуманной архитектуре -- **Отладка**: [отладчик Tracy|tracy:] с панелью маршрутизации -- **Производительность**: умный кеш, ленивая загрузка компонентов -- **Гибкость**: лёгкое изменение URL даже после завершения приложения -- **Компоненты**: уникальная система переиспользуемых элементов интерфейса -- **Современность**: полная поддержка PHP 8.3+ и системы типов - - -Первые шаги ------------ - -1. [Как работают приложения? |how-it-works] - понимание базовой архитектуры -2. [Презентеры |presenters] - работа с презентерами и действиями -3. [Шаблоны |templates] - создание шаблонов на Latte -4. [Маршрутизация |routing] - настройка URL-адресов -5. [Интерактивные компоненты |components] - использование системы компонентов - - -Совместимость с PHP -------------------- - -| версия | совместимость с PHP -|-----------------------|------------------- -| Nette Application 3.3 | PHP 8.3 - 8.5 -| Nette Application 3.2 | PHP 8.1 - 8.5 -| 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/ru/@left-menu.texy b/application/ru/@left-menu.texy deleted file mode 100644 index dadb55bbc3..0000000000 --- a/application/ru/@left-menu.texy +++ /dev/null @@ -1,24 +0,0 @@ -Nette Application -***************** -- [Обзор |@home] -- [Как работают приложения? |how-it-works] -- [Bootstrapping|bootstrapping] -- [Презентеры|presenters] -- [Шаблоны|templates] -- [Структура каталогов |directory-structure] -- [Маршрутизация|routing] -- [Создание URL-ссылок |creating-links] -- [Интерактивные компоненты |components] -- [AJAX и сниппеты |ajax] -- [Multiplier |multiplier] -- [Конфигурация|configuration] -- [Обновление|upgrading] - - -Дополнительные материалы -************************ -- [Зачем использовать Nette?|www:10-reasons-why-nette] -- [Установка |nette:installation] -- [Создайте своё первое приложение! |quickstart:] -- [Лучшие практики |best-practices:] -- [Устранение неполадок |nette:troubleshooting] diff --git a/application/ru/@meta.texy b/application/ru/@meta.texy deleted file mode 100644 index 7f329adfce..0000000000 --- a/application/ru/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документация Nette}} diff --git a/application/ru/ajax.texy b/application/ru/ajax.texy deleted file mode 100644 index 9aedad772c..0000000000 --- a/application/ru/ajax.texy +++ /dev/null @@ -1,265 +0,0 @@ -AJAX и сниппеты -*************** - -
    - -В эпоху современных веб-приложений, где функциональность часто распределена между сервером и браузером, AJAX служит незаменимым связующим звеном. Какие возможности предлагает в этой области Nette Framework? -- отправку частей шаблона, называемых сниппетами -- передачу переменных между PHP и JavaScript -- инструменты для отладки AJAX-запросов - -
    - - -AJAX-запрос -=========== - -AJAX-запрос принципиально не отличается от обычного HTTP-запроса. Вызывается презентер с определёнными параметрами. Как ответить на запрос, решает сам презентер: он может вернуть данные в формате JSON, отправить часть HTML-кода, XML-документ и так далее. - -На стороне браузера мы инициируем AJAX-запрос функцией `fetch()`: - -```js -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -.then(response => response.json()) -.then(payload => { - // обрабатываем ответ -}); -``` - -На стороне сервера AJAX-запрос распознаётся методом `$httpRequest->isAjax()` сервиса, [упаковывающего HTTP-запрос |http:request]. Для определения он использует HTTP-заголовок `X-Requested-With`, поэтому принципиально важно его отправлять. Внутри презентера можно использовать метод `$this->isAjax()`. - -Если вы хотите отправить данные в формате JSON, используйте метод [`sendJson()` |presenters#Отправка ответа]. Этот метод также завершает работу презентера. - -```php -public function actionExport(): void -{ - $this->sendJson($this->model->getData()); -} -``` - -Если вы собираетесь ответить особым шаблоном, предназначенным для AJAX, это делается так: - -```php -public function handleClick($param): void -{ - if ($this->isAjax()) { - $this->template->setFile('path/to/ajax.latte'); - } - // ... -} -``` - - -Сниппеты -======== - -Самый мощный инструмент, который Nette предлагает для связи сервера с клиентом, - это сниппеты. С их помощью вы можете превратить обычное приложение в AJAX-приложение минимальными усилиями и несколькими строками кода. Пример Fifteen показывает, как всё это работает, а его код можно найти на [GitHub |https://github.com/nette-examples/fifteen]. - -Сниппеты позволяют обновлять только части страницы вместо перезагрузки всей страницы. Это не только быстрее и эффективнее, но и удобнее для пользователя. Сниппеты могут напомнить вам Hotwire для Ruby on Rails или Symfony UX Turbo. Любопытно, что Nette представил сниппеты на 14 лет раньше. - -Как работают сниппеты? При первой загрузке страницы (не-AJAX-запрос) загружается вся страница, включая все сниппеты. Когда пользователь взаимодействует со страницей (например, нажимает кнопку, отправляет форму и так далее), вместо перезагрузки всей страницы инициируется AJAX-запрос. Код в презентере выполняет действие и решает, какие сниппеты нужно обновить. Nette отрисовывает эти сниппеты и отправляет их как payload в формате JSON, содержащий массив со сниппетами. Обрабатывающий код в браузере затем вставляет полученные сниппеты обратно в страницу. Таким образом передаётся только код изменившихся сниппетов, что экономит трафик и ускоряет загрузку по сравнению с передачей всего содержимого страницы. Если ни один сниппет не был инвалидирован через `redrawControl()`, Nette возвращает всю страницу даже для AJAX-запроса: сниппеты отправляются, только когда что-то инвалидировано. - - -Naja ----- - -Для работы со сниппетами на стороне браузера используется [библиотека Naja |https://naja.js.org]. [Установите её |https://naja.js.org/#/guide/01-install-setup-naja] как пакет Node.js (для использования со сборщиками вроде Webpack, Rollup, Vite, Parcel и других): - -```shell -npm install naja -``` - -…или вставьте прямо в шаблон страницы: - -```latte - -``` - -Сначала библиотеку нужно [инициализировать |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization]: - -```js -naja.initialize(); -``` - -Чтобы превратить обычную ссылку (сигнал) или отправку формы в AJAX-запрос, достаточно пометить соответствующую ссылку, форму или кнопку классом `ajax`: - -```latte -Go - -
    - -
    - -или - -
    - -
    -``` - - -Перерисовка сниппетов ---------------------- - -Каждый объект класса [Control |components] (включая сам презентер) следит за тем, произошли ли изменения, требующие перерисовки. Для этого служит метод `redrawControl()`: - -```php -public function handleLogin(string $user): void -{ - // после входа нужно перерисовать соответствующую часть - $this->redrawControl(); - // ... -} -``` - -Nette позволяет ещё точнее управлять тем, что нужно перерисовать. Метод может принять аргументом имя сниппета. Благодаря этому можно инвалидировать (то есть заставить перерисоваться) на уровне частей шаблона. Если инвалидирован весь компонент, будет перерисован и каждый сниппет внутри него: - -```php -// инвалидирует сниппет 'header' -$this->redrawControl('header'); -``` - -Можно и отменить ожидающую инвалидацию вторым параметром `$redraw`: вызов `$this->redrawControl('header', redraw: false)` помечает сниппет как не нуждающийся в перерисовке. Полная сигнатура - `redrawControl(?string $snippet = null, bool $redraw = true)`. - - -Сниппеты в Latte ----------------- - -Использовать сниппеты в Latte исключительно просто. Чтобы объявить часть шаблона сниппетом, достаточно обернуть её тегами `{snippet}` и `{/snippet}`: - -```latte -{snippet header} -

    Hello ...

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

    Hello ...

    -
    -``` - - -Области сниппетов ------------------ - -Имена сниппетов могут быть и выражениями: - -```latte -{foreach $items as $id => $item} -
  • {$item}
  • -{/foreach} -``` - -Сам по себе это неработающий промежуточный шаг: отрисованный вне статического `{snippet}` или `{snippetArea}`, динамический сниппет выдаёт `E_USER_WARNING` с сообщением *Dynamic snippets are allowed only inside static snippet/snippetArea.* Ниже мы это исправим. - -Так создаётся несколько сниппетов вроде `item-0`, `item-1` и так далее. Если бы мы напрямую инвалидировали динамический сниппет (например, `item-1`), не перерисовалось бы ничего. Причина в том, что сниппеты действительно работают как выдержки и напрямую отрисовываются только они сами. Однако в шаблоне технически нет сниппета с именем `item-1`. Он возникает только тогда, когда выполняется код вокруг сниппета, то есть цикл foreach. Поэтому мы помечаем ту часть шаблона, которая должна выполниться, тегом `{snippetArea}`: - -```latte -
      - {foreach $items as $id => $item} -
    • {$item}
    • - {/foreach} -
    -``` - -И запрашиваем перерисовку как отдельного сниппета, так и всей родительской области: - -```php -$this->redrawControl('itemsContainer'); -$this->redrawControl('item-1'); -``` - -Одновременно стоит позаботиться о том, чтобы в массиве `$items` были только те элементы, которые нужно перерисовать. - -Если мы подключаем в главный шаблон другой шаблон со сниппетами через тег `{include}`, подключение шаблона нужно снова обернуть в `snippetArea` и инвалидировать её вместе со сниппетом: - -```latte -{snippetArea include} - {include 'included.latte'} -{/snippetArea} -``` - -```latte -{* included.latte *} -{snippet item} - ... -{/snippet} -``` - -```php -$this->redrawControl('include'); -$this->redrawControl('item'); -``` - - -Сниппеты в компонентах ----------------------- - -Сниппеты можно создавать и внутри [компонентов|components], и Nette будет автоматически их перерисовывать. Однако есть ограничение: для перерисовки сниппетов Nette вызывает метод `render()` без параметров. Поэтому передача параметров в шаблоне работать не будет: - -```latte -OK -{control productGrid} - -работать не будет: -{control productGrid, $arg, $arg} -{control productGrid:paginator} -``` - - -Отправка собственных данных ---------------------------- - -Вместе со сниппетами вы можете отправить клиенту любые дополнительные данные. Достаточно записать их в объект `payload`: - -```php -public function actionDelete(int $id): void -{ - // ... - if ($this->isAjax()) { - $this->payload->message = 'Success'; - } -} -``` - - -Перенаправление ---------------- - -Во время AJAX-запроса методы `redirect()` и `redirectUrl()` не отправляют HTTP-перенаправление. Вместо этого они записывают целевой URL в payload (объект данных, отправляемый в AJAX-ответе), а именно в его свойство `payload.redirect`, и отправляют его; само перенаправление затем выполняет клиентская библиотека (Naja). - - -Передача параметров -=================== - -Отправляя параметры компоненту через AJAX-запрос, будь то параметры сигнала или постоянные параметры, мы должны указать в запросе их глобальное имя, включающее имя компонента. Полное имя параметра возвращает метод `getParameterId()`. - -```js -let url = new URL({link //foo!}); -url.searchParams.set({$control->getParameterId('bar')}, bar); - -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -``` - -И метод handle с соответствующими параметрами в компоненте: - -```php -public function handleFoo(int $bar): void -{ -} -``` - - -Дополнительные материалы -======================== - -- [Динамические сниппеты |best-practices:dynamic-snippets] diff --git a/application/ru/bootstrapping.texy b/application/ru/bootstrapping.texy deleted file mode 100644 index 3842b68a2b..0000000000 --- a/application/ru/bootstrapping.texy +++ /dev/null @@ -1,312 +0,0 @@ -Bootstrapping -************* - -
    - -Bootstrapping - это процесс инициализации окружения приложения, создания контейнера внедрения зависимостей (DI) и запуска приложения. Мы обсудим: - -- как класс Bootstrap инициализирует окружение -- как приложения настраиваются файлами NEON -- как различать производственный режим и режим разработки -- как создать и настроить DI-контейнер - -
    - - -Приложения, будь то веб-приложения или скрипты, запускаемые из командной строки, начинают своё выполнение с той или иной инициализации окружения. В былые времена за это отвечал файл с именем вроде `include.inc.php`, подключаемый начальным файлом. В современных приложениях Nette его заменил класс `Bootstrap`, который как часть приложения находится в файле `app/Bootstrap.php`. Он может выглядеть, например, так: - -```php -namespace App; - -use Nette; -use Nette\Bootstrap\Configurator; - -class Bootstrap -{ - private Configurator $configurator; - private string $rootDir; - - public function __construct() - { - $this->rootDir = dirname(__DIR__); - // Configurator отвечает за настройку окружения приложения и сервисов. - $this->configurator = new Configurator; - // Задаём каталог для временных файлов, порождаемых Nette (например, скомпилированных шаблонов) - $this->configurator->setTempDirectory($this->rootDir . '/temp'); - } - - public function bootWebApplication(): Nette\DI\Container - { - $this->initializeEnvironment(); - $this->setupContainer(); - return $this->configurator->createContainer(); - } - - private function initializeEnvironment(): void - { - // Nette умён, и режим разработки включается автоматически, - // либо вы можете включить его для конкретного IP-адреса, раскомментировав следующую строку: - // $this->configurator->setDebugMode('secret@23.75.345.200'); - - // Включает Tracy - лучший "швейцарский нож" для отладки. - $this->configurator->enableTracy($this->rootDir . '/log'); - - // RobotLoader: автоматически загружает все классы в выбранном каталоге - $this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); - } - - private function setupContainer(): void - { - // Загружаем конфигурационные файлы - $this->configurator->addConfig($this->rootDir . '/config/common.neon'); - } -} -``` - - -index.php -========= - -У веб-приложений начальным файлом служит `index.php`, лежащий в [публичном каталоге |directory-structure#Публичный каталог www/] `www/`. Он поручает классу Bootstrap инициализировать окружение и создать DI-контейнер. Затем он получает из контейнера сервис `Application`, который и запускает веб-приложение: - -```php -$bootstrap = new App\Bootstrap; -// Инициализируем окружение и создаём DI-контейнер -$container = $bootstrap->bootWebApplication(); -// DI-контейнер создаёт объект Nette\Application\Application -$application = $container->getByType(Nette\Application\Application::class); -// Запускаем приложение Nette и обрабатываем входящий запрос -$application->run(); -``` - -.[note] -Объект `$application` в ходе обработки запроса испускает [события |nette:glossary#События]: `onStartup`, `onRequest`, `onPresenter`, `onResponse`, `onShutdown` и `onError` (при необработанном исключении). Вы можете привязать к ним обработчики, что удобно для ведения лога или мониторинга всего приложения. - -Как видите, настроить окружение и создать контейнер внедрения зависимостей (DI) помогает класс [api:Nette\Bootstrap\Configurator]. Сейчас мы познакомим вас с ним подробнее. - - -Режим разработки и производственный режим -========================================= - -Nette ведёт себя по-разному в зависимости от того, работает он на сервере разработки или на производственном: - -🛠️ Режим разработки: - - Показывает панель отладки Tracy с полезными сведениями (SQL-запросы, время выполнения, использованная память) - - При ошибке показывает подробную страницу ошибки с вызовами функций и содержимым переменных - - Автоматически обновляет кеш при изменении шаблонов Latte, конфигурационных файлов и прочего - - -🚀 Производственный режим: - - Не показывает никаких отладочных сведений, все ошибки записываются в лог - - При ошибке показывает ErrorPresenter или общую страницу "Server Error" - - Кеш никогда не обновляется автоматически! - - Оптимизирован ради скорости и безопасности - - -Режим выбирается автоопределением, поэтому обычно ничего настраивать и переключать вручную не нужно: - -- режим разработки: на localhost (IP-адрес `127.0.0.1` или `::1`), если нет прокси (то есть его HTTP-заголовок не обнаружен) -- производственный режим: везде остальном - -Если мы хотим включить режим разработки и в других случаях, например для программистов, заходящих с определённого IP-адреса, мы используем `setDebugMode()`: - -```php -$this->configurator->setDebugMode('23.75.345.200'); // можно передать и массив IP-адресов -``` - -Мы настоятельно рекомендуем сочетать IP-адрес с cookie. Сохраните в cookie `nette-debug` секретный токен, например `secret1234`, и тем самым включите режим разработки для программистов, заходящих с определённого IP-адреса и имеющих в cookie этот токен: - -```php -$this->configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Мы можем и полностью отключить режим разработки, даже на localhost: - -```php -$this->configurator->setDebugMode(false); -``` - -Учтите, что значение `true` принудительно включает режим разработки, чего на производственном сервере быть **никогда** не должно. - -Автоопределением внутренне занимается статический метод `Configurator::detectDebugMode()`, который вы можете вызвать и сами, например чтобы определить режим разработки вне конфигуратора. Он принимает необязательный белый список IP-адресов или имён компьютеров и возвращает, должен ли текущий запрос выполняться в режиме разработки: - -```php -$debug = Nette\Bootstrap\Configurator::detectDebugMode('23.75.345.200'); -``` - - -Инструмент отладки Tracy -======================== - -Ради удобной отладки мы включим прекрасный инструмент [Tracy |tracy:]. В режиме разработки он наглядно показывает ошибки, а в производственном записывает их в указанный каталог: - -```php -$this->configurator->enableTracy($this->rootDir . '/log'); -``` - - -Временные файлы -=============== - -Nette использует кеш для DI-контейнера, RobotLoader, шаблонов и прочего. Поэтому нужно задать путь к каталогу, где будет храниться кеш: - -```php -$this->configurator->setTempDirectory($this->rootDir . '/temp'); -``` - -В Linux или macOS задайте каталогам `log/` и `temp/` [права на запись |nette:troubleshooting#Задание прав на каталоги]. - - -RobotLoader -=========== - -Обычно мы хотим автоматически загружать классы с помощью [RobotLoader |robot-loader:], поэтому нам нужно его запустить и дать ему загружать классы из каталога, где лежит `Bootstrap.php` (то есть `__DIR__`), и из всех его подкаталогов: - -```php -$this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); -``` - -Альтернативный подход - загружать классы исключительно через [Composer |best-practices:composer] по PSR-4. - - -Часовой пояс -============ - -Часовой пояс по умолчанию можно задать через конфигуратор. - -```php -$this->configurator->setTimeZone('Europe/Prague'); -``` - - -Конфигурация DI-контейнера -========================== - -Частью процесса запуска является создание DI-контейнера, то есть фабрики объектов, которая служит сердцем всего приложения. На деле это PHP-класс, порождённый Nette и сохранённый в каталоге кеша. Фабрика создаёт ключевые объекты приложения, а конфигурационными файлами мы указываем ей, как их создавать и настраивать, и тем самым влияем на поведение всего приложения. - -Конфигурационные файлы обычно пишутся в [формате NEON |neon:format]. В отдельной главе вы можете прочитать, [что можно настраивать |nette:configuring]. - -.[tip] -В режиме разработки контейнер автоматически обновляется при изменении кода или конфигурационных файлов. В производственном режиме он порождается только один раз, а изменения не проверяются ради максимальной производительности. - -Метод `createContainer()` собирает контейнер и возвращает его экземпляр, а метод `loadContainer()` возвращает только имя порождённого класса контейнера, который вы затем можете создать сами. Это полезно в продвинутых сценариях. - -Конфигурационные файлы загружаются методом `addConfig()`: - -```php -$this->configurator->addConfig($this->rootDir . '/config/common.neon'); -``` - -Если мы хотим добавить больше конфигурационных файлов, мы можем вызвать функцию `addConfig()` несколько раз. - -```php -$configDir = $this->rootDir . '/config'; -$this->configurator->addConfig($configDir . '/common.neon'); -$this->configurator->addConfig($configDir . '/services.neon'); -if (PHP_SAPI === 'cli') { - $this->configurator->addConfig($configDir . '/cli.php'); -} -``` - -Имя `cli.php` - не опечатка: конфигурацию можно записать и в PHP-файле, который возвращает её массивом. - -Другие конфигурационные файлы мы можем добавить и в [секции `includes` |dependency-injection:configuration#Подключение файлов]. - -Если в конфигурационных файлах встречаются элементы с одинаковыми ключами, они будут перезаписаны или, в случае [массивов, объединены |dependency-injection:configuration#Слияние]. Файл, подключённый позже, имеет более высокий приоритет, чем предыдущий. Файл, в котором указана секция `includes`, имеет более высокий приоритет, чем подключённые в нём файлы. - - -Статические параметры ---------------------- - -Параметры, используемые в конфигурационных файлах, можно определить [в секции `parameters` |dependency-injection:configuration#Параметры], а также передать (или переопределить) методом `addStaticParameters()` (его более старый, ныне устаревший псевдоним - `addParameters()`). Важно, что разные значения параметров вызовут порождение дополнительных DI-контейнеров, то есть дополнительных классов. - -```php -$this->configurator->addStaticParameters([ - 'projectId' => 23, -]); -``` - -На параметр `projectId` можно сослаться в конфигурации обычной записью `%projectId%`. - - -Динамические параметры ----------------------- - -Мы можем добавить в контейнер и динамические параметры, разные значения которых, в отличие от статических, не вызовут порождения новых DI-контейнеров. - -```php -$this->configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Так мы легко добавим, например, переменные окружения, на которые затем можно сослаться в конфигурации записью `%env.variable%`. - -```php -$this->configurator->addDynamicParameters([ - 'env' => getenv(), -]); -``` - - -Параметры по умолчанию ----------------------- - -В конфигурационных файлах вы можете использовать эти параметры: - -- `%appDir%` - абсолютный путь к каталогу с файлом `Bootstrap.php` -- `%wwwDir%` - абсолютный путь к каталогу с начальным файлом `index.php` -- `%tempDir%` - абсолютный путь к каталогу временных файлов -- `%vendorDir%` - абсолютный путь к каталогу, куда Composer устанавливает библиотеки -- `%rootDir%` - абсолютный путь к корневому каталогу проекта -- `%baseUrl%` - абсолютный URL корневого каталога (динамический параметр, вычисляемый во время выполнения) -- `%debugMode%` - находится ли приложение в режиме отладки -- `%consoleMode%` - пришёл ли запрос из командной строки - - -Импортированные сервисы ------------------------ - -Теперь копнём глубже. Хотя назначение DI-контейнера - создавать объекты, изредка может понадобиться вставить в контейнер уже существующий объект. Мы делаем это, определив сервис с флагом `imported: true`. - -```neon -services: - myservice: - type: App\Model\MyCustomService - imported: true -``` - -А в bootstrap вставляем объект в контейнер: - -```php -$this->configurator->addServices([ - 'myservice' => new App\Model\MyCustomService('foobar'), -]); -``` - - -Разные окружения -================ - -Смело меняйте класс `Bootstrap` под свои нужды. Вы можете добавить в метод `bootWebApplication()` параметры, чтобы различать веб-проекты. Или можно добавить другие методы, например `bootTestEnvironment()`, инициализирующий окружение для модульных тестов, `bootConsoleApplication()` для скриптов, вызываемых из командной строки, и так далее. - -```php -public function bootTestEnvironment(): Nette\DI\Container -{ - Tester\Environment::setup(); // инициализация Nette Tester - $this->setupContainer(); - return $this->configurator->createContainer(); -} - -public function bootConsoleApplication(): Nette\DI\Container -{ - $this->configurator->setDebugMode(false); - $this->initializeEnvironment(); - $this->setupContainer(); - return $this->configurator->createContainer(); -} -``` diff --git a/application/ru/components.texy b/application/ru/components.texy deleted file mode 100644 index 66e9d69bb7..0000000000 --- a/application/ru/components.texy +++ /dev/null @@ -1,504 +0,0 @@ -Интерактивные компоненты -************************ - -
    - -Компоненты - самостоятельные переиспользуемые объекты, которые мы встраиваем в страницы. Это могут быть формы, таблицы данных, опросы - в общем, всё, что имеет смысл использовать повторно. Мы покажем: - -- как использовать компоненты? -- как их писать? -- что такое сигналы? - -
    - -В Nette встроена система компонентов. Нечто похожее может быть знакомо ветеранам Delphi или ASP.NET Web Forms; React или Vue.js построены на чём-то отдалённо похожем. Однако в мире PHP-фреймворков это уникальная возможность. - -При этом компоненты принципиально меняют подход к разработке приложений. Вы можете собирать страницы из заранее подготовленных единиц. Нужна таблица данных в вашей администраторской части? Найдите её на [Componette |https://componette.org/search/component], в хранилище дополнений с открытым кодом (не только компонентов) для Nette, и просто вставьте в презентер. - -Вы можете встроить в презентер сколько угодно компонентов. А в некоторые компоненты можно встраивать другие компоненты. Так возникает дерево компонентов, корнем которого служит презентер. - - -Фабричные методы -================ - -Как компоненты вставляются в презентер и затем используются? Обычно через фабричные методы. - -Фабрика компонента - изящный способ создавать компоненты только тогда, когда они действительно нужны (лениво, по требованию). Вся магия заключается в реализации метода с именем `createComponent()`, где `` - имя создаваемого компонента; этот метод создаёт и возвращает компонент. - -```php .{file:DefaultPresenter.php} -class DefaultPresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentPoll(): PollControl - { - $poll = new PollControl; - $poll->items = $this->items; - return $poll; - } -} -``` - -Поскольку все компоненты создаются в отдельных методах, код становится нагляднее. - -.[note] -Имена компонентов всегда начинаются со строчной буквы, хотя в имени метода они пишутся с заглавной. - -Мы никогда не вызываем фабрики напрямую: они вызываются автоматически при первом использовании компонента. Благодаря этому компонент создаётся в нужный момент и только если он действительно нужен. Если мы компонент не используем (например, при AJAX-запросе, когда передаётся лишь часть страницы, или при кешировании шаблона), он вообще не создастся, что сэкономит производительность сервера. - -```php .{file:DefaultPresenter.php} -// обращаемся к компоненту, и если это в первый раз, -// вызывается createComponentPoll(), который его создаёт -$poll = $this->getComponent('poll'); -// альтернативная запись: $poll = $this['poll']; -``` - -В шаблоне компонент можно отрисовать тегом [{control} |#Отрисовка]. Поэтому передавать компоненты в шаблон вручную не нужно. - -```latte -

    Please Vote

    - -{control poll} -``` - -.[tip] -Для динамического создания переменного числа компонентов используйте [Multiplier |multiplier]. - -Фабричные методы `createComponent()` работают не только в презентерах. Тем же способом вы можете вложить компонент в другой компонент и собрать их в дерево - это удобно, например, для отдельно отрисовываемой формы внутри компонента. - - -Голливудский стиль -================== - -Компоненты обычно используют бодрый приём, который мы любим называть голливудским стилем. Вы наверняка знаете штамп, который часто слышат участники кинопроб: "Не звоните нам, мы позвоним вам". Именно об этом и речь. - -В Nette вместо того, чтобы постоянно задавать вопросы ("была ли форма отправлена?", "была ли она корректна?", "нажал ли пользователь эту кнопку?"), вы говорите фреймворку: "когда произойдёт вот это, вызови этот метод", и оставляете дальнейшую работу ему. Если вы программируете на JavaScript, этот стиль вам близко знаком. Вы пишете функции, которые вызываются, когда происходит определённое событие. И язык передаёт им нужные параметры. - -Это полностью меняет взгляд на написание приложений. Чем больше задач вы можете оставить фреймворку, тем меньше у вас работы. И тем меньше вы можете упустить. - - -Написание компонента -==================== - -Под словом "компонент" мы обычно понимаем потомка класса [api:Nette\Application\UI\Control]. (Точнее было бы говорить "controls", но в некоторых языках это слово имеет другое значение, и "компоненты" прижилось лучше.) Сам презентер [api:Nette\Application\UI\Presenter] тоже является потомком класса `Control`. - -```php .{file:PollControl.php} -use Nette\Application\UI\Control; - -class PollControl extends Control -{ -} -``` - - -Отрисовка -========= - -Мы уже знаем, что для отрисовки компонента служит тег `{control componentName}`. На деле он вызывает метод `render()` компонента, в котором мы заботимся об отрисовке. У нас есть, как и в презентере, [шаблон Latte|templates] в переменной `$this->template`, куда мы передаём параметры. В отличие от презентера, мы обязаны указать файл шаблона и заставить его отрисоваться: - -```php .{file:PollControl.php} -public function render(): void -{ - // вставляем в шаблон какие-то параметры - $this->template->param = $value; - // и отрисовываем его - $this->template->render(__DIR__ . '/poll.latte'); -} -``` - -Тег `{control}` позволяет передать параметры в метод `render()`: - -```latte -{control poll, $id, $message} -``` - -```php .{file:PollControl.php} -public function render(int $id, string $message): void -{ - // ... -} -``` - -Иногда компонент может состоять из нескольких частей, которые мы хотим отрисовывать по отдельности. Для каждой из них мы создаём собственный метод отрисовки, здесь в примере `renderPaginator()`: - -```php .{file:PollControl.php} -public function renderPaginator(): void -{ - // ... -} -``` - -А в шаблоне вызываем его так: - -```latte -{control poll:paginator} -``` - -Для лучшего понимания полезно знать, как этот тег превращается в PHP-код. - -```latte -{control poll} -{control poll:paginator, 123, 'hello'} -``` - -превращается в: - -```php -$control->getComponent('poll')->render(); -$control->getComponent('poll')->renderPaginator(123, 'hello'); -``` - -Метод `getComponent()` возвращает компонент `poll`, а у этого компонента вызывается метод `render()` или `renderPaginator()`, если в теге после двоеточия указан другой метод отрисовки. - -.[caution] -Осторожно: если в параметрах вне квадратных скобок появляется **`=>`**, все параметры будут упакованы в массив и переданы как первый аргумент: - -```latte -{control poll, id: 123, message: 'hello'} -``` - -превращается в: - -```php -$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']); -``` - -Отрисовка подкомпонента: - -```latte -{control cartControl-someForm} -``` - -превращается в: - -```php -$control->getComponent("cartControl-someForm")->render(); -``` - -Компоненты, как и презентеры, автоматически передают в шаблоны несколько полезных переменных: - -- `$basePath` - абсолютный путь URL к корневому каталогу (например, `/eshop`) -- `$baseUrl` - абсолютный URL корневого каталога (например, `http://localhost/eshop`) -- `$user` - объект, [представляющий пользователя |security:authentication] -- `$presenter` - текущий презентер -- `$control` - текущий компонент -- `$flashes` - массив [сообщений |#Flash-сообщения], отправленных функцией `flashMessage()` - - -Сигнал -====== - -Мы уже знаем, что навигация в приложении Nette состоит из ссылок или перенаправлений на пары `Презентер:действие`. Но что, если мы хотим просто выполнить действие на **текущей странице**? Например, изменить сортировку столбцов таблицы; удалить элемент; переключить светлый или тёмный режим; отправить форму; проголосовать в опросе и так далее. - -Такой вид запроса называется сигналом. И так же как действия вызывают методы `action()` или `render()`, сигналы вызывают методы `handle()`. Понятие действия (или представления) относится исключительно к презентерам, а сигналы касаются всех компонентов. А значит, и презентеров, потому что `UI\Presenter` - потомок `UI\Control`. - -```php -public function handleClick(int $x, int $y): void -{ - // ... обработка сигнала ... -} -``` - -Ссылка, вызывающая сигнал, создаётся обычным способом, то есть в шаблоне атрибутом `n:href` или тегом `{link}`, а в коде методом `link()`. Подробнее в главе [Создание URL-ссылок |creating-links#Ссылки на сигнал]. - -```latte -click here -``` - -Сигнал всегда вызывается на текущем презентере и действии; вызвать его на другом презентере или действии нельзя. - -Таким образом, сигнал вызывает перезагрузку страницы точно так же, как исходный запрос, но дополнительно вызывает метод обработки сигнала с нужными параметрами. Если метода не существует, выбрасывается исключение [api:Nette\Application\UI\BadSignalException], которое показывается пользователю как страница ошибки 403 Forbidden. - - -Сниппеты и AJAX -=============== - -Сигналы могут немного напомнить вам AJAX: обработчики, вызываемые на текущей странице. И вы правы, сигналы действительно часто вызываются через AJAX, и затем в браузер передаются только изменившиеся части страницы. Они называются сниппетами. Подробнее на [странице, посвящённой AJAX |ajax]. - - -Flash-сообщения -=============== - -У компонента есть собственное хранилище flash-сообщений, независимое от презентера. Это сообщения, которые, например, извещают о результате операции. Важная особенность flash-сообщений в том, что они доступны в шаблоне и после перенаправления. Даже после показа они остаются активными ещё 30 секунд: например, на случай, если пользователь обновит страницу из-за ошибки передачи, сообщение не исчезнет сразу. - -Отправкой занимается метод [flashMessage |api:Nette\Application\UI\Control::flashMessage()]. Первый параметр - текст сообщения (`string`, `Stringable`) или объект `stdClass`, представляющий сообщение. Необязательный второй параметр - его тип (error, warning, info и так далее). Метод `flashMessage()` возвращает экземпляр flash-сообщения как объект `stdClass`, в который можно добавить дополнительные сведения. - -```php -$this->flashMessage('Item was deleted.'); -$this->redirect(/* ... */); // и перенаправляем -``` - -Эти сообщения доступны шаблону в переменной `$flashes` как объекты `stdClass`, содержащие свойства `message` (текст сообщения), `type` (тип сообщения) и, возможно, упомянутые пользовательские сведения. Отрисовываем мы их, например, так: - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -Перенаправление после обработки сигнала -======================================= - -За обработкой сигнала компонента часто следует перенаправление. Это похоже на формы: после их отправки мы тоже перенаправляем, чтобы данные не отправились повторно при обновлении страницы в браузере. - -```php -$this->redirect('this'); // перенаправляет на текущие презентер и действие -``` - -Поскольку компонент - переиспользуемый элемент, который обычно не должен быть напрямую связан с конкретными презентерами, методы `redirect()` и `link()` автоматически истолковывают параметр как сигнал компонента: - -```php -$this->redirect('click'); // перенаправляет на сигнал 'click' того же компонента -``` - -Если вам нужно перенаправить на другой презентер или действие, это можно сделать через презентер: - -```php -$this->getPresenter()->redirect('Product:show'); // перенаправляет на другой презентер или действие -``` - - -Постоянные параметры -==================== - -Постоянные параметры служат для сохранения состояния компонентов между разными запросами. Их значение остаётся тем же и после щелчка по ссылке. В отличие от данных сессии, они передаются в URL. И происходит это полностью автоматически, включая ссылки, созданные в других компонентах на той же странице. - -Например, у вас есть компонент постраничного вывода содержимого. Таких компонентов на странице может быть несколько. И мы хотим, чтобы все компоненты после щелчка по ссылке остались на своей текущей странице. Поэтому мы делаем номер страницы (`page`) постоянным параметром. - -Создать постоянный параметр в Nette исключительно просто. Достаточно создать публичное свойство и пометить его атрибутом (раньше использовалось `/** @persistent */`): - -```php -use Nette\Application\Attributes\Persistent; // эта строка важна - -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; // должно быть public -} -``` - -Мы рекомендуем указывать тип данных свойства (например, `int`), а также можно задать значение по умолчанию. Значения параметров можно [проверять |#Проверка постоянных параметров]. - -При создании ссылки значение постоянного параметра можно изменить: - -```latte -next -``` - -Или его можно *сбросить*, то есть убрать из URL. Тогда он примет значение по умолчанию: - -```latte -reset -``` - - -Постоянные компоненты -===================== - -Постоянными могут быть не только параметры, но и компоненты. Их постоянные параметры тогда передаются даже между разными действиями презентера или между несколькими презентерами. Постоянные компоненты мы помечаем атрибутом на классе презентера. Например, компоненты `calendar` и `poll` мы помечаем так: - -```php -use Nette\Application\Attributes\Persistent; - -#[Persistent('calendar', 'poll')] -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Подкомпоненты внутри этих компонентов помечать не нужно, они тоже становятся постоянными. - -Более старая аннотация `@persistent` ещё работает, но объявлена устаревшей и выдаёт предупреждение: - -```php -/** - * @persistent(calendar, poll) - */ -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Компоненты с зависимостями -========================== - -Как создавать компоненты с зависимостями, не "захламляя" презентеры, которые будут их использовать? Благодаря продуманным возможностям DI-контейнера Nette, как и в случае с обычными сервисами, большую часть работы можно оставить фреймворку. - -Возьмём для примера компонент, зависящий от сервиса `PollFacade`: - -```php -class PollControl extends Control -{ - public function __construct( - private int $id, // ID опроса, для которого мы создаём компонент - private PollFacade $facade, - ) { - } - - public function handleVote(int $voteId): void - { - $this->facade->vote($this->id, $voteId); - // ... - } -} -``` - -Если бы мы писали обычный сервис, обсуждать было бы нечего. DI-контейнер незаметно позаботился бы о передаче всех зависимостей. Однако с компонентами мы обычно обходимся созданием нового экземпляра прямо в презентере, в [фабричных методах |#Фабричные методы] `createComponent…()`. Но передавать в презентер все зависимости всех компонентов только для того, чтобы передать их дальше компонентам, утомительно. И сколько кода приходится писать... - -Логично спросить: почему бы просто не зарегистрировать компонент как обычный сервис, передать его в презентер и затем возвращать в методе `createComponent…()`? Однако такой подход неуместен, потому что мы хотим иметь возможность при необходимости создавать компонент несколько раз. - -Правильное решение - написать для компонента фабрику, то есть класс, который создаёт компонент за нас: - -```php -class PollControlFactory -{ - public function __construct( - private PollFacade $facade, - ) { - } - - public function create(int $id): PollControl - { - return new PollControl($id, $this->facade); - } -} -``` - -Эту фабрику мы регистрируем в нашем контейнере в конфигурации: - -```neon -services: - - PollControlFactory -``` - -и наконец используем её в своём презентере: - -```php -class PollPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private PollControlFactory $pollControlFactory, - ) { - } - - protected function createComponentPollControl(): PollControl - { - $pollId = 1; // мы можем передать свой параметр - return $this->pollControlFactory->create($pollId); - } -} -``` - -Прекрасно то, что Nette DI умеет [порождать |dependency-injection:factory] такие простые фабрики, поэтому вместо всего её кода вам достаточно написать её интерфейс: - -```php -interface PollControlFactory -{ - public function create(int $id): PollControl; -} -``` - -И это всё. Nette внутренне реализует этот интерфейс и внедрит его в презентер, где мы сможем им пользоваться. Он волшебным образом добавит в наш компонент параметр `$id` и экземпляр класса `PollFacade`. - - -Компоненты в подробностях -========================= - -Компоненты в Nette Application представляют переиспользуемые части веб-приложения, которые мы встраиваем в страницы и которым посвящена вся эта глава. Каковы же в точности возможности такого компонента? - -1) Он отрисовывается в шаблоне -2) Он знает, [какую свою часть |ajax#Сниппеты] отрисовать при AJAX-запросе (сниппеты) -3) Он умеет хранить своё состояние в URL (постоянные параметры) -4) Он умеет реагировать на действия пользователя (сигналы) -5) Он образует иерархическую структуру (корнем которой служит презентер) - -За каждую из этих функций отвечает один из классов в цепочке наследования. За отрисовку (1 + 2) отвечает [api:Nette\Application\UI\Control], за встраивание в [жизненный цикл |presenters#Жизненный цикл презентера] (3, 4) - класс [api:Nette\Application\UI\Component], а за создание иерархической структуры (5) - классы [Container и Component |component-model:]. - -``` -Nette\ComponentModel\Component { IComponent } -| -+- Nette\ComponentModel\Container { IContainer } - | - +- Nette\Application\UI\Component { SignalReceiver, StatePersistent } - | - +- Nette\Application\UI\Control { Renderable } - | - +- Nette\Application\UI\Presenter { IPresenter } -``` - - -Жизненный цикл компонента -------------------------- - -[* lifecycle-component.svg *] *** *Жизненный цикл компонента* .<> - - -Проверка постоянных параметров ------------------------------- - -Значения [постоянных параметров |#Постоянные параметры], полученные из URL, записываются в свойства методом `loadState()`. Он также проверяет, соответствуют ли они типу данных, указанному у свойства; иначе он отвечает ошибкой 404, и страница не отображается. - -Никогда не доверяйте постоянным параметрам вслепую, потому что пользователь легко может переписать их в URL. Вот так мы проверяем, например, что номер страницы `$this->page` больше 0. Подходящий способ - переопределить упомянутый метод `loadState()`: - -```php -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; - - public function loadState(array $params): void - { - parent::loadState($params); // здесь задаётся $this->page - // далее идёт собственная проверка значения: - if ($this->page < 1) { - $this->error(); - } - } -} -``` - -Обратным процессом, то есть сбором значений из постоянных свойств, занимается метод `saveState()`. - - -Подключение к презентеру ------------------------- - -В момент, когда компонент становится частью иерархии презентера, вызываются его callback-функции, хранящиеся в массиве `$onAnchor`. С этого момента у компонента есть презентер, он может безопасно создавать ссылки, читать постоянные параметры и так далее. - -```php -$control->onAnchor[] = function ($control): void { - // у компонента теперь есть презентер -}; -``` - - -Сигналы в подробностях ----------------------- - -Сигнал вызывает перезагрузку страницы ровно так же, как исходный запрос (кроме случая вызова через AJAX), и вызывает метод `signalReceived($signal)`, реализация которого по умолчанию в классе `Nette\Application\UI\Component` пытается вызвать метод, составленный из слов `handle`. Дальнейшая обработка зависит от объекта. Объекты, наследующие от `Component` (то есть `Control` и `Presenter`), реагируют попыткой вызвать метод `handle` с нужными параметрами. - -Иначе говоря: берётся определение функции `handle` вместе со всеми параметрами, пришедшими с запросом, параметры из URL сопоставляются аргументам по имени, и делается попытка вызвать метод. Например, значение параметра `id` из URL передаётся как аргумент `$id`, `something` из URL - как `$something` и так далее. А если метода не существует, метод `signalReceived` выбрасывает [исключение |api:Nette\Application\UI\BadSignalException]. - -Помимо параметров из URL сигнал читает и параметры, отправленные в **теле POST-запроса**. Это удобно, потому что сигналы часто вызываются из JavaScript, где данные естественно отправлять методом POST. Однако если параметр с одним и тем же именем приходит и из URL, и из тела POST, приоритет имеет значение **из URL**. Поэтому не давайте полю POST такое же имя, как у параметра URL или маршрута, иначе значение из URL молча его перекроет. Параметры сигнала делят общее пространство с параметрами действия и постоянными параметрами, см. [Общее пространство параметров |presenters#Общее пространство параметров]. - -Сигнал может принять любой компонент, презентер или объект, который реализует интерфейс `SignalReceiver` и подключён к дереву компонентов. - -Главными получателями сигналов будут `Presenters` и визуальные компоненты, наследующие от `Control`. Сигнал предназначен служить объекту знаком, что нужно что-то сделать: опрос должен засчитать голос пользователя, блок новостей должен раскрыться и показать вдвое больше новостей, форма была отправлена и должна обработать данные и так далее. - -URL для сигнала создаётся методом [Component::link() |api:Nette\Application\UI\Component::link()]. Как параметр `$destination` мы передаём строку `{signal}!`, а как `$args` - массив аргументов, которые хотим передать сигналу. Сигнал всегда вызывается на текущем презентере и действии с текущими параметрами; параметры сигнала лишь добавляются. Кроме того, добавляется **параметр `?do`, задающий сигнал**. - -Его формат - либо `{signal}`, либо `{signalReceiver}-{signal}`. `{signalReceiver}` - имя компонента в презентере. Поэтому в имени компонента нельзя использовать дефис: он служит для разделения имени компонента и сигнала, хотя таким способом можно вкладывать несколько компонентов друг в друга. - -Метод [isSignalReceiver()|api:Nette\Application\UI\Presenter::isSignalReceiver()] проверяет, является ли компонент (первый аргумент) получателем сигнала (второй аргумент). Второй аргумент можно опустить, тогда проверяется, является ли компонент получателем какого-либо сигнала. Если второй параметр установлен в `true`, проверяется, является ли получателем указанный компонент или любой из его потомков. - -На любом этапе, предшествующем `handle`, мы можем выполнить сигнал вручную, вызвав метод [processSignal()|api:Nette\Application\UI\Presenter::processSignal()], который заботится об обработке сигнала: он берёт компонент, определённый как получатель сигнала (если получатель не указан, это сам презентер), и отправляет ему сигнал. - -Пример: - -```php -if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) { - $this->processSignal(); -} -``` - -Это выполняет сигнал досрочно, и повторно он вызван не будет. diff --git a/application/ru/configuration.texy b/application/ru/configuration.texy deleted file mode 100644 index 7b37255d05..0000000000 --- a/application/ru/configuration.texy +++ /dev/null @@ -1,186 +0,0 @@ -Конфигурация приложения -*********************** - -.[perex] -Обзор параметров конфигурации Nette Application. - - -Application -=========== - -```neon -application: - # показывать панель "Nette Application" в Tracy BlueScreen? - debugger: ... # (bool) включено при наличии Tracy - - # в продакшене исключения всегда обрабатывает error-презентер; - # этот параметр лишь включает такое поведение и в режиме разработки - catchExceptions: ... # (bool) по умолчанию false, то есть выключено в dev, всегда включено в продакшене - - # имя error-презентера - errorPresenter: Error # (string|array) по умолчанию 'Nette:Error' - - # задаёт псевдонимы презентеров и действий - aliases: ... - - # задаёт правила преобразования имени презентера в класс - mapping: ... - - # подавлять предупреждения о некорректных ссылках? - # действует только в режиме разработки - silentLinks: ... # (bool) по умолчанию false -``` - -Начиная с версии `nette/application` 3.2 можно задать пару error-презентеров: - -```neon -application: - errorPresenter: - 4xx: Error4xx # для Nette\Application\BadRequestException - 5xx: Error5xx # для остальных исключений -``` - -Разделять их полезно, потому что эти две ситуации принципиально различны. `BadRequestException` (коды 4xx) означает, что с приложением всё в порядке и просто посетитель запросил то, чего не существует. Поэтому вы можете использовать полноценный презентер, показывающий дружелюбное сообщение в макете вашего сайта. Напротив, ошибка 5xx означает, что в приложении что-то сломалось и вы не знаете что. Держите презентер для 5xx максимально простым, чтобы при его отрисовке уже ничто не могло отказать: в идеале он не должен трогать ни базу данных, ни макет, ни вошедшего пользователя. - -Параметр `silentLinks` определяет, как ведёт себя Nette в режиме разработки, когда порождение ссылки не удаётся (например, потому что презентера не существует). Значение по умолчанию `false` означает, что Nette выдаёт ошибку `E_USER_WARNING`. Значение `true` подавляет это сообщение. В производственной среде `E_USER_WARNING` выдаётся всегда. На это поведение можно повлиять и переменной презентера [$invalidLinkMode |creating-links#Некорректные ссылки]. - -[Псевдонимы упрощают обращение |creating-links#Псевдонимы] к часто используемым презентерам. - -[Mapping задаёт правила |directory-structure#Отображение презентеров], по которым имя класса выводится из имени презентера. - - -Автоматическая регистрация презентеров --------------------------------------- - -Nette автоматически добавляет презентеры как сервисы в DI-контейнер, что заметно ускоряет их создание. То, как Nette находит презентеры, можно настроить: - -```neon -application: - # искать презентеры в class map Composer? - scanComposer: ... # (bool) по умолчанию true - - # маска, которой должны соответствовать имя класса и имя файла - scanFilter: ... # (string) по умолчанию '*Presenter' - - # в каких каталогах искать презентеры? - scanDirs: # (string[]|false) по умолчанию '%appDir%' - - %vendorDir%/mymodule -``` - -Каталоги, перечисленные в `scanDirs`, не заменяют значение по умолчанию `%appDir%`, а дополняют его, поэтому в `scanDirs` окажутся оба пути: `%appDir%` и `%vendorDir%/mymodule`. Если мы хотим обойтись без каталога по умолчанию, мы используем [восклицательный знак |dependency-injection:configuration#Слияние]: - -```neon -application: - scanDirs!: - - %vendorDir%/mymodule -``` - -Сканирование каталогов можно отключить значением `false`. Тогда презентеры больше не регистрируются как сервисы, поэтому их нельзя настроить через секцию [decorator |dependency-injection:configuration#Decorator], а их создание идёт медленнее. Поэтому мы не рекомендуем полностью отключать автоматическую регистрацию: это снизит производительность приложения. - - -Шаблоны Latte -============= - -Эта настройка глобально влияет на поведение Latte в компонентах и презентерах. - -```neon -latte: - # показывать панель Latte в Tracy Bar для главного шаблона (true) или для всех компонентов (all)? - debugger: ... # (true|false|'all') включено при наличии Tracy (только в режиме отладки) - - # порождать шаблоны с заголовком declare(strict_types=1) - strictTypes: ... # (bool) по умолчанию false - - # включить [строгий режим разбора |latte:develop#strict mode] - strictParsing: ... # (bool) по умолчанию false - - # ограничивает область видимости переменных телом цикла - scopedLoopVariables: ... # (bool) по умолчанию false - - # убирает отступы, возникающие из-за вложенности в парные теги - dedent: ... # (bool) по умолчанию false - - # включить [проверку порождённого кода |latte:develop#Checking Generated Code] - phpLinter: ... # (string) по умолчанию null - - # задать локаль - locale: cs_CZ # (string) по умолчанию null - - # класс объекта $this->template - templateClass: App\MyTemplateClass # по умолчанию Nette\Bridges\ApplicationLatte\DefaultTemplate -``` - -Новые [расширения |latte:extending-latte#Расширение Latte] добавляются так: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - - -Маршрутизация -============= - -Основные настройки: - -```neon -routing: - # показывать панель маршрутизации в Tracy Bar? - debugger: ... # (bool) включено при наличии Tracy (только в режиме отладки) - - # сериализовать маршрутизатор в DI-контейнер - cache: ... # (bool) по умолчанию false -``` - -Маршрутизация обычно задаётся в классе [RouterFactory |routing#Набор маршрутов]. Как вариант, маршруты можно задать и в конфигурации парами `маска: действие`, но такой способ не даёт большой гибкости: - -```neon -routing: - routes: - 'detail/': Admin:Home:default - '/': Front:Home:default -``` - - -Константы -========= - -Создание констант PHP. - -```neon -constants: - Foobar: 'baz' -``` - -Константа `Foobar` будет создана после старта приложения. - -.[note] -Константы не должны служить глобально доступными переменными. Для передачи значений объектам используйте [внедрение зависимостей |dependency-injection:passing-dependencies]. - - -PHP -=== - -Настройка директив PHP. Обзор всех директив есть на [php.net |https://www.php.net/manual/en/ini.list.php]. - -```neon -php: - date.timezone: Europe/Prague -``` - - -Сервисы DI -========== - -Эти сервисы добавляются в DI-контейнер: - -| Имя | Тип | Описание -|----------------------------|---------------------------------------------------|----------------------------------------- -| `application.application` | [api:Nette\Application\Application] | [запускающий модуль приложения |how-it-works#Nette Application] -| `application.linkGenerator` | [api:Nette\Application\LinkGenerator] | [LinkGenerator |creating-links#LinkGenerator] -| `application.presenterFactory` | [api:Nette\Application\IPresenterFactory] | фабрика презентеров -| `application.###` | [api:Nette\Application\UI\Presenter] | отдельные презентеры -| `routing.router` | [api:Nette\Routing\Router] | маршрутизатор -| `latte.latteFactory` | [api:Nette\Bridges\ApplicationLatte\LatteFactory] | фабрика объекта `Latte\Engine` -| `latte.templateFactory` | [api:Nette\Application\UI\TemplateFactory] | фабрика [`$this->template` |templates] diff --git a/application/ru/creating-links.texy b/application/ru/creating-links.texy deleted file mode 100644 index 330e2fc49e..0000000000 --- a/application/ru/creating-links.texy +++ /dev/null @@ -1,319 +0,0 @@ -Создание URL-ссылок -******************* - -
    - -Создавать ссылки в Nette так же просто, как показать пальцем. Достаточно прицелиться, а всю работу сделает фреймворк. Мы покажем: - -- как создавать ссылки в шаблонах и не только -- как отличить ссылку на текущую страницу -- что делать с некорректными ссылками - -
    - - -Благодаря [двунаправленной маршрутизации |routing] вам никогда не придётся жёстко прописывать URL вашего приложения в шаблонах или коде, ведь они могут позже измениться или оказаться сложными в сборке. В ссылке достаточно указать презентер и действие, передать параметры, а URL фреймворк породит сам. По сути это очень похоже на вызов функции. Вам понравится. - - -В шаблоне презентера -==================== - -Чаще всего мы создаём ссылки в шаблонах, и отличный помощник здесь - атрибут `n:href`: - -```latte -detail -``` - -Обратите внимание, что вместо HTML-атрибута `href` мы использовали [n:атрибут |latte:syntax#n:атрибуты] `n:href`. Его значением служит не URL, как было бы у атрибута `href`, а имя презентера и действия. - -Щелчок по ссылке, попросту говоря, похож на вызов метода `ProductPresenter::renderShow()`. А если у него есть параметры в сигнатуре, мы можем вызвать его с аргументами: - -```latte -product detail -``` - -Можно передавать и именованные параметры. Следующая ссылка передаёт параметр `lang` со значением `en`: - -```latte -product detail -``` - -Если у метода `ProductPresenter::renderShow()` нет `$lang` в сигнатуре, он может получить значение параметра через `$lang = $this->getParameter('lang')` или из [свойства |presenters#Параметры запроса]. - -Если параметры хранятся в массиве, их можно развернуть оператором `...`: - -```latte -{var $args = [$product->id, lang => en]} -product detail -``` - -Так называемые [постоянные параметры |presenters#Постоянные параметры] тоже передаются в ссылках автоматически. - -Атрибут `n:href` очень удобен для HTML-тегов ``. Если мы хотим вывести ссылку в другом месте, например в тексте, мы используем `{link}`: - -```latte -URL is: {link Home:default} -``` - - -В коде -====== - -Для создания ссылки в презентере служит метод `link()`: - -```php -$url = $this->link('Product:show', $product->id); -``` - -Параметры можно передать и массивом, где можно указать и именованные параметры: - -```php -$url = $this->link('Product:show', [$product->id, 'lang' => 'en']); -``` - -Ссылки можно создавать и без презентера, с помощью [#LinkGenerator] и его метода `link()`. - -Иногда нужно создать ссылку сейчас, а сам URL породить только позже. Для этого есть метод `lazyLink()`, возвращающий объект `Nette\Application\UI\Link`. Преимущество в том, что этот объект можно передать дальше, например в шаблон, и до его отрисовки ещё изменить его параметры методом `setParameter()`. Сам URL собирается только при преобразовании объекта в строку: - -```php -$link = $this->lazyLink('Product:show', $id); -// ... -echo $link; // URL порождается только здесь -``` - - -Ссылки на презентер -=================== - -Если целью ссылки служит презентер и действие, синтаксис такой: - -``` -[//] [[[[:]module:]presenter:]action | this] [#fragment] -``` - -Этот формат поддерживают все теги Latte и все методы презентера, работающие со ссылками, то есть `n:href`, `{link}`, `{plink}`, `link()`, `lazyLink()`, `isLinkCurrent()`, `redirect()`, `redirectPermanent()`, `forward()`, `canonicalize()`, а также [#LinkGenerator]. Так что, хотя в примерах используется `n:href`, на его месте могла бы быть любая из этих функций. - -Основная форма, таким образом, - `Презентер:действие`: - -```latte -home page -``` - -Если мы ссылаемся на действие текущего презентера, его имя можно опустить: - -```latte -home page -``` - -Если целевое действие - `default`, его можно опустить, но двоеточие должно остаться: - -```latte -home page -``` - -Ссылки могут указывать и на другие [модули |directory-structure#Презентеры и шаблоны]. Здесь ссылки различаются как относительные к вложенному подмодулю или абсолютные. Принцип аналогичен путям на диске, только вместо слешей используются двоеточия. Если считать, что текущий презентер входит в модуль `Front`, мы написали бы: - -```latte -link to Front:Shop:Product:show -link to Admin:Product:show -``` - -Особый случай - ссылка [на саму себя |#Ссылка на текущую страницу], где мы указываем целью `this`. - -```latte -refresh -``` - -Мы можем сослаться на определённую часть страницы через так называемый фрагмент после знака решётки `#`: - -```latte -link to Home:default and fragment #main -``` - -.{data-version:3.3.0} -Фрагмент можно задать и динамически, аргументом с ключом `#`. Его значение автоматически кодируется и имеет приоритет над фрагментом, указанным в цели: - -```php -$this->link('Home:default', ['#' => $fragment]); -``` - - -Абсолютные пути -=============== - -Ссылки, порождаемые через `link()` или `n:href`, всегда являются абсолютными путями (то есть начинаются с `/`), но не абсолютными URL с протоколом и доменом вроде `https://domain`. - -Чтобы породить абсолютный URL, добавьте в начале два слеша (например, `n:href="//Home:"`). Как вариант, вы можете переключить презентер на порождение только абсолютных ссылок, задав `$this->absoluteUrls = true`. - -В шаблоне для преобразования относительного пути в абсолютный можно использовать и фильтр `|absoluteUrl`. - - -Ссылка на текущую страницу -========================== - -Цель `this` создаёт ссылку на текущую страницу: - -```latte -refresh -``` - -При этом передаются все параметры, указанные в сигнатуре метода `action()` или `render()` (если `action()` не определён). Так что если мы находимся на странице `Product:show` с `id: 123`, ссылка на `this` передаст и этот параметр. - -Разумеется, параметры можно указать и напрямую: - -```latte -refresh -``` - -Функция `isLinkCurrent()` проверяет, совпадает ли цель ссылки с текущей страницей. Это можно использовать, например, в шаблоне, чтобы выделять ссылки и подобное. - -Параметры те же, что и у метода `link()`, но вместо конкретного действия можно использовать подстановочный знак `*`, означающий любое действие данного презентера. - -```latte -{if !isLinkCurrent('Admin:login')} - Login -{/if} - -
  • - ... -
  • -``` - -В сочетании с `n:href` в одном элементе можно использовать сокращённую форму: - -```latte -... -``` - -Подстановочный знак `*` можно использовать только вместо действия, но не вместо презентера. - -Чтобы определить, находимся ли мы в определённом модуле или его подмодуле, используйте метод `isModuleCurrent(moduleName)`. - -```latte -
  • - ... -
  • -``` - - -Изменение базы ссылок .{data-version:3.2.7} -=========================================== - -По умолчанию относительные ссылки отсчитываются от текущего презентера. Это можно изменить с помощью `{linkBase}`: - -```latte -{linkBase Admin:Dashboard} -product detail -``` - -Ссылка приведёт на `Admin:Dashboard:Product:show`. Затрагиваются только относительные ссылки: абсолютные, начинающиеся с двоеточия, и ссылки на текущий презентер (`this`, `show`) остаются без изменений. - -`{linkBase}` действует на весь шаблон и особенно полезен в шаблонах макетов, где обеспечивает единообразные ссылки независимо от вызывающего презентера. -Тег должен стоять в начале шаблона, иначе он выбросит `CompileException`. - - -Ссылки на сигнал -================ - -Целью ссылки может быть не только презентер и действие, но и [сигнал |components#Сигнал] (он вызывает метод `handle()`). Тогда синтаксис такой: - -``` -[//] [sub-component:]signal! [#fragment] -``` - -Сигнал, таким образом, отличается восклицательным знаком: - -```latte -signal -``` - -Можно создать и ссылку на сигнал подкомпонента (или под-подкомпонента): - -```latte -signal -``` - - -Ссылки в компоненте -=================== - -Поскольку [компоненты|components] - самостоятельные переиспользуемые единицы, которые не должны быть связаны с окружающими презентерами, ссылки здесь работают немного иначе. Атрибут Latte `n:href` и тег `{link}`, а также методы компонента вроде `link()` и другие **всегда считают целью ссылки имя сигнала**. Поэтому восклицательный знак даже не нужен: - -```latte -signal, not an action -``` - -Если бы мы хотели сослаться в шаблоне компонента на презентеры, мы использовали бы тег `{plink}`: - -```latte -home -``` - -или в коде - -```php -$this->getPresenter()->link('Home:default') -``` - - -Псевдонимы .{data-version:3.2.3} -================================ - -Иногда бывает полезно назначить паре "презентер:действие" легко запоминающийся псевдоним. Например, назвать главную страницу `Front:Home:default` просто `home`, а `Admin:Dashboard:default` - `admin`. - -Псевдонимы задаются в [конфигурации|configuration] под ключом `application › aliases`: - -```neon -application: - aliases: - home: Front:Home:default - admin: Admin:Dashboard:default - sign: Front:Sign:in -``` - -В ссылках они затем записываются со знаком собаки, например: - -```latte -administration -``` - -Они поддерживаются и во всех методах, работающих со ссылками, таких как `redirect()` и подобные. - - -Некорректные ссылки -=================== - -Может случиться, что мы создадим некорректную ссылку: либо потому, что она ведёт на несуществующий презентер, либо потому, что передаёт больше параметров, чем принимает в сигнатуре целевой метод, либо когда URL для целевого действия породить нельзя. Как обходиться с некорректными ссылками, задаётся в презентере через `$this->invalidLinkMode`. Он может принимать сочетание этих значений (констант): - -- `Presenter::InvalidLinkSilent` - молчаливый режим, возвращает в качестве URL символ # -- `Presenter::InvalidLinkWarning` - выдаётся предупреждение E_USER_WARNING, которое в производственном режиме попадёт в лог, но не прервёт выполнение скрипта -- `Presenter::InvalidLinkTextual` - наглядное предупреждение, выводит ошибку прямо в ссылку -- `Presenter::InvalidLinkException` - выбрасывает InvalidLinkException - -По умолчанию задано `InvalidLinkWarning` в производственном режиме и `InvalidLinkWarning | InvalidLinkTextual` в режиме разработки. `InvalidLinkWarning` в производственной среде не прерывает скрипт, но предупреждение попадает в лог. В среде разработки его перехватывает [Tracy |tracy:] и показывает синий экран. `InvalidLinkTextual` работает так, что возвращает как URL сообщение об ошибке, начинающееся с символов `#error:`. Чтобы такие ссылки бросались в глаза с первого взгляда, добавьте в свой CSS: - -```css -a[href^="#error:"] { - background: red; - color: white; -} -``` - -Если мы не хотим, чтобы в среде разработки возникали предупреждения, мы можем подавить их прямо в [конфигурации|configuration]. - -```neon -application: - silentLinks: true -``` - - -LinkGenerator -============= - -Как создавать ссылки с таким же удобством, как методом `link()`, но без презентера? Для этого и существует [api:Nette\Application\LinkGenerator]. - -LinkGenerator - сервис, который вы можете получить через конструктор и затем создавать ссылки его методом `link()`. - -По сравнению с презентерами есть отличие. LinkGenerator создаёт все ссылки сразу как абсолютные URL. Кроме того, "текущего презентера" нет, поэтому нельзя указать целью только имя действия `link('default')` или использовать относительные пути к модулям. - -Некорректные ссылки всегда выбрасывают `Nette\Application\UI\InvalidLinkException`. diff --git a/application/ru/directory-structure.texy b/application/ru/directory-structure.texy deleted file mode 100644 index 58cb3e5bba..0000000000 --- a/application/ru/directory-structure.texy +++ /dev/null @@ -1,526 +0,0 @@ -Структура каталогов приложения -****************************** - -
    - -Как спроектировать наглядную и масштабируемую структуру каталогов для проектов на Nette Framework? Мы покажем проверенные практики, которые помогут вам упорядочить код. Вы узнаете: - -- как **логично разложить** приложение по каталогам -- как спроектировать структуру так, чтобы она **хорошо масштабировалась** по мере роста проекта -- какие есть **возможные альтернативы** и в чём их плюсы и минусы - -
    - - -Важно упомянуть, что сам Nette Framework не навязывает никакой конкретной структуры. Он спроектирован так, чтобы легко подстраиваться под любые потребности и предпочтения. - - -Базовая структура проекта -========================= - -Хотя Nette Framework не диктует никакой фиксированной структуры каталогов, есть проверенное расположение по умолчанию в виде [Web Project|https://github.com/nette/web-project]: - -/--pre -web-project/ -├── app/ ← каталог приложения -├── assets/ ← файлы SCSS, JS, изображения..., как вариант resources/ -├── bin/ ← скрипты для командной строки -├── config/ ← конфигурация -├── log/ ← записанные ошибки -├── temp/ ← временные файлы, кеш -├── tests/ ← тесты -├── vendor/ ← библиотеки, установленные Composer -└── www/ ← публичный каталог (document-root) -\-- - -Вы можете свободно менять эту структуру под свои нужды: переименовывать или переносить папки. Затем достаточно поправить относительные пути к каталогам в `Bootstrap.php` и, возможно, в `composer.json`. Больше ничего не нужно, никакой сложной перенастройки, никаких изменений констант. У Nette есть умное автоопределение, и он сам распознаёт расположение приложения, включая его базовый URL. - - -Принципы организации кода -========================= - -Когда вы впервые изучаете новый проект, вы должны быстро сориентироваться. Представьте, что вы заходите в каталог `app/Model/` и видите такую структуру: - -/--pre -app/Model/ -├── Services/ -├── Repositories/ -└── Entities/ -\-- - -Отсюда вы узнаёте только то, что в проекте используются какие-то сервисы, репозитории и сущности. О настоящем назначении приложения вы не узнаёте ничего. - -Посмотрим на другой подход - **организацию по доменам**: - -/--pre -app/Model/ -├── Cart/ -├── Payment/ -├── Order/ -└── Product/ -\-- - -Здесь всё иначе: с первого взгляда ясно, что это интернет-магазин. Сами имена каталогов раскрывают, что умеет приложение: оно работает с платежами, заказами и товарами. - -Первый подход (организация по типам классов) приносит на практике несколько проблем: логически связанный код раздроблен по разным папкам, и вам приходится прыгать между ними. Поэтому мы будем организовывать по доменам. - - -Пространства имён ------------------ - -Принято, чтобы структура каталогов соответствовала пространствам имён приложения. Это значит, что физическое расположение файлов совпадает с их пространством имён. Например, у класса, лежащего в `app/Model/Product/ProductRepository.php`, должно быть пространство имён `App\Model\Product`. Этот принцип помогает ориентироваться в коде и упрощает автозагрузку. - - -Единственное и множественное число в именах -------------------------------------------- - -Обратите внимание, что для главных каталогов приложения мы используем единственное число: `app`, `config`, `log`, `temp`, `www`. То же и внутри приложения: `Model`, `Core`, `Presentation`. Это потому, что каждый из них представляет одно цельное понятие. - -Точно так же `app/Model/Product` представляет всё, что связано с товарами. Мы не называем его `Products`, потому что это не папка, полная товаров (в ней лежали бы файлы вроде `nokia.php`, `samsung.php`). Это пространство имён с классами для работы с товарами - `ProductRepository.php`, `ProductService.php`. - -Папка `app/Tasks` во множественном числе, потому что содержит набор отдельных исполняемых скриптов - `CleanupTask.php`, `ImportTask.php`. Каждый из них - самостоятельная единица. - -Ради единообразия мы рекомендуем использовать: -- единственное число для пространств имён, представляющих функциональную единицу (даже если она работает с несколькими сущностями) -- множественное число для наборов самостоятельных единиц -- в случае сомнений или если не хочется над этим думать, выбирайте единственное число - - -Публичный каталог `www/` -======================== - -Этот каталог - единственный, доступный из веба (document-root). Часто вместо `www/` вам может встретиться имя `public/` - это лишь вопрос соглашения, на работу приложения оно не влияет. Каталог содержит: -- [точку входа |bootstrapping#index.php] приложения `index.php` -- файл `.htaccess` с правилами для mod_rewrite (для Apache) -- статические файлы (CSS, JavaScript, изображения) -- загруженные файлы - -Для правильной безопасности приложения принципиально важно [правильно настроить document-root |nette:troubleshooting#Как изменить или убрать из URL каталог www?]. - -.[note] -Никогда не помещайте в этот каталог папку `node_modules/`: в ней тысячи файлов, которые могут быть исполняемыми и не должны быть публично доступны. - - -Каталог приложения `app/` -========================= - -Это главный каталог с кодом приложения. Базовая структура: - -/--pre -app/ -├── Core/ ← инфраструктурные вопросы -├── Model/ ← бизнес-логика -├── Presentation/ ← презентеры и шаблоны -├── Tasks/ ← командные скрипты -└── Bootstrap.php ← стартовый класс приложения -\-- - -`Bootstrap.php` - [стартовый класс приложения|bootstrapping], который инициализирует окружение, загружает конфигурацию и создаёт DI-контейнер. - -Теперь рассмотрим отдельные подкаталоги подробнее. - - -Презентеры и шаблоны -==================== - -Презентационная часть приложения находится в каталоге `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` (см. [#Отображение презентеров]): - -```php -namespace App\Presentation\Admin\Dashboard; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -К презентеру `Dashboard` внутри модуля `Admin` мы обращаемся в приложении записью через двоеточие как `Admin:Dashboard`. К его действию `default` - как `Admin:Dashboard:default`. Для вложенных модулей мы используем несколько двоеточий, например `Shop:Order:Detail:default`. - - -Гибкое развитие структуры -------------------------- - -Одно из больших преимуществ этой структуры в том, как изящно она подстраивается под растущие потребности проекта. Возьмём для примера часть, порождающую XML-ленты. Поначалу у нас простой вид: - -/--pre -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` и прочих. - - -Расположение шаблонов ---------------------- - -В предыдущих примерах мы видели, что шаблоны лежат прямо в папке с презентером: - -/--pre -Dashboard/ -├── DashboardPresenter.php ← презентер -├── DashboardTemplate.php ← необязательный класс шаблона -└── default.latte ← шаблон -\-- - -На практике такое расположение оказывается самым удобным: все связанные файлы у вас под рукой. - -Как вариант, вы можете поместить шаблоны в подпапку `templates/`. Nette поддерживает оба варианта. Вы можете даже разместить шаблоны совсем вне папки `Presentation/`. Всё о возможностях размещения шаблонов можно найти в главе [Поиск шаблонов |templates#Поиск шаблона]. - - -Вспомогательные классы и компоненты ------------------------------------ - -Презентерам и шаблонам часто сопутствуют другие вспомогательные файлы. Мы размещаем их логично, по области действия: - -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`. Выбор зависит от соглашений команды. - - -Модель - сердце приложения -========================== - -Модель содержит всю бизнес-логику приложения. Правило её организации снова то же - структура по доменам: - -/--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 -// Сущность, отображённая на таблицу 'orders' в базе данных -class Order extends Nette\Database\Table\ActiveRow -{ - public function addItem(Product $product, int $quantity): void - { - $this->related('order_items')->insert([ - 'product_id' => $product->id, - 'quantity' => $quantity, - 'unit_price' => $product->price, - ]); - } -} -``` - -**Объекты-значения**: неизменяемые объекты, представляющие значения без собственной идентичности, например денежную сумму или адрес электронной почты. Два экземпляра объекта-значения с одинаковыми значениями считаются одинаковыми. - - -Инфраструктурный код -==================== - -Папка `Core/` (или, как вариант, `Infrastructure/`) - дом для технической основы приложения. Инфраструктурный код обычно включает: - -/--pre -app/Core/ -├── Router/ ← маршрутизация и управление URL -│ └── RouterFactory.php -├── Security/ ← аутентификация и авторизация -│ ├── Authenticator.php -│ └── Authorizator.php -├── Logging/ ← логирование и мониторинг -│ ├── SentryLogger.php -│ └── FileLogger.php -├── Cache/ ← слой кеширования -│ └── FullPageCache.php -└── Integration/ ← интеграция с внешними сервисами - ├── Slack/ - └── Stripe/ -\-- - -Для небольших проектов, естественно, достаточно плоской структуры: - -/--pre -Core/ -├── RouterFactory.php -├── Authenticator.php -└── QueueMailer.php -\-- - -Это код, который: - -- Занимается технической инфраструктурой (маршрутизация, логирование, кеширование) -- Интегрирует внешние сервисы (Sentry, Elasticsearch, Redis) -- Предоставляет базовые сервисы всему приложению (почта, база данных) -- Как правило, не зависит от конкретного домена: кеш или логгер работают одинаково и для интернет-магазина, и для блога. - -Не уверены, куда относится определённый класс - сюда или в модель? Ключевое отличие в том, что код в `Core/`: - -- Ничего не знает о домене (товарах, заказах, статьях) -- Обычно можно перенести в другой проект -- Решает "как это работает" (как отправить письмо), а не "что оно делает" (какое письмо отправить) - -Пример для лучшего понимания: - -- `App\Core\MailerFactory` - создаёт экземпляры класса для отправки писем, занимается настройками SMTP -- `App\Model\OrderMailer` - использует `MailerFactory` для отправки писем о заказах, знает их шаблоны и то, когда их следует отправлять - - -Командные скрипты -================= - -Приложениям часто нужно выполнять действия вне обычных HTTP-запросов, будь то фоновая обработка данных, обслуживание или периодические задачи. Для запуска служат простые скрипты в каталоге `bin/`, а сама логика реализации размещается в `app/Tasks/` (или `app/Commands/`). - -Пример: - -/--pre -app/Tasks/ -├── Maintenance/ ← скрипты обслуживания -│ ├── CleanupCommand.php ← удаление старых данных -│ └── DbOptimizeCommand.php ← оптимизация базы данных -├── Integration/ ← интеграция с внешними системами -│ ├── ImportProducts.php ← импорт из системы поставщика -│ └── SyncOrders.php ← синхронизация заказов -└── Scheduled/ ← регулярные задачи - ├── NewsletterCommand.php ← рассылка новостных писем - └── ReminderCommand.php ← уведомления клиентов -\-- - -Что относится к модели, а что к командным скриптам? Например, логика отправки одного письма - часть модели, а массовая отправка тысяч писем относится к `Tasks/`. - -Задачи обычно запускаются из командной строки или через cron: скрипт в `bin/` создаёт DI-контейнер методом [bootConsoleApplication() |bootstrapping#Разные окружения] и достаёт из него нужный сервис. Их можно запускать и HTTP-запросом, но при этом нужно подумать о безопасности. Презентер, запускающий задачу, нужно защитить, например только для вошедших пользователей или сильным токеном и доступом с разрешённых IP-адресов. Для долгих задач нужно увеличить ограничение времени работы скрипта и использовать `session_write_close()`, чтобы не блокировать сессию. - - -Другие возможные каталоги -========================= - -Помимо упомянутых базовых каталогов вы можете добавить и другие специализированные папки под нужды проекта. Посмотрим на самые частые и на их применение: - -/--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/ ← шаблоны писем -│ ├── order-confirmation.latte -│ └── welcome.latte -└── OrderMailer.php -\-- - - -Отображение презентеров -======================= - -Отображение задаёт правила выведения имени класса из имени презентера. Мы указываем их в [конфигурации|configuration] под ключом `application › mapping`. - -На этой странице мы показали, что размещаем презентеры в папке `app/Presentation` (или `app/UI`). Начиная с Nette Application 3.3 это соглашение по умолчанию, которое настраивать не нужно. Если вы используете другую структуру или хотите указать отображение явно, настройке по умолчанию соответствует такая строка: - -```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.3): - -```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 deleted file mode 100644 index ab4934da46..0000000000 --- a/application/ru/how-it-works.texy +++ /dev/null @@ -1,200 +0,0 @@ -Как работают приложения? -************************ - -
    - -Сейчас вы читаете основополагающую главу документации Nette. Вы узнаете полные принципы работы веб-приложений от А до Я, с момента рождения запроса до завершения выполнения PHP-скрипта. После прочтения вы будете понимать: - -- как всё это работает -- что такое Bootstrap, презентер и DI-контейнер -- как выглядит структура каталогов - -
    - - -Структура каталогов -=================== - -Откройте пример скелета веб-приложения под названием [WebProject|https://github.com/nette/web-project]. По ходу чтения вы можете заглядывать в обсуждаемые файлы. - -Структура каталогов выглядит примерно так: - -/--pre -web-project/ -├── app/ ← каталог приложения -│ ├── Core/ ← базовые классы, необходимые для работы -│ │ └── RouterFactory.php ← настройка URL-адресов -│ ├── Presentation/ ← презентеры, шаблоны и прочее -│ │ ├── @layout.latte ← шаблон макета -│ │ └── Home/ ← каталог презентера Home -│ │ ├── HomePresenter.php ← класс презентера Home -│ │ └── default.latte ← шаблон для действия default -│ └── Bootstrap.php ← стартовый класс Bootstrap -├── assets/ ← ресурсы (SCSS, TypeScript, исходные изображения) -├── bin/ ← скрипты, запускаемые из командной строки -├── config/ ← конфигурационные файлы -│ ├── common.neon -│ └── services.neon -├── log/ ← записанные ошибки -├── temp/ ← временные файлы, кеш, … -├── vendor/ ← библиотеки, установленные Composer -│ ├── ... -│ └── autoload.php ← автозагрузка всех установленных пакетов -├── www/ ← публичный каталог, document-root проекта -│ ├── assets/ ← скомпилированные статические файлы (CSS, JS, изображения, ...) -│ ├── .htaccess ← правила для mod_rewrite -│ └── index.php ← начальный файл, запускающий приложение -└── .htaccess ← запрещает доступ ко всем каталогам, кроме www -\-- - -Структуру каталогов можно изменять как угодно, переименовывать или переносить папки, она полностью гибкая. У Nette есть и умное автоопределение, которое само распознаёт расположение приложения, включая базовый URL. - -Для чуть более крупных приложений мы можем разложить каталоги презентеров и шаблонов по [подкаталогам |directory-structure#Презентеры и шаблоны] и сгруппировать классы в пространства имён, которые мы называем модулями. - -Каталог `www/` представляет собой публичный каталог, или document-root проекта. Вы можете его переименовать, ничего больше на стороне приложения настраивать не придётся. Нужно лишь [настроить хостинг |nette:troubleshooting#Как изменить или убрать из URL каталог www?] так, чтобы document-root указывал на этот каталог. - -WebProject можно скачать сразу вместе с Nette через [Composer |best-practices:composer]: - -```shell -composer create-project nette/web-project -``` - -В Linux или macOS задайте каталогам `log/` и `temp/` [права на запись |nette:troubleshooting#Задание прав на каталоги]. - -Приложение WebProject готово к запуску, настраивать вообще ничего не нужно, и вы можете открыть его прямо в браузере, обратившись к папке `www/`. - - -HTTP-запрос -=========== - -Всё начинается с того, что пользователь открывает страницу в браузере. Браузер отправляет на сервер HTTP-запрос. Этот запрос нацелен на единственный PHP-файл в публичном каталоге `www/`, а именно на `index.php`. Допустим, запрос идёт по адресу `https://example.com/product/123`. Благодаря подходящей [настройке сервера |nette:troubleshooting#Как настроить сервер для красивых URL?] даже такой URL сопоставляется файлу `index.php`, который и выполняется. - -Его задача: - -1) инициализировать окружение -2) получить фабрику -3) запустить приложение Nette, которое обработает запрос - -Какую фабрику? Мы же не тракторы производим, а сайты делаем! Погодите, сейчас всё объяснится. - -Под "инициализацией окружения" мы понимаем, например, включение [Tracy|tracy:] - потрясающего инструмента для записи в лог и наглядного показа ошибок. На производственном сервере она записывает ошибки в лог, а в среде разработки показывает их прямо на экране. Поэтому инициализация включает и определение того, работает сайт в производственном режиме или в режиме разработки. Nette использует для этого [умное автоопределение |bootstrapping#Режим разработки и производственный режим]: если вы запускаете сайт на localhost, он работает в режиме разработки. Настраивать ничего не нужно, и приложение сразу готово и к разработке, и к боевому развёртыванию. Эти шаги выполняются и подробно описываются в главе о [классе Bootstrap|bootstrapping]. - -Третий пункт (да, второй мы пропустили, но вернёмся к нему) - запуск приложения. Обработка HTTP-запросов в Nette лежит на классе `Nette\Application\Application` (далее `Application`). Так что, когда мы говорим "запустить приложение", мы имеем в виду вызов метода с говорящим именем `run()` у объекта этого класса. - -Nette выступает наставником, который направляет вас писать чистые приложения по проверенным методикам. Одна из самых устоявшихся - **внедрение зависимостей**, сокращённо DI. Мы не хотим сейчас нагружать вас объяснением DI, для этого есть [отдельная глава|dependency-injection:introduction]. Важное следствие в том, что ключевые объекты обычно создаёт фабрика объектов, известная как **DI-контейнер** (или DIC). Да, это та самая фабрика, о которой шла речь. Она порождает нам и объект `Application`, поэтому сначала нам нужен контейнер. Мы получаем его через класс `Configurator`, даём ему создать объект `Application`, вызываем у него метод `run()`, и приложение Nette запускается. Именно это и происходит в файле [index.php |bootstrapping#index.php]. - - -Nette Application -================= - -У класса `Application` единственная задача: ответить на HTTP-запрос. - -Приложения, написанные на Nette, делятся на множество так называемых презентеров (в других фреймворках вы можете встретить термин "контроллер", это по сути одно и то же). Это классы, каждый из которых представляет определённую страницу сайта: например, главную страницу, товар в интернет-магазине, форму входа, ленту карты сайта и так далее. У приложения может быть от одного до тысяч презентеров. - -`Application` начинает с того, что спрашивает так называемый маршрутизатор, какой презентер должен обработать текущий запрос. Маршрутизатор определяет ответственного. Он изучает входной URL `https://example.com/product/123` и по своей настройке решает, что эта задача принадлежит, например, **презентеру** `Product`, который должен выполнить **действие** `show` для товара с `id: 123`. Пару "презентер + действие" принято записывать через двоеточие: `Product:show`. - -Итак, маршрутизатор превратил URL в пару `Презентер:действие` с параметрами, в нашем случае `Product:show` и `id: 123`. Как выглядит такой маршрутизатор, вы можете увидеть в файле `app/Core/RouterFactory.php`, а подробно мы описываем его в главе [Маршрутизация |Routing]. - -Пойдём дальше. `Application` теперь знает имя презентера и может действовать. Он создаёт экземпляр класса `ProductPresenter`, содержащего код презентера `Product`. Точнее, он просит DI-контейнер создать презентер, потому что создание объектов - его обязанность. - -Презентер может выглядеть так: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ProductRepository $repository, - ) { - } - - public function renderShow(int $id): void - { - // получаем данные из модели и передаём их в шаблон - $this->template->product = $this->repository->getProduct($id); - } -} -``` - -Презентер берёт обработку запроса на себя. Задача ясна: выполнить действие `show` с `id: 123`. В терминологии презентеров это значит, что вызывается метод `renderShow()`, получающий `123` в параметре `$id`. - -Презентер может обрабатывать несколько действий, то есть у него может быть несколько методов `render()`. Однако мы рекомендуем проектировать презентеры с одним действием или с как можно меньшим их числом. - -Итак, был вызван метод `renderShow(123)`. Его код - выдуманный пример, но он показывает, как данные передаются в шаблон, а именно записью в `$this->template`. - -Затем презентер возвращает ответ. Это может быть HTML-страница, изображение, XML-документ, отправка файла с диска, JSON или, скажем, перенаправление на другую страницу. Важно, что, если мы явно не указываем, как отвечать (а именно так и обстоит дело с `ProductPresenter`), ответом будет отрисовка шаблона в HTML-страницу. Почему? Потому что в 99 % случаев мы хотим отрисовать шаблон. Поэтому презентер принимает такое поведение как поведение по умолчанию, чтобы упростить нам работу. В этом суть Nette. - -Нам даже не нужно указывать, какой шаблон отрисовать: фреймворк выведет путь автоматически. В случае действия `show` он просто попробует загрузить шаблон `show.latte`, лежащий в том же каталоге, что и класс `ProductPresenter`. Он также попробует найти макет в файле `@layout.latte` (подробнее о [поиске шаблонов |templates#Поиск шаблона]). - -Затем шаблоны отрисовываются. На этом задача презентера и всего приложения завершена. Если шаблона не существует, возвращается страница с ошибкой 404. Подробнее о презентерах можно узнать на странице [Презентеры|presenters]. - -[* request-flow.svg *] - -Для верности повторим весь ход событий с чуть другим URL: - -1) URL - `https://example.com` -2) Приложение стартует, создаётся DI-контейнер и выполняется `Application::run()`. -3) Маршрутизатор расшифровывает URL в пару `Home:default`. -4) Создаётся экземпляр класса `HomePresenter`. -5) Вызывается метод `renderDefault()` (если он существует). -6) Отрисовывается шаблон, например `default.latte`, вместе с макетом, например `@layout.latte`. - - -Возможно, вы только что столкнулись со множеством новых понятий, но мы верим, что они осмысленны. Разрабатывать приложения на Nette на удивление просто. - - -Шаблоны -======= - -Раз уж речь зашла о шаблонах: Nette использует систему шаблонов [Latte |latte:]. Поэтому файлы шаблонов имеют расширение `.latte`. Latte используется прежде всего потому, что это самая безопасная система шаблонов для PHP, а ещё самая интуитивная. Учить много нового не нужно: достаточно знания PHP и нескольких тегов. Всё нужное вы найдёте [в документации |templates]. - -В шаблоне вы [создаёте ссылки |creating-links] на другие презентеры и действия вот так: - -```latte -product detail -``` - -Просто напишите привычную пару `Презентер:действие` вместо настоящего URL и добавьте нужные параметры. Хитрость в `n:href`, которая говорит Nette обработать этот атрибут. Он затем породит: - -```latte -product detail -``` - -Порождением URL занимается упомянутый маршрутизатор. Маршрутизаторы в Nette исключительны тем, что умеют не только преобразовывать URL в пару `Презентер:действие`, но и наоборот: порождать URL из имени презентера, действия и параметров. Благодаря этому вы можете полностью изменить формат URL во всём готовом приложении на Nette, не меняя ни одного символа в шаблонах или презентерах, - достаточно поправить маршрутизатор. Это же обеспечивает так называемую канонизацию, ещё одну уникальную возможность Nette, которая улучшает SEO, автоматически не давая одному и тому же содержимому существовать под разными URL. Многих программистов эта возможность поражает. - - -Интерактивные компоненты -======================== - -Нам нужно рассказать о презентерах ещё одну вещь: в них встроена система компонентов. Те, у кого больше опыта, могут вспомнить нечто похожее из Delphi или ASP.NET Web Forms; React или Vue.js построены на в чём-то родственных идеях. В мире PHP-фреймворков это совершенно уникальная возможность. - -Компоненты - самостоятельные переиспользуемые единицы, которые мы встраиваем в страницы (то есть в презентеры). Это могут быть [формы |forms:in-presenter], [таблицы данных |https://componette.org/contributte/datagrid/], меню, опросы - в общем, всё, что имеет смысл использовать повторно. Мы можем создавать собственные компоненты или воспользоваться какими-то из [огромного выбора |https://componette.org] компонентов с открытым кодом. - -Компоненты принципиально меняют подход к разработке приложений. Они открывают новые возможности собирать страницы из заранее подготовленных единиц. И у них есть кое-что общее с [Голливудом |components#Голливудский стиль]. - - -DI-контейнер и конфигурация -=========================== - -DI-контейнер, то есть фабрика объектов, - сердце всего приложения. - -Не переживайте, это не какой-то волшебный чёрный ящик, как могли бы навести на мысль предыдущие строки. На деле это довольно обыденный PHP-класс, порождённый Nette и сохранённый в каталоге кеша. В нём много методов с именами вроде `createServiceAbcd()`, каждый из которых умеет создать и вернуть определённый объект. Да, там есть и метод `createServiceApplication__application()`, порождающий экземпляр `Nette\Application\Application`, который понадобился нам в `index.php` для запуска приложения. Есть и методы для создания отдельных презентеров и так далее. - -Объекты, создаваемые DI-контейнером, по некоторым причинам называют сервисами. - -По-настоящему особенное в этом классе то, что вы его не программируете - это делает фреймворк. Он действительно порождает PHP-код и сохраняет его на диск. Вы лишь даёте указания, какие объекты контейнер должен уметь создавать и как именно. Эти указания записываются в [конфигурационных файлах |bootstrapping#Конфигурация DI-контейнера], которые используют формат [NEON|neon:format] и потому имеют расширение `.neon`. - -Конфигурационные файлы служат исключительно для указаний DI-контейнеру. Так что, если вы, например, зададите параметр `expiration: 14 days` в секции [session |http:configuration#Сессия], DI-контейнер при создании объекта `Nette\Http\Session`, представляющего сессию, вызовет его метод `setExpiration('14 days')` и тем самым воплотит конфигурацию в жизнь. - -Для вас подготовлена целая глава о том, что можно [настраивать |nette:configuring] и как [определять собственные сервисы |dependency-injection:services]. - -Как только вы немного погрузитесь в создание сервисов, вы столкнётесь с термином [autowiring |dependency-injection:autowiring]. Это возможность, которая невероятно упростит вам жизнь. Она умеет автоматически передавать объекты туда, где они вам нужны (например, в конструкторы ваших классов), без каких-либо действий с вашей стороны. Вы обнаружите, что DI-контейнер в Nette - маленькое чудо. - - -Что дальше? -=========== - -Мы разобрали основополагающие принципы приложений Nette. Пока это был поверхностный обзор, но вскоре вы погрузитесь глубже и со временем начнёте создавать замечательные веб-приложения. Куда двигаться дальше? Вы уже пробовали руководство [Создайте своё первое приложение|quickstart:]? - -Помимо описанного выше Nette предлагает целый арсенал [полезных классов|utils:], [слой работы с базой данных|database:] и многое другое. Попробуйте походить по документации. Или загляните в [блог|https://blog.nette.org]. Вы обнаружите много интересного. - -Пусть фреймворк принесёт вам много радости 💙 diff --git a/application/ru/multiplier.texy b/application/ru/multiplier.texy deleted file mode 100644 index 415a0d5f84..0000000000 --- a/application/ru/multiplier.texy +++ /dev/null @@ -1,63 +0,0 @@ -Multiplier: динамические компоненты -*********************************** - -.[perex] -Инструмент для динамического создания интерактивных компонентов. - -Начнём с типичного примера: представьте список товаров в интернет-магазине, где для каждой позиции нужна форма "Добавить в корзину". Один из возможных подходов - обернуть весь список в одну форму. Однако куда более удобный способ предлагает [api:Nette\Application\UI\Multiplier]. - -Multiplier позволяет удобно определить фабрику сразу для нескольких компонентов. Он работает по принципу вложенных компонентов: любой компонент, наследующий от [api:Nette\ComponentModel\Container], может содержать другие компоненты. - -.[tip] -См. главу о [компонентной модели |components#Компоненты в подробностях] в документации. - -Суть Multiplier в том, что он выступает родителем, который может динамически создавать своих потомков с помощью callback, переданного в конструктор. Смотрите пример: - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function () { - $form = new Nette\Application\UI\Form; - $form->addInteger('amount', 'Количество:') - ->setRequired(); - $form->addSubmit('send', 'Добавить в корзину'); - return $form; - }); -} -``` - -Теперь в шаблоне мы можем просто отрисовать форму для каждого товара, и каждая из них действительно будет отдельным компонентом. - -```latte -{foreach $items as $item} -

    {$item->title}

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

    Hello

    '; - } -}; -$this->sendResponse(new Responses\CallbackResponse($callback)); -``` - -Вы можете написать и собственный ответ. Достаточно реализовать интерфейс `Nette\Application\Response` с единственным методом `send()`, получающим HTTP-запрос и ответ. Это полезно, например, при потоковой передаче данных, которые вы не хотите держать в памяти: - -```php -class CsvResponse implements Nette\Application\Response -{ - public function __construct( - private string $fileName, - private iterable $rows, - ) { - } - - public function send(Nette\Http\IRequest $request, Nette\Http\IResponse $response): void - { - $response->setContentType('text/csv', 'utf-8'); - $response->sendAsFile($this->fileName); - - $handle = fopen('php://output', 'w'); - foreach ($this->rows as $row) { - fputcsv($handle, $row); - } - - fclose($handle); - } -} -``` - -Затем вы отправляете его в презентере как обычно: `$this->sendResponse(new CsvResponse('export.csv', $rows));` - - -HTTP-кеширование ----------------- - -Метод `lastModified()` позволяет легко воспользоваться HTTP-кешированием. Вы передаёте ему дату и время последнего изменения содержимого (как временную метку, строку или объект `DateTimeInterface`), а при желании ещё валидатор ETag (короткую строку, определяющую текущую версию содержимого, например её хеш) и время истечения. Если у браузера уже есть подходящая версия, презентер отправляет ответ `304 Not Modified` и завершается, поэтому страница не отрисовывается и не передаётся зря: - -```php -public function renderArticle(int $id): void -{ - $article = $this->articles->getById($id); - $this->lastModified($article->updatedAt); - // ... -} -``` - - -Завершение шаблона .{data-version:3.3.0} ----------------------------------------- - -Когда презентер отрисовывает шаблон, метод `sendTemplate()` прямо перед отрисовкой вызывает `completeTemplate()`. Этот метод заполняет переменные, помеченные атрибутом `#[TemplateVariable]`, и находит файл шаблона (переменные по умолчанию уже задаёт `TemplateFactory` при создании шаблона). Вы можете переопределить этот защищённый метод, чтобы добавить переменные, общие для всех представлений, или задать другой файл: - -```php -protected function completeTemplate(Nette\Application\UI\Template $template): void -{ - parent::completeTemplate($template); - $template->siteName = 'My App'; -} -``` - - -Ограничение доступа через `#[Requires]` .{data-version:3.2.3} -------------------------------------------------------------- - -Атрибут `#[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')]` - -.[note] -Начиная с версии 3.3 совпадение источника проверяется по заголовку браузера `Sec-Fetch-Site` (раньше через cookie SameSite), что надёжнее и проверяет точное совпадение схемы, домена и порта. - -Подробности в руководстве [Как использовать атрибут Requires |best-practices:attribute-requires]. - - -Проверка HTTP-метода --------------------- - -Презентеры в Nette автоматически проверяют HTTP-метод каждого входящего запроса, прежде всего из соображений безопасности. По умолчанию разрешены методы `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH`. - -Если вы хотите дополнительно разрешить, например, метод `OPTIONS`, используйте атрибут `#[Requires]` (начиная с Nette Application v3.2.3): - -```php -#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])] -class MyPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Начиная с версии 3.1.13 проверка выполняется в `checkHttpMethod()`, который проверяет, входит ли указанный в запросе метод в массив `$presenter->allowedMethods`. Начиная с версии 3.2.3 этот подход объявлен устаревшим в пользу `#[Requires]`. Переопределить метод можно так: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - protected function checkHttpMethod(): void - { - $this->allowedMethods[] = 'OPTIONS'; - parent::checkHttpMethod(); - } -} -``` - -Важно подчеркнуть, что если вы разрешаете метод `OPTIONS`, вы должны затем соответствующим образом обработать его в своём презентере. Этот метод часто используется как так называемый предварительный (preflight) запрос, который браузер автоматически отправляет перед настоящим запросом, когда нужно определить, допустим ли запрос по политике CORS (Cross-Origin Resource Sharing). Если вы разрешите метод, но не реализуете правильный ответ, это может привести к несогласованности и возможным проблемам с безопасностью. - - -Пометка устаревших действий .{data-version:3.2.3} -------------------------------------------------- - -Атрибут `#[Deprecated]` помечает действия, сигналы или целые презентеры как устаревшие и предназначенные к будущему удалению. При порождении ссылок на устаревшие части приложения Nette выдаёт предупреждение, чтобы обратить на это внимание разработчиков. - -Атрибут можно применить как ко всему классу презентера, так и к отдельным методам `action()`, `render()` и `handle()`. - - -Дополнительные материалы -======================== - -- [Методы и атрибуты inject |best-practices:inject-method-attribute] -- [Составление презентеров из трейтов |best-practices:presenter-traits] -- [Передача настроек в презентеры |best-practices:passing-settings-to-presenters] -- [Как вернуться на предыдущую страницу |best-practices:restore-request] diff --git a/application/ru/routing.texy b/application/ru/routing.texy deleted file mode 100644 index 7981746ea4..0000000000 --- a/application/ru/routing.texy +++ /dev/null @@ -1,749 +0,0 @@ -Маршрутизация -************* - -
    - -Маршрутизатор берёт на себя всё, что связано с URL-адресами, так что вам о них думать не приходится. Мы покажем: - -- как настроить маршрутизатор, чтобы URL выглядели так, как вы хотите -- обсудим SEO и перенаправления -- и покажем, как написать собственный маршрутизатор - -
    - - -Более дружелюбные к человеку URL (их называют также красивыми) удобнее, лучше запоминаются и положительно влияют на SEO. Nette помнит об этом и полностью идёт навстречу потребностям разработчиков. Вы можете спроектировать для своего приложения ровно ту структуру URL, которую хотите. Вы можете спроектировать её даже тогда, когда приложение уже готово, потому что это не требует изменений ни в коде, ни в шаблонах. Она изящно задаётся в [одном месте |#Встраивание], в маршрутизаторе, а не разбросана аннотациями по всем презентерам. - -Маршрутизатор в Nette исключителен тем, что он **двунаправленный**. Он умеет и расшифровывать URL из HTTP-запросов, и создавать ссылки. Тем самым он играет ключевую роль в [Nette Application |how-it-works#Nette Application], потому что не только решает, какой презентер и действие выполнят текущий запрос, но и используется для [порождения URL |creating-links] в шаблонах и не только. - -Однако маршрутизатор не ограничивается таким применением: вы можете использовать его в приложениях, где презентеры вообще не применяются, для REST API и прочего. Подробности в разделе [#Самостоятельное использование]. - - -Набор маршрутов -=============== - -Самый приятный способ задать структуру URL-адресов приложения предлагает класс [api:Nette\Application\Routers\RouteList]. Определение состоит из списка так называемых маршрутов, то есть масок URL-адресов и связанных с ними презентеров и действий, через простой API. Никак называть маршруты не нужно. - -```php -$router = new Nette\Application\Routers\RouteList; -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('article/', 'Article:view'); -// ... -``` - -Пример показывает, что если мы откроем в браузере `https://domain.com/rss.xml`, отобразится презентер `Feed` с действием `rss`. Если `https://domain.com/article/12`, отобразится презентер `Article` с действием `view` и так далее. Если подходящего маршрута не найдено, Nette Application отвечает выбрасыванием [BadRequestException |api:Nette\Application\BadRequestException], которое показывается пользователю как страница ошибки 404 Not Found. - - -Порядок маршрутов ------------------ - -**Порядок**, в котором перечислены отдельные маршруты, совершенно **решающий**, потому что они проверяются по очереди сверху вниз. Правило в том, что маршруты объявляются **от частных к общим**: - -```php -// НЕВЕРНО: 'rss.xml' перехватывается первым маршрутом и воспринимается как -$router->addRoute('', 'Article:view'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// ВЕРНО -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('', 'Article:view'); -``` - -Маршруты проверяются сверху вниз и при порождении ссылок: - -```php -// НЕВЕРНО: ссылка на 'Feed:rss' порождается как 'admin/feed/rss' -$router->addRoute('admin//', 'Admin:default'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// ВЕРНО -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('admin//', 'Admin:default'); -``` - -Не будем скрывать: правильно собрать маршруты требует некоторого навыка. Пока вы им не овладеете, полезным инструментом будет [панель маршрутизации |#Отладка маршрутизатора]. - - -Маска и параметры ------------------ - -Маска описывает относительный путь от корневого каталога сайта. Простейшая маска - статический URL: - -```php -$router->addRoute('products', 'Products:default'); -``` - -Часто маски содержат так называемые **параметры**. Они заключаются в угловые скобки (например, ``) и передаются целевому презентеру, например в метод `renderShow(int $year)` или в постоянный параметр `$year`: - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -Пример показывает, что если мы откроем в браузере `https://example.com/chronicle/2020`, отобразится презентер `History` с действием `show` и параметром `year: 2020`. - -Значение по умолчанию для параметров можно задать прямо в маске, и тогда они станут необязательными: - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -Теперь маршрут примет и URL `https://example.com/chronicle/`, который снова отобразит `History:show` с параметром `year: 2020`. - -Разумеется, параметрами могут быть и имена презентера и действия. Например: - -```php -$router->addRoute('/', 'Home:default'); -``` - -Указанный маршрут принимает, например, URL вида `/article/edit` или `/catalog/list` и понимает их как презентеры и действия `Article:edit` и `Catalog:list` соответственно. - -При этом он задаёт параметрам `presenter` и `action` значения по умолчанию `Home` и `default`, благодаря чему они тоже становятся необязательными. Таким образом, маршрут принимает и URL вида `/article` и понимает его как `Article:default`. И наоборот, ссылка на `Product:default` порождает путь `/product`, а ссылка на `Home:default` по умолчанию порождает путь `/`. - -Маска может описывать не только относительный путь от корневого каталога сайта, но и абсолютный путь, если начинается со слеша, или даже весь абсолютный URL, если начинается с двух слешей: - -```php -// относительно корня документов -$router->addRoute('/', /* ... */); - -// абсолютный путь (относительно домена) -$router->addRoute('//', /* ... */); - -// абсолютный URL с доменом (относительно схемы) -$router->addRoute('//.example.com//', /* ... */); - -// абсолютный URL со схемой -$router->addRoute('https://.example.com//', /* ... */); -``` - - -Проверочные выражения ---------------------- - -Для каждого параметра можно задать условие проверки с помощью [регулярного выражения|https://www.php.net/manual/en/reference.pcre.pattern.syntax.php]. Например, для параметра `id` мы указываем, что он может содержать только цифры, регулярным выражением `\d+`: - -```php -$router->addRoute('/[/]', /* ... */); -``` - -Регулярное выражение по умолчанию для всех параметров - `[^/]+`, то есть всё, кроме слеша. Если параметр должен принимать и слеши, мы задаём выражение `.+`: - -```php -// принимает https://example.com/a/b/c, путь будет 'a/b/c' -$router->addRoute('', /* ... */); -``` - - -Необязательные последовательности ---------------------------------- - -В маске необязательные части можно пометить квадратными скобками. Необязательной может быть любая часть маски, и она может содержать параметры: - -```php -$router->addRoute('[/]', /* ... */); - -// Принимает пути: -// /en/download => lang => en, name => download -// /download => lang => null, name => download -``` - -Когда параметр входит в необязательную последовательность, он, естественно, тоже становится необязательным. Если у него не задано значение по умолчанию, он будет null. - -Необязательные части могут быть и в домене: - -```php -$router->addRoute('//[.]example.com//', /* ... */); -``` - -Последовательности можно вкладывать и сочетать как угодно: - -```php -$router->addRoute( - '[[-]/][/page-]', - 'Home:default', -); - -// Принимает пути: -// /en/hello -// /en-us/hello -// /hello -// /hello/page-12 -``` - -При порождении URL предпочитается самый короткий вариант, поэтому всё, что можно опустить, опускается. Так, например, маршрут `index[.html]` порождает путь `/index`. Это поведение можно перевернуть, поставив после левой квадратной скобки восклицательный знак: - -```php -// принимает /hello и /hello.html, порождает /hello -$router->addRoute('[.html]', /* ... */); - -// принимает /hello и /hello.html, порождает /hello.html -$router->addRoute('[!.html]', /* ... */); -``` - -Необязательные параметры (то есть параметры со значением по умолчанию) без квадратных скобок по сути ведут себя так, будто заключены следующим образом: - -```php -$router->addRoute('//', /* ... */); - -// соответствует этому: -$router->addRoute('[/[/[]]]', /* ... */); -``` - -Если мы хотим повлиять на поведение завершающего слеша, чтобы, например, порождался `/home` вместо `/home/`, этого можно добиться так: - -```php -$router->addRoute('[[/[/]]]', /* ... */); -``` - - -Подстановочные знаки --------------------- - -В маске абсолютного URL мы можем использовать следующие подстановочные знаки, чтобы, например, не приходилось писать в маске домен, который может различаться в среде разработки и в производственной среде: - -- `%tld%` = домен верхнего уровня, например `com` или `org` -- `%sld%` = домен второго уровня, например `example` -- `%domain%` = домен без поддоменов, например `example.com` -- `%host%` = весь хост, например `www.example.com` -- `%basePath%` = путь к корневому каталогу - -```php -$router->addRoute('//www.%domain%/%basePath%//', /* ... */); -$router->addRoute('//www.%sld%.%tld%/%basePath%//', /* ... */); -``` - - -Расширенная запись ------------------- - -Цель маршрута, обычно записываемую в формате `Презентер:действие`, можно записать и массивом, задающим отдельные параметры и их значения по умолчанию: - -```php -$router->addRoute('/[/]', [ - 'presenter' => 'Home', - 'action' => 'default', -]); -``` - -Для более подробного описания можно использовать ещё более развёрнутую форму, где, помимо значений по умолчанию, можно задать другие свойства параметров, например проверочное регулярное выражение (см. параметр `id`): - -```php -use Nette\Routing\Route; - -$router->addRoute('/[/]', [ - 'presenter' => [ - Route::Value => 'Home', - ], - 'action' => [ - Route::Value => 'default', - ], - 'id' => [ - Route::Pattern => '\d+', - ], -]); -``` - -Важно отметить, что если параметры, заданные в массиве, не перечислены в маске пути, их значения изменить нельзя, даже параметрами запроса, указанными после вопросительного знака в URL. - -Это полезно для **фиксированных параметров**: чтобы дать конкретной странице короткий запоминающийся URL. Например, чтобы `/tos` всегда открывал `Article:view` с `id: 123`: - -```php -$router->addRoute('tos', [ - 'presenter' => 'Article', - 'action' => 'view', - 'id' => 123, -]); -``` - - -Фильтры и переводы ------------------- - -Исходный код приложения мы пишем по-английски, но если у сайта должны быть чешские URL, то простая маршрутизация вида: - -```php -$router->addRoute('/', 'Home:default'); -``` - -будет порождать английские URL вроде `/product/123` или `/cart`. Если мы хотим, чтобы презентеры и действия в URL были представлены чешскими словами (например, `/produkt/123` или `/kosik`), мы можем использовать словарь переводов. Для его записи нам уже нужен "более многословный" вариант второго параметра: - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterTable => [ - // строка в URL => презентер - 'produkt' => 'Product', - 'kosik' => 'Cart', - 'katalog' => 'Catalog', - ], - ], - 'action' => [ - Route::Value => 'default', - Route::FilterTable => [ - 'seznam' => 'list', - ], - ], -]); -``` - -Несколько ключей словаря переводов могут вести к одному презентеру. Так у него возникают разные псевдонимы. Последний ключ считается каноническим вариантом (то есть тем, который окажется в порождённом URL). - -Таблицу переводов можно так использовать для любого параметра. Если перевода нет, берётся исходное значение. Это поведение можно изменить, добавив `Route::FilterStrict => true`, и тогда маршрут отклонит URL, если значения нет в словаре. - -Помимо словаря переводов в виде массива можно применять и собственные функции перевода. - -```php -use Nette\Routing\Route; - -$router->addRoute('//', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterIn => function (string $s): string { /* ... */ }, - Route::FilterOut => function (string $s): string { /* ... */ }, - ], - 'action' => 'default', - 'id' => null, -]); -``` - -Функция `Route::FilterIn` преобразует параметр из URL в строку, которая затем передаётся презентеру; функция `FilterOut` обеспечивает преобразование в обратную сторону. - -У параметров `presenter`, `action` и `module` уже есть предопределённые фильтры, преобразующие стиль PascalCase или camelCase в kebab-case, используемый в URL. Значение параметров по умолчанию записывается в том виде, в каком оно передаётся приложению (PascalCase для презентера и модуля, camelCase для действия), поэтому, например, в случае презентера мы пишем ``, а не ``. - - -Общие фильтры -------------- - -Помимо фильтров, предназначенных для конкретных параметров, мы можем задать и общие фильтры, которые получают ассоциативный массив всех параметров, могут как угодно его изменить и вернуть. Общие фильтры задаются под пустым ключом. - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => 'Home', - 'action' => 'default', - '' => [ - Route::FilterIn => function (array $params): array { /* ... */ }, - Route::FilterOut => function (array $params): array { /* ... */ }, - ], -]); -``` - -Общие фильтры дают возможность совершенно как угодно изменить поведение маршрута. Мы можем использовать их, например, чтобы менять параметры на основе других параметров. Скажем, переводить `` и `` по текущему значению параметра ``. - -Если у параметра задан собственный фильтр и при этом существует общий, свой `FilterIn` выполняется раньше общего, и наоборот, общий `FilterOut` выполняется раньше своего. Таким образом, внутри общего фильтра значения параметров `presenter` и `action` записаны в стиле PascalCase или camelCase соответственно. - -О практическом применении этих фильтров, порождении дружественных к SEO URL вроде `/article/123-how-to-bake-bread` без изменения шаблонов, см. [Красивые URL со слагами |best-practices:pretty-urls]. - - -Флаг OneWay ------------ - -Односторонние маршруты служат для сохранения работоспособности старых URL, которые приложение больше не порождает, но всё ещё принимает. Мы помечаем их флагом `OneWay`: - -```php -// старый URL /product-info?id=123 -$router->addRoute('product-info', 'Product:detail', oneWay: true); -// новый URL /product/123 -$router->addRoute('product/', 'Product:detail'); -``` - -При обращении к старому URL презентер автоматически перенаправляет на новый, поэтому поисковые системы не проиндексируют эти страницы дважды (см. [#SEO и канонизация]). - - -Динамическая маршрутизация с callback-функциями ------------------------------------------------ - -Динамическая маршрутизация с callback-функциями позволяет напрямую сопоставить маршрутам функции (callback), которые выполняются при посещении данного пути. Эта гибкая возможность позволяет быстро и эффективно создавать разные точки входа в ваше приложение: - -```php -$router->addRoute('test', function () { - echo 'You are at the /test address'; -}); -``` - -Вы можете задать в маске и параметры, которые автоматически передаются в ваш callback: - -```php -$router->addRoute('', function (string $lang) { - echo match ($lang) { - 'cs' => 'Welcome to the Czech version of our website!', - 'en' => 'Welcome to the English version of our website!', - }; -}); -``` - -Помимо параметров из маски callback может получать и сервисы из DI-контейнера. Они передаются по типу параметра. Кроме того, параметр `$presenter` получает экземпляр [MicroPresenter |api:NetteModule\MicroPresenter], обрабатывающего маршрут: - -```php -$router->addRoute('', function (string $lang, Nette\Http\Request $httpRequest, NetteModule\MicroPresenter $presenter) { - // ... -}); -``` - - -Модули ------- - -Если у нас несколько маршрутов, относящихся к общему [модулю |directory-structure#Презентеры и шаблоны], мы используем `withModule()`. Указанный модуль автоматически подставляется перед презентером каждого маршрута группы и полностью исчезает из URL: - -```php -$router = new RouteList; -$router->withModule('Forum') // следующие маршруты входят в модуль Forum - ->addRoute('rss', 'Feed:rss') // презентером будет Forum:Feed - ->addRoute('/') - - ->withModule('Admin') // следующие маршруты входят в модуль Forum:Admin - ->addRoute('sign:in', 'Sign:in'); -``` - -Альтернатива - параметр `module`, который так же задаёт фиксированный модуль и держит его вне URL: - -```php -// URL manage/dashboard/default отображается на презентер Admin:Dashboard -$router->addRoute('manage//', [ - 'module' => 'Admin', -]); -``` - -Имя каждого презентера полно только вместе с его модулем, например `Front:Admin:ProductList`. Всякий раз, когда такое полное имя попадает в параметр URL, маршрутизатор кодирует его по двум простым правилам: каждое двоеточие `:` (разделитель модулей) становится **точкой**, а каждая граница слов в имени PascalCase - **дефисом**. Так `Front:Admin:ProductList` появляется в URL как `front.admin.product-list` и расшифровывается обратно тем же способом. Именно поэтому модульное приложение без перечисленных выше средств порождает URL, полные точек. - -И `withModule()`, и параметр `module` этого избегают именно потому, что убирают известный префикс модуля из имени презентера прежде, чем оно попадёт в URL: раз модуль постоянен, кодировать его вообще не нужно. - -Иногда мы хотим, чтобы сам модуль менялся и появлялся в URL, и тогда мы берём `` прямо в маску. Осторожно с одной принципиальной деталью: **`` захватывает весь путь модуля**, то есть всё до последнего двоеточия в имени презентера. Для презентера `Shop:Admin:Product` это означает модуль `Shop:Admin` и презентер `Product`, а поскольку двоеточия становятся точками, мы получаем: - - -Поддомены ---------- - -Наборы маршрутов можно разделять по поддоменам: - -```php -$router = new RouteList; -$router->withDomain('example.com') - ->addRoute('rss', 'Feed:rss') - ->addRoute('/'); -``` - -В имени домена тоже можно использовать [#Подстановочные знаки]: - -```php -$router = new RouteList; -$router->withDomain('example.%tld%') - // ... -``` - - -Префикс пути ------------- - -Наборы маршрутов можно разделять по пути в URL: - -```php -$router = new RouteList; -$router->withPath('eshop') - ->addRoute('rss', 'Feed:rss') // соответствует URL /eshop/rss - ->addRoute('/'); // соответствует URL /eshop// -``` - - -Сочетания ---------- - -Перечисленные группировки можно сочетать друг с другом: - -```php -$router = (new RouteList) - ->withDomain('admin.example.com') - ->withModule('Admin') - ->addRoute(/* ... */) - ->addRoute(/* ... */) - ->end() - ->withModule('Images') - ->addRoute(/* ... */) - ->end() - ->end() - ->withDomain('example.com') - ->withPath('export') - ->addRoute(/* ... */) - // ... -``` - - -Параметры запроса ------------------ - -Маски могут содержать и параметры запроса (параметры после вопросительного знака в URL). Задать для них проверочное выражение нельзя, но можно изменить имя, под которым они передаются презентеру: - -```php -// мы хотим использовать параметр запроса 'cat' в приложении под именем 'categoryId' -$router->addRoute('product ? id= & cat=', /* ... */); -``` - - -Параметры Foo -------------- - -Теперь копнём глубже. Параметры Foo - по сути безымянные параметры, позволяющие сопоставить регулярное выражение. Пример - маршрут, принимающий `/index`, `/index.html`, `/index.htm` и `/index.php`: - -```php -$router->addRoute('index', /* ... */); -``` - -Можно и явно задать строку, которая будет использоваться при порождении URL. Строку нужно поместить сразу после вопросительного знака. Следующий маршрут похож на предыдущий, но порождает `/index.html` вместо `/index`, потому что как значение для порождения задана строка `.html`: - -```php -$router->addRoute('index', /* ... */); -``` - - -Встраивание -=========== - -Чтобы встроить созданный маршрутизатор в приложение, нам нужно сообщить о нём DI-контейнеру. Проще всего подготовить фабрику, которая создаст объект маршрутизатора, и сказать контейнеру в конфигурации использовать её. Допустим, для этого мы пишем метод `App\Core\RouterFactory::createRouter()`: - -```php -namespace App\Core; - -use Nette\Application\Routers\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute(/* ... */); - return $router; - } -} -``` - -Затем мы пишем в [конфигурации |dependency-injection:services]: - -```neon -services: - - App\Core\RouterFactory::createRouter -``` - -Любые зависимости, например от базы данных, передаются в фабричный метод его параметрами через [autowiring|dependency-injection:autowiring]: - -```php -public static function createRouter(Nette\Database\Connection $db): RouteList -{ - // ... -} -``` - - -SimpleRouter -============ - -Гораздо более простой маршрутизатор, чем набор маршрутов, - [SimpleRouter |api:Nette\Application\Routers\SimpleRouter]. Мы используем его, когда у нас нет особых требований к формату URL, если `mod_rewrite` (или его аналоги) недоступен либо если мы пока не хотим возиться с красивыми URL. - -Он порождает адреса примерно такого вида: - -``` -http://example.com/?presenter=Product&action=detail&id=123 -``` - -Параметром конструктора `SimpleRouter` служит презентер и действие по умолчанию, то есть действие, которое выполнится, если мы откроем, например, `http://example.com/` без дополнительных параметров. - -```php -// презентером по умолчанию будет 'Home', а действием 'default' -$router = new Nette\Application\Routers\SimpleRouter('Home:default'); -``` - -Мы рекомендуем задавать SimpleRouter прямо в [конфигурации |dependency-injection:services]: - -```neon -services: - - Nette\Application\Routers\SimpleRouter('Home:default') -``` - - -SEO и канонизация -================= - -Фреймворк способствует SEO (поисковой оптимизации), предотвращая существование одинакового содержимого под разными URL. Если к определённой цели ведут несколько адресов, например `/index` и `/index.html`, фреймворк объявляет первый основным (каноническим) и перенаправляет остальные на него HTTP-кодом 301. Благодаря этому поисковые системы не индексируют страницы дважды и не размывают их вес. - -Этот процесс называется канонизацией. Канонический URL - тот, который порождает маршрутизатор, то есть первый подходящий маршрут в наборе без флага OneWay. Поэтому в наборе мы перечисляем **основные маршруты первыми**. - -Канонизацию выполняет презентер, подробнее в главе [канонизация |presenters#Канонизация]. - - -HTTPS -===== - -Чтобы использовать протокол HTTPS, нужно включить его на хостинге и правильно настроить сервер. - -Перенаправление всего сайта на HTTPS должно быть задано на уровне сервера, например через файл `.htaccess` в корневом каталоге нашего приложения, с HTTP-кодом 301. Настройки могут отличаться в зависимости от хостинга и выглядеть примерно так: - -``` - - RewriteEngine On - ... - RewriteCond %{HTTPS} off - RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] - ... - -``` - -Маршрутизатор порождает URL с тем же протоколом, по которому была загружена страница, поэтому больше ничего задавать не нужно. - -Однако если нам в виде исключения нужно, чтобы разные маршруты работали по разным протоколам, мы указываем это в маске маршрута: - -```php -// Будет порождать HTTP-адрес -$router->addRoute('http://%host%//', /* ... */); - -// Будет порождать HTTPS-адрес -$router->addRoute('https://%host%//', /* ... */); -``` - - -Отладка маршрутизатора -====================== - -Панель маршрутизации, отображаемая в [Tracy Bar |tracy:], - полезный помощник, показывающий список маршрутов, а также параметры, которые маршрутизатор получил из URL. - -Зелёная полоса с символом ✓ обозначает маршрут, который обработал текущий URL; синий цвет и символ ≈ указывают маршруты, которые тоже обработали бы URL, если бы зелёный их не опередил. Далее мы видим текущие презентер и действие. - -[* routing-debugger.webp *] - -Кроме того, если из-за [канонизации |#SEO и канонизация] происходит неожиданное перенаправление, полезно посмотреть на панель в полосе *redirect*, где можно выяснить, как маршрутизатор изначально понял URL и почему он перенаправил. - -.[note] -При отладке маршрутизатора мы рекомендуем открыть инструменты разработчика в браузере (Ctrl+Shift+I или Cmd+Option+I) и отключить кеш в панели Network, чтобы перенаправления в нём не сохранялись. - - -Производительность -================== - -Число маршрутов влияет на скорость работы маршрутизатора. Их число точно не должно превышать нескольких десятков. Если у вашего сайта слишком сложная структура URL, вы можете написать собственный [#Собственный маршрутизатор]. - -Если у маршрутизатора нет зависимостей, например от базы данных, а его фабрика не принимает аргументов, мы можем сериализовать его скомпилированный вид прямо в DI-контейнер и тем самым немного ускорить приложение. - -```neon -routing: - cache: true -``` - - -Собственный маршрутизатор -========================= - -Следующие строки предназначены очень продвинутым пользователям. Вы можете создать собственный маршрутизатор и естественным образом встроить его в набор маршрутов. Маршрутизатор - реализация интерфейса [api:Nette\Routing\Router] с двумя методами: - -```php -use Nette\Http\IRequest as HttpRequest; -use Nette\Http\UrlScript; - -class MyRouter implements Nette\Routing\Router -{ - public function match(HttpRequest $httpRequest): ?array - { - // ... - } - - public function constructUrl(array $params, UrlScript $refUrl): ?string - { - // ... - } -} -``` - -Метод `match` обрабатывает текущий запрос [$httpRequest |http:request], из которого можно получить не только URL, но и заголовки и прочее, в массив с именем презентера и его параметрами. Если он не может обработать запрос, он возвращает null. При обработке запроса мы должны вернуть как минимум презентер; действие необязательно и по умолчанию равно `default`, если не указано. Имя презентера полное и включает все модули: - -```php -[ - 'presenter' => 'Front:Home', - 'action' => 'default', -] -``` - -Метод `constructUrl`, наоборот, собирает итоговый абсолютный URL из массива параметров. Он может использовать сведения из параметра [`$refUrl`|api:Nette\Http\UrlScript], которым служит текущий URL. - -Добавьте его в набор маршрутов методом `add()`: - -```php -$router = new Nette\Application\Routers\RouteList; -$router->add($myRouter); -$router->addRoute(/* ... */); -// ... -``` - - -Самостоятельное использование -============================= - -Под самостоятельным использованием мы понимаем применение возможностей маршрутизатора в приложении, которое не использует Nette Application и презентеры. К нему относится почти всё, что мы показали в этой главе, со следующими отличиями: - -- для наборов маршрутов мы используем класс [api:Nette\Routing\RouteList] -- как простой маршрутизатор - класс [api:Nette\Routing\SimpleRouter] -- поскольку пары `Презентер:действие` не существует, мы используем [#Расширенная запись] - -Итак, снова создаём метод, который соберёт нам маршрутизатор, например: - -```php -namespace App\Core; - -use Nette\Routing\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute('rss.xml', [ - 'controller' => 'RssFeedController', - ]); - $router->addRoute('article/', [ - 'controller' => 'ArticleController', - ]); - // ... - return $router; - } -} -``` - -Если вы используете DI-контейнер, что мы и рекомендуем, снова добавьте метод в конфигурацию, а затем получите маршрутизатор вместе с HTTP-запросом из контейнера: - -```php -$router = $container->getByType(Nette\Routing\Router::class); -$httpRequest = $container->getByType(Nette\Http\IRequest::class); -``` - -Или создайте объекты напрямую: - -```php -$router = App\Core\RouterFactory::createRouter(); -$httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); -``` - -Теперь остаётся дать маршрутизатору сделать свою работу: - -```php -$params = $router->match($httpRequest); -if ($params === null) { - // подходящий маршрут не найден, отправляем ошибку 404 - exit; -} - -// обрабатываем полученные параметры -$controller = $params['controller']; -// ... -``` - -И наоборот, используйте маршрутизатор для сборки ссылки: - -```php -$params = ['controller' => 'ArticleController', 'id' => 123]; -$url = $router->constructUrl($params, $httpRequest->getUrl()); -``` - - -{{composer: nette/routing}} -{{repo: nette/routing}} -{{api: https://api.nette.org/routing/}} diff --git a/application/ru/templates.texy b/application/ru/templates.texy deleted file mode 100644 index 89fcead967..0000000000 --- a/application/ru/templates.texy +++ /dev/null @@ -1,419 +0,0 @@ -Шаблоны -******* - -.[perex] -Nette использует систему шаблонов [Latte |latte:]. Latte выбран потому, что это самая безопасная система шаблонов для PHP и одновременно самая интуитивная. Учить много нового не нужно: достаточно знания PHP и нескольких тегов. - -Обычно страница состоит из шаблона макета и шаблона конкретного действия. Вот как может выглядеть шаблон макета; обратите внимание на блоки `{block}` и тег `{include}`: - -```latte - - - - {block title}My App{/block} - - -
    ...
    - {include content} -
    ...
    - - -``` - -А вот шаблон действия: - -```latte -{block title}Homepage{/block} - -{block content} -

    Homepage

    -... -{/block} -``` - -Он задаёт блок `content`, который вставляется на место `{include content}` в макете, а также переопределяет блок `title`, перекрывающий `{block title}` в макете. Попробуйте представить результат. - - -Поиск шаблона -------------- - -В презентерах вам не нужно указывать, какой шаблон отрисовать: фреймворк выводит путь автоматически и избавляет вас от его написания. - -Если вы используете структуру каталогов, где у каждого презентера свой каталог, просто положите шаблон в этот каталог под именем действия (то есть представления). Например, для действия `default` используйте шаблон `default.latte`: - -/--pre -app/ -└── Presentation/ - └── Home/ - ├── HomePresenter.php - └── default.latte -\-- - -Если вы используете структуру, где презентеры лежат вместе в одном каталоге, а шаблоны в папке `templates`, сохраните его либо в файле `..latte`, либо `/.latte`: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── Home/ - │ └── default.latte ← 1-й вариант - └── Home.default.latte ← 2-й вариант -\-- - -Каталог `templates` можно поместить и на уровень выше, то есть рядом с каталогом классов презентеров. - -Если шаблон не найден, презентер отвечает [ошибкой 404 - страница не найдена |presenters#Ошибка 404 и подобные]. - -Изменить представление можно через `$this->setView('otherView')`. Можно и прямо указать файл шаблона через `$this->template->setFile('/path/to/template.latte')`. - -.[note] -Файлы, в которых ищутся шаблоны, можно изменить, переопределив метод [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], возвращающий массив возможных имён файлов. - - -Поиск шаблона макета --------------------- - -Nette также автоматически ищет файл макета. - -Если вы используете структуру каталогов, где у каждого презентера свой каталог, поместите макет либо в папку с презентером, если он относится только к нему, либо на уровень выше, если он общий для нескольких презентеров: - -/--pre -app/ -└── Presentation/ - ├── @layout.latte ← общий макет - └── Home/ - ├── @layout.latte ← только для презентера Home - ├── HomePresenter.php - └── default.latte -\-- - -Если вы используете структуру, где презентеры собраны в одном каталоге, а шаблоны в папке `templates`, макет будет ожидаться в этих местах: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── @layout.latte ← общий макет - ├── Home/ - │ └── @layout.latte ← только для Home, 1-й вариант - └── Home.@layout.latte ← только для Home, 2-й вариант -\-- - -Если презентер находится в модуле, поиск идёт и выше по уровням каталогов, соответственно вложенности модулей. - -Имя макета можно изменить через `$this->setLayout('layoutAdmin')`, и тогда он будет ожидаться в файле `@layoutAdmin.latte`. Можно и прямо указать файл шаблона макета через `$this->setLayout('/path/to/template.latte')`. - -Вызов `$this->setLayout(false)` или тег `{layout none}` внутри шаблона отключают поиск макета. - -.[note] -Файлы, в которых ищутся шаблоны макетов, можно изменить, переопределив метод [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], возвращающий массив возможных имён файлов. - - -Переменные шаблона ------------------- - -Переменные передаются в шаблоны записью в `$this->template`. Затем они становятся доступны в шаблоне как локальные переменные: - -```php -$this->template->article = $this->articles->getById($id); -``` - -Чтобы значение свойства автоматически передавалось в шаблон как переменная, пометьте его атрибутом `#[TemplateVariable]` и сделайте публичным: .{data-version:3.2.9} - -```php -use Nette\Application\Attributes\TemplateVariable; - -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - #[TemplateVariable] - public string $siteName = 'My blog'; -} -``` - -Если вы передадите в шаблон переменную с тем же именем, `#[TemplateVariable]` её не перебьёт. - - -Переменные по умолчанию ------------------------ - -Презентеры и компоненты автоматически передают в шаблоны несколько полезных переменных: - -- `$basePath` - абсолютный путь URL к корневому каталогу (например, `/eshop`) -- `$baseUrl` - абсолютный URL корневого каталога (например, `http://localhost/eshop`) -- `$user` - объект, [представляющий пользователя |security:authentication] -- `$presenter` - текущий презентер -- `$control` - текущий компонент или презентер -- `$flashes` - массив [сообщений |presenters#Flash-сообщения], отправленных функцией `flashMessage()` - -Если вы используете собственный класс шаблона, эти переменные передаются, если вы создадите для них свойства. - - -Типобезопасные шаблоны ----------------------- - -При разработке надёжных приложений полезно явно определить, какие переменные ожидает шаблон и какого они типа. Это даёт проверку типов в PHP, умные подсказки в IDE и позволяет статическому анализу отлавливать ошибки. - -Как определить такой список? Просто как класс со свойствами, представляющими переменные шаблона. Назовите его похоже на презентер, только с `Template` на конце: - -```php -/** - * @property-read ArticleTemplate $template - */ -class ArticlePresenter extends Nette\Application\UI\Presenter -{ -} - -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - public Model\Article $article; - public Nette\Security\User $user; - - // и другие переменные -} -``` - -Объект `$this->template` в презентере теперь будет экземпляром класса `ArticleTemplate`. PHP таким образом проверит объявленные типы при записи. - -Класс шаблона Nette выбирает автоматически. Сначала он ищет класс с именем `Template`, например `ArticleEditTemplate` для действия `edit`, и только если такого нет, переходит к `Template`. - -Аннотация `@property-read` предназначена для IDE и статического анализа, она включает дополнение кода, см. "PhpStorm и дополнение кода для $this⁠-⁠>⁠template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template. - -[* phpstorm-completion.webp *] - -Дополнение кода можно использовать и прямо в шаблонах. Достаточно установить плагин Latte для PhpStorm и указать в начале шаблона имя класса параметров шаблона, подробнее в главе [Latte: система типов |latte:type-system]: - -```latte -{templateType App\Presentation\Article\ArticleTemplate} -... -``` - -То же относится и к компонентам. Достаточно придерживаться соглашения об именовании и создать класс параметров `FifteenTemplate` для компонента вида `FifteenControl`. - -Если вам нужно использовать другой класс параметров, воспользуйтесь методом `createTemplate()`: - -```php -public function renderDefault(): void -{ - $template = $this->createTemplate(SpecialTemplate::class); - $template->foo = 123; - // ... - $this->sendTemplate($template); -} -``` - -.{data-version:3.3.0} -Если вам нужно повлиять на то, как шаблон завершается перед отрисовкой, например добавить переменные, общие для всех действий, вы можете переопределить в презентере метод `completeTemplate()`. Он вызывается прямо перед отрисовкой шаблона: - -```php -protected function completeTemplate(Nette\Application\UI\Template $template): void -{ - parent::completeTemplate($template); - $template->siteName = 'My blog'; -} -``` - - -Создание ссылок ---------------- - -В шаблоне ссылки на другие презентеры и действия создаются так: - -```latte -product detail -``` - -Атрибут `n:href` очень удобен для HTML-тегов ``. Если мы хотим вывести ссылку в другом месте, например в тексте, мы используем `{link}`: - -```latte -URL is: {link Home:default} -``` - -Подробнее в главе [Создание URL-ссылок|creating-links]. - - -Собственные фильтры, теги и прочее ----------------------------------- - -Систему шаблонов Latte можно расширять собственными фильтрами, функциями, тегами и другими элементами. Есть три подхода, от быстрых разовых решений до архитектурных приёмов для целых приложений. - -**Разово в методах презентера** - -Самый быстрый подход - добавить фильтры или функции прямо в код презентера или компонента. В презентерах для этого хорошо подходят методы `beforeRender()` или `render()`: - -```php -protected function beforeRender(): void -{ - // добавляем фильтр - $this->template->addFilter('money', fn($val) => '$' . number_format($val, 2)); - - // добавляем функцию - $this->template->addFunction('isWeekend', fn($date) => $date->format('N') >= 6); -} -``` - -В шаблоне: - -```latte -

    Price: {$price|money}

    - -{if isWeekend($now)} ... {/if} -``` - -Для более сложной логики можно настроить объект `Latte\Engine` напрямую: - -```php -protected function beforeRender(): void -{ - $latte = $this->template->getLatte(); - $latte->setFeature(Latte\Feature::MigrationWarnings); -} -``` - -**С помощью атрибутов** - -Более изящный подход - определить фильтры и функции методами прямо в [классе параметров шаблона|#Типобезопасные шаблоны] презентера или компонента, пометив их атрибутами: - -```php -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - #[Latte\Attributes\TemplateFilter] - public function money(float $val): string - { - return '$' . number_format($val, 2); - } - - #[Latte\Attributes\TemplateFunction] - public function isWeekend(DateTimeInterface $date): bool - { - return $date->format('N') >= 6; - } -} -``` - -Latte автоматически обнаруживает и регистрирует методы, помеченные этими атрибутами. Имя фильтра или функции в шаблонах совпадает с именем метода. Эти методы должны быть публичными. - -**Глобально через расширения** - -Предыдущие подходы годятся для фильтров и функций, нужных только в отдельных презентерах или компонентах, а не во всём приложении. Для всего приложения лучше всего создать [расширение |latte:extending-latte#Расширение Latte]. Этот класс собирает все расширения Latte вашего проекта в одном месте. Краткий пример: - -```php -namespace App\Presentation\Accessory; - -final class LatteExtension extends Latte\Extension -{ - public function __construct( - private App\Model\Facade $facade, - private Nette\Security\User $user, - // ... - ) { - } - - public function getFilters(): array - { - return [ - 'timeAgoInWords' => $this->filterTimeAgoInWords(...), - 'money' => $this->filterMoney(...), - // ... - ]; - } - - public function getFunctions(): array - { - return [ - 'canEditArticle' => - fn($article) => $this->facade->canEditArticle($article, $this->user->getId()), - // ... - ]; - } - - private function filterTimeAgoInWords(DateTimeInterface $time): string - { - // ... - } - - // ... -} -``` - -Зарегистрируйте расширение через [конфигурацию |configuration#Шаблоны Latte]: - -```neon -latte: - extensions: - - App\Presentation\Accessory\LatteExtension -``` - -Расширения дают несколько преимуществ: поддержку внедрения зависимостей, доступ к слою модели вашего приложения и управление всеми расширениями из одного места. Они поддерживают и собственные теги, провайдеры, проходы компилятора и прочее. - - -Настройка всех шаблонов ------------------------ - -Сервис `TemplateFactory`, создающий все шаблоны, предлагает публичный массив callback-функций `$onCreate`. Они вызываются каждый раз при создании любого шаблона, поэтому вы можете из одного места настроить фильтры, функции или переменные для всех шаблонов приложения. Каждая callback-функция получает только что созданный шаблон. Получите сервис `TemplateFactory` [через внедрение |dependency-injection:passing-dependencies] и зарегистрируйте callback-функции, например при старте приложения: - -```php -$templateFactory->onCreate[] = function (Nette\Bridges\ApplicationLatte\Template $template): void { - $template->addFilter('money', fn($val) => '$' . number_format($val, 2)); -}; -``` - - -Перевод -------- - -Если вы разрабатываете многоязычное приложение, вам, скорее всего, понадобится выводить в шаблоне часть текстов на разных языках. Для этого Nette Framework определяет интерфейс перевода [api:Nette\Localization\Translator] с единственным методом `translate()`. Он принимает сообщение `$message`, которым обычно служит строка, и любые другие параметры. Задача - вернуть переведённую строку. Реализации по умолчанию в Nette нет; вы можете выбрать из нескольких готовых решений, доступных на [Componette |https://componette.org/search/localization], по своим потребностям. Их документация объясняет, как настроить переводчик. - -Шаблонам можно задать переводчик, который мы [получаем через внедрение |dependency-injection:passing-dependencies], методом `setTranslator()`: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator); -} -``` - -Как вариант, переводчик можно задать через [конфигурацию |configuration#Шаблоны Latte]: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -Затем переводчик можно использовать, например, как фильтр `|translate`, в том числе с дополнительными параметрами, которые передаются в метод `translate()` (см. `foo, bar`): - -```latte -
    {='Корзина'|translate} -{$item|translate} -{$item|translate, foo, bar} -``` - -Или как тег с подчёркиванием: - -```latte -{_'Корзина'} -{_$item} -{_$item, foo, bar} -``` - -Для перевода участка шаблона есть парный тег `{translate}` (начиная с Latte 2.11, раньше использовался тег `{_}`): - -```latte -{translate}Заказ{/translate} -{translate foo, bar}Заказ{/translate} -``` - -Переводчик обычно вызывается во время выполнения, при отрисовке шаблона. Однако Latte версии 3 умеет переводить все статические тексты уже во время компиляции шаблона. Это экономит производительность, потому что каждая строка переводится только один раз, а готовый перевод записывается в скомпилированный вид. При этом в каталоге кеша появляется несколько скомпилированных версий шаблона, по одной на каждый язык. Для этого достаточно указать язык вторым параметром: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator, $lang); -} -``` - -Под статическим текстом понимается, например, `{_'hello'}` или `{translate}hello{/translate}`. Нестатические тексты вроде `{_$foo}` по-прежнему будут переводиться во время выполнения. diff --git a/application/ru/upgrading.texy b/application/ru/upgrading.texy deleted file mode 100644 index a98a84b81c..0000000000 --- a/application/ru/upgrading.texy +++ /dev/null @@ -1,47 +0,0 @@ -Обновление -********** - - -Обновление до версии 3.0 -======================== - -Nette 3.0 добавляет объявления типов параметров и возвращаемых значений методов. Если вы переопределяете такой метод в классе, наследующем от Nette (например, в презентере или компоненте), вам нужно добавить те же объявления типов, иначе PHP выдаст ошибку "Declaration must be compatible". - -Интерфейс `Nette\Application\IRouter` изменился. Метод `match()` теперь возвращает, а `constructUrl()` принимает массив параметров вместо объекта `Nette\Application\Request`. - -Nette теперь проверяет, что каждый сигнал отправлен с того же источника (то есть с того же домена и поддомена). Эта политика одного источника - критически важный механизм безопасности, помогающий сократить возможные векторы атак. Если вы хотите разрешить другие источники, добавьте к методу-обработчику аннотацию `@crossOrigin`: - -```php -/** - * @crossOrigin - */ -public function handleXy(): void -{ -} -``` - -Это относится и к отправке форм. Если вы хотите разрешить отправку с других источников, сделайте так: - -```php -$form = new Nette\Application\UI\Form; -$form->allowCrossOrigin(); -``` - -Конструктор `Nette\ComponentModel\Component` не использовался годами и был удалён в версии 3.0. Это несовместимое изменение: если вы вызываете родительский конструктор в компоненте или презентере, наследующем от `Nette\Application\UI\Presenter`, вам нужно убрать этот вызов. - - -Обновление до версии 2.4 -======================== - -- `Route` и `SimpleRouter` теперь порождают ту же схему HTTP/HTTPS, по которой был открыт сайт. Маршрут, требующий определённого протокола, можно задать со схемой, например `Route('http://domain.cz/')`. -- Для параметров типа bool у методов render/action (то есть со значением по умолчанию true или false) и для постоянных параметров теперь различаются `false` и `null`. Если параметра нет в URL, его значением теперь будет `null` (раньше `false`). -- Класс, возвращаемый `Presenter::getReflection()`, больше не является потомком `Nette\Reflection\ClassType`, а `getReflection()->getMethod()` больше не является потомком `Nette\Reflection\Method`. -- Флаг `SECURED` и `Route::$defaultFlags` объявлены устаревшими. - - -Обновление до версии 2.3 -======================== - -- маршруты и имена презентеров **чувствительны к регистру**. Nette предупредит вас, если вы используете неверный регистр в имени презентера; из соображений производительности маска Route не проверяется, поэтому проверьте её вручную. -- `Route::addStyle()` и `Route::setStyleProperty()` объявлены устаревшими и теперь выдают `E_USER_DEPRECATED`. -- расширение шаблонов `.phtml` и старый синтаксис ссылок больше не поддерживаются. diff --git a/application/tr/@home.texy b/application/tr/@home.texy deleted file mode 100644 index 5b5f2f9b5a..0000000000 --- a/application/tr/@home.texy +++ /dev/null @@ -1,85 +0,0 @@ -Nette Application -***************** - -.[perex] -Nette Application, Nette framework'ünün çekirdeğidir ve modern web uygulamaları oluşturmak için güçlü araçlar sunar. Geliştirmeyi belirgin biçimde kolaylaştıran, kodun güvenliğini ve bakımını iyileştiren bir dizi olağanüstü özellik sağlar. - - -Kurulum -------- - -Kütüphaneyi [Composer|best-practices:composer] ile indirin ve kurun: - -```shell -composer require nette/application -``` - - -Neden Nette Application? ------------------------- - -Nette web teknolojilerinde her zaman öncü olmuştur. - -**Çift yönlü router:** Nette, çift yönlü olmasıyla eşsiz olan gelişmiş bir yönlendirme sistemine sahiptir; URL'leri yalnızca uygulama eylemlerine çevirmekle kalmaz, tersine URL de üretebilir. Bu şu anlama gelir: -- Şablonları değiştirmeye gerek kalmadan tüm uygulamanın URL yapısını istediğiniz zaman değiştirebilirsiniz -- URL'ler otomatik olarak kanonikleştirilir, bu da SEO'yu iyileştirir -- Yönlendirme tek bir yerde tanımlanır, anotasyonlara dağılmaz - -**Bileşenler ve sinyaller:** Delphi ve React.js'ten esinlenen yerleşik bileşen sistemi PHP framework'leri arasında eşsizdir: -- Yeniden kullanılabilir UI elemanları oluşturmayı sağlar -- Hiyerarşik bileşen kompozisyonunu destekler -- Sinyaller sayesinde AJAX isteklerinin zarif bir şekilde işlenmesini sunar -- [Componette](https://componette.org) üzerinde zengin bir hazır bileşen kütüphanesi - -**AJAX ve snippet'ler:** Nette, AJAX ile çalışmanın devrimsel bir yolunu 2009'da, Ruby on Rails için Hotwire veya Symfony UX Turbo gibi benzer çözümlerden çok önce tanıttı: -- Snippet'ler, JavaScript yazmaya gerek kalmadan sayfanın yalnızca bir bölümünü güncellemeyi sağlar -- Bileşen sistemiyle otomatik entegrasyon -- Sayfa bölümlerinin akıllı biçimde geçersiz kılınması -- En az veri aktarımı - -**Sezgisel [Latte|latte:] şablonları:** PHP için en güvenli şablon sistemi, gelişmiş özelliklerle: -- Bağlama duyarlı kaçış ile otomatik XSS koruması -- Özel filtreler, fonksiyonlar ve etiketlerle genişletilebilir -- Şablon kalıtımı ve AJAX için snippet'ler -- Tip sistemiyle mükemmel PHP 8.x desteği - -**Bağımlılık enjeksiyonu:** Nette, bağımlılık enjeksiyonundan tam olarak yararlanır: -- Bağımlılıkların otomatik aktarımı (autowiring) -- Anlaşılır NEON formatıyla yapılandırma -- Bileşen factory'leri için destek - - -Başlıca avantajlar ------------------- - -- **Güvenlik**: XSS, CSRF vb. [güvenlik açıklarına|nette:vulnerability-protection] karşı otomatik koruma -- **Verimlilik**: Akıllı tasarım sayesinde daha az yazı, daha çok özellik -- **Hata ayıklama**: Yönlendirme paneline sahip [Tracy hata ayıklayıcı|tracy:] -- **Performans**: Akıllı önbellek, bileşenlerin tembel yüklenmesi -- **Esneklik**: Uygulama tamamlandıktan sonra bile URL'leri kolayca değiştirme -- **Bileşenler**: Yeniden kullanılabilir UI elemanlarının eşsiz sistemi -- **Modern**: PHP 8.3+ ve tip sistemi için tam destek - - -Başlarken ---------- - -1. [Uygulamalar nasıl çalışır? |how-it-works] - Temel mimariyi anlamak -2. [Presenter'lar |presenters] - Presenter'lar ve eylemlerle çalışmak -3. [Şablonlar |templates] - Latte'de şablon oluşturmak -4. [Yönlendirme |routing] - URL adreslerini yapılandırmak -5. [Etkileşimli bileşenler |components] - Bileşen sistemini kullanmak - - -PHP uyumluluğu --------------- - -| sürüm | uyumlu olduğu PHP -|-----------------------|------------------- -| Nette Application 3.3 | PHP 8.3 - 8.5 -| Nette Application 3.2 | PHP 8.1 - 8.5 -| 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 - -En son yama sürümleri için geçerlidir. diff --git a/application/tr/@left-menu.texy b/application/tr/@left-menu.texy deleted file mode 100644 index 3a084f4197..0000000000 --- a/application/tr/@left-menu.texy +++ /dev/null @@ -1,24 +0,0 @@ -Nette Application -***************** -- [Genel bakış |@home] -- [Uygulamalar nasıl çalışır? |how-it-works] -- [Bootstrapping|bootstrapping] -- [Presenter'lar|presenters] -- [Şablonlar|templates] -- [Dizin yapısı |directory-structure] -- [Yönlendirme|routing] -- [URL bağlantıları oluşturma |creating-links] -- [Etkileşimli bileşenler |components] -- [AJAX ve snippet'ler |ajax] -- [Multiplier |multiplier] -- [Yapılandırma|configuration] -- [Yükseltme|upgrading] - - -İleri okuma -*********** -- [Neden Nette kullanmalı?|www:10-reasons-why-nette] -- [Kurulum |nette:installation] -- [İlk uygulamanızı oluşturun! |quickstart:] -- [En iyi uygulamalar |best-practices:] -- [Sorun giderme |nette:troubleshooting] diff --git a/application/tr/@meta.texy b/application/tr/@meta.texy deleted file mode 100644 index 8dfe82f311..0000000000 --- a/application/tr/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokümantasyonu}} diff --git a/application/tr/ajax.texy b/application/tr/ajax.texy deleted file mode 100644 index 68afb4e5cb..0000000000 --- a/application/tr/ajax.texy +++ /dev/null @@ -1,265 +0,0 @@ -AJAX ve snippet'ler -******************* - -
    - -İşlevselliğin çoğu zaman sunucu ile tarayıcı arasında dağıtıldığı modern web uygulamaları çağında AJAX, vazgeçilmez bir bağlayıcı unsurdur. Nette Framework bu alanda hangi olanakları sunuyor? -- snippet denilen şablon parçalarının gönderilmesi -- PHP ile JavaScript arasında değişken aktarımı -- AJAX isteklerini hata ayıklama araçları - -
    - - -AJAX isteği -=========== - -Bir AJAX isteği, klasik bir HTTP isteğinden temelde farklı değildir. Belirli parametrelerle bir presenter çağrılır. İsteğe nasıl yanıt vereceğine presenter karar verir; veriyi JSON formatında döndürebilir, bir HTML kodu parçası, bir XML belgesi vb. gönderebilir. - -Tarayıcı tarafında AJAX isteğini `fetch()` fonksiyonuyla başlatırız: - -```js -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -.then(response => response.json()) -.then(payload => { - // yanıtı işle -}); -``` - -Sunucu tarafında AJAX isteği, [HTTP isteğini kapsülleyen |http:request] servisin `$httpRequest->isAjax()` metoduyla tanınır. Algılama için `X-Requested-With` HTTP header'ını kullanır, bu yüzden onu göndermek çok önemlidir. Presenter içinde `$this->isAjax()` metodunu kullanabilirsiniz. - -Veriyi JSON formatında göndermek isterseniz [`sendJson()` |presenters#Yanıt gönderme] metodunu kullanın. Bu metot presenter'ın çalışmasını da sonlandırır. - -```php -public function actionExport(): void -{ - $this->sendJson($this->model->getData()); -} -``` - -AJAX için tasarlanmış özel bir şablonla yanıt vermeyi planlıyorsanız şöyle yapabilirsiniz: - -```php -public function handleClick($param): void -{ - if ($this->isAjax()) { - $this->template->setFile('path/to/ajax.latte'); - } - // ... -} -``` - - -Snippet'ler -=========== - -Nette'in sunucuyu istemciyle birleştirmek için sunduğu en güçlü araç snippet'lerdir. Onlarla sıradan bir uygulamayı en az çabayla ve yalnızca birkaç satır kodla AJAX uygulamasına dönüştürebilirsiniz. Bunun nasıl çalıştığını Fifteen örneği gösterir; kodunu [GitHub |https://github.com/nette-examples/fifteen] üzerinde bulabilirsiniz. - -Snippet'ler, tüm sayfayı yeniden yüklemek yerine yalnızca sayfanın bölümlerini güncellemenizi sağlar. Bu yalnızca daha hızlı ve verimli olmakla kalmaz, aynı zamanda daha rahat bir kullanıcı deneyimi sunar. Snippet'ler size Ruby on Rails için Hotwire'ı veya Symfony UX Turbo'yu hatırlatabilir. İlginçtir ki Nette snippet'leri 14 yıl önce tanıttı. - -Snippet'ler nasıl çalışır? Sayfa ilk yüklendiğinde (AJAX olmayan istek) tüm sayfa, tüm snippet'ler dahil, yüklenir. Kullanıcı sayfayla etkileşime girdiğinde (örneğin bir düğmeye tıkladığında, bir formu gönderdiğinde vb.), tüm sayfayı yeniden yüklemek yerine bir AJAX isteği başlatılır. Presenter'daki kod eylemi gerçekleştirir ve hangi snippet'lerin güncellenmesi gerektiğine karar verir. Nette bu snippet'leri render eder ve snippet'leri içeren bir dizi taşıyan JSON payload'ı olarak gönderir. Tarayıcıdaki işleyici kod, aldığı snippet'leri sayfaya geri yerleştirir. Böylece yalnızca değişen snippet'lerin kodu aktarılır, bu da tüm sayfa içeriğini aktarmaya kıyasla bant genişliğinden tasarruf sağlar ve yüklemeyi hızlandırır. `redrawControl()` ile hiçbir snippet geçersiz kılınmazsa, Nette AJAX isteğinde de tüm sayfayı döndürür; snippet'ler yalnızca bir şey geçersiz kılındığında gönderilir. - - -Naja ----- - -Snippet'leri tarayıcı tarafında işlemek için [Naja kütüphanesi |https://naja.js.org] kullanılır. Onu bir Node.js paketi olarak [kurun |https://naja.js.org/#/guide/01-install-setup-naja] (Webpack, Rollup, Vite, Parcel gibi paketleyicilerle kullanmak için): - -```shell -npm install naja -``` - -…ya da doğrudan sayfa şablonuna ekleyin: - -```latte - -``` - -Önce 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ı (sinyali) veya form gönderimini AJAX isteğine dönüştürmek için ilgili bağlantıyı, formu veya düğmeyi `ajax` sınıfıyla işaretlemeniz yeterlidir: - -```latte -Git - -
    - -
    - -veya - -
    - -
    -``` - - -Snippet'lerin yeniden çizilmesi -------------------------------- - -[Control |components] sınıfının her nesnesi (Presenter'ın kendisi dahil), yeniden çizilmesini gerektiren değişikliklerin olup olmadığını izler. Bunun için `redrawControl()` metodu kullanılır: - -```php -public function handleLogin(string $user): void -{ - // girişten sonra ilgili bölümün yeniden çizilmesi gerekir - $this->redrawControl(); - // ... -} -``` - -Nette, neyin yeniden çizileceği üzerinde daha da ince bir denetim sağlar. Metot, argüman olarak snippet'in adını kabul edebilir. Böylece şablon parçaları düzeyinde geçersiz kılmak (yani yeniden çizilmeye zorlamak) mümkündür. Tüm bileşen geçersiz kılınırsa, içindeki her snippet de yeniden çizilir: - -```php -// 'header' snippet'ini geçersiz kılar -$this->redrawControl('header'); -``` - -Bekleyen bir geçersiz kılmayı ikinci parametre `$redraw` ile iptal de edebilirsiniz: `$this->redrawControl('header', redraw: false)` çağrısı, snippet'i yeniden çizilmesi gerekmeyen olarak işaretler. Tam imza `redrawControl(?string $snippet = null, bool $redraw = true)` şeklindedir. - - -Latte'de snippet'ler --------------------- - -Latte'de snippet kullanmak son derece kolaydır. Şablonun bir bölümünü snippet olarak tanımlamak için onu `{snippet}` ve `{/snippet}` etiketleriyle sarmanız yeterlidir: - -```latte -{snippet header} -

    Merhaba ...

    -{/snippet} -``` - -Snippet, HTML sayfasında özel olarak üretilmiş bir `id`'ye sahip bir `
    ` elemanı oluşturur. Snippet yeniden çizildiğinde bu elemanın içeriği güncellenir. Bu yüzden sayfa ilk render edildiğinde tüm snippet'lerin de render edilmesi gerekir, başlangıçta boş olabilseler bile. - -Snippet'i `
    ` dışında bir elemanla, n:niteliği kullanarak da oluşturabilirsiniz: - -```latte -
    -

    Merhaba ...

    -
    -``` - - -Snippet alanları ----------------- - -Snippet adları ifade de olabilir: - -```latte -{foreach $items as $id => $item} -
  • {$item}
  • -{/foreach} -``` - -Tek başına bu, işlevsiz bir ara adımdır: statik bir `{snippet}` veya `{snippetArea}` dışında render edilen dinamik bir snippet, *Dynamic snippets are allowed only inside static snippet/snippetArea.* mesajıyla `E_USER_WARNING` tetikler. Bunu aşağıda düzeltiyoruz. - -Bu, `item-0`, `item-1` gibi birkaç snippet oluşturur. Dinamik bir snippet'i doğrudan geçersiz kılsaydık (örneğin `item-1`), hiçbir şey yeniden çizilmezdi. Bunun nedeni, snippet'lerin gerçekten alıntı gibi çalışması ve yalnızca kendilerinin doğrudan render edilmesidir. Oysa şablonda teknik olarak `item-1` adlı bir snippet yoktur. O ancak snippet'i çevreleyen kod, yani foreach döngüsü, çalıştırıldığında var olur. Bu yüzden şablonun çalıştırılması gereken bölümünü `{snippetArea}` etiketiyle işaretleriz: - -```latte -
      - {foreach $items as $id => $item} -
    • {$item}
    • - {/foreach} -
    -``` - -Ve hem tek tek snippet'in hem de tüm üst alanın yeniden çizilmesini isteriz: - -```php -$this->redrawControl('itemsContainer'); -$this->redrawControl('item-1'); -``` - -Aynı zamanda `$items` dizisinin yalnızca yeniden çizilmesi gereken öğeleri içermesini sağlamak yerinde olur. - -Ana şablona snippet içeren başka bir şablonu `{include}` etiketiyle dahil edersek, şablon dahil etmeyi yine bir `snippetArea` içine sarmak ve onu snippet'le birlikte geçersiz kılmak gerekir: - -```latte -{snippetArea include} - {include 'included.latte'} -{/snippetArea} -``` - -```latte -{* included.latte *} -{snippet item} - ... -{/snippet} -``` - -```php -$this->redrawControl('include'); -$this->redrawControl('item'); -``` - - -Bileşenlerde snippet'ler ------------------------- - -[Bileşenlerin|components] içinde snippet oluşturabilirsiniz ve Nette bunları otomatik olarak yeniden çizer. Ancak bir sınırlama vardır: snippet'leri yeniden çizmek için Nette `render()` metodunu parametresiz çağırır. Bu yüzden şablonda parametre aktarmak işe yaramaz: - -```latte -Tamam -{control productGrid} - -çalışmaz: -{control productGrid, $arg, $arg} -{control productGrid:paginator} -``` - - -Kendi verilerinizi gönderme ---------------------------- - -Snippet'lerle birlikte istemciye istediğiniz ek veriyi gönderebilirsiniz. Onları `payload` nesnesine yazmanız yeterlidir: - -```php -public function actionDelete(int $id): void -{ - // ... - if ($this->isAjax()) { - $this->payload->message = 'Başarılı'; - } -} -``` - - -Yeniden yönlendirme -------------------- - -AJAX isteği sırasında `redirect()` ve `redirectUrl()` metotları HTTP yeniden yönlendirmesi göndermez. Bunun yerine hedef URL'yi payload'a (AJAX yanıtında gönderilen veri nesnesine), yani onun `payload.redirect` özelliğine yazar ve gönderir; asıl yönlendirmeyi ise istemci tarafındaki kütüphane (Naja) gerçekleştirir. - - -Parametre aktarımı -================== - -Bir bileşene AJAX isteğiyle parametre gönderirken, ister sinyal parametreleri ister kalıcı parametreler olsun, istekte bileşenin adını da içeren genel adlarını belirtmemiz gerekir. `getParameterId()` metodu parametrenin tam adını döndürür. - -```js -let url = new URL({link //foo!}); -url.searchParams.set({$control->getParameterId('bar')}, bar); - -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -``` - -Ve bileşende ilgili parametrelere sahip handle metodu: - -```php -public function handleFoo(int $bar): void -{ -} -``` - - -İleri okuma -=========== - -- [Dinamik snippet'ler |best-practices:dynamic-snippets] diff --git a/application/tr/bootstrapping.texy b/application/tr/bootstrapping.texy deleted file mode 100644 index 90487846bb..0000000000 --- a/application/tr/bootstrapping.texy +++ /dev/null @@ -1,312 +0,0 @@ -Bootstrapping -************* - -
    - -Bootstrapping, uygulama ortamının hazırlanması, bağımlılık enjeksiyonu (DI) konteynerinin oluşturulması ve uygulamanın başlatılması sürecidir. Şunları ele alacağız: - -- Bootstrap sınıfının ortamı nasıl hazırladığını -- uygulamaların NEON dosyalarıyla nasıl yapılandırıldığını -- üretim ve geliştirme modunun nasıl ayırt edildiğini -- DI konteynerinin nasıl oluşturulup yapılandırıldığını - -
    - - -Uygulamalar, ister web tabanlı ister komut satırından çalıştırılan betikler olsun, çalışmalarına bir tür ortam hazırlığıyla başlar. Eskiden bundan, ilk dosyanın dahil ettiği belki `include.inc.php` adlı bir dosya sorumluydu. Modern Nette uygulamalarında onun yerini, uygulamanın bir parçası olarak `app/Bootstrap.php` dosyasında bulunan `Bootstrap` sınıfı aldı. Örneğin şöyle görünebilir: - -```php -namespace App; - -use Nette; -use Nette\Bootstrap\Configurator; - -class Bootstrap -{ - private Configurator $configurator; - private string $rootDir; - - public function __construct() - { - $this->rootDir = dirname(__DIR__); - // Configurator, uygulama ortamının ve servislerin kurulumundan sorumludur. - $this->configurator = new Configurator; - // Nette'in ürettiği geçici dosyalar (örneğin derlenmiş şablonlar) için dizini ayarla - $this->configurator->setTempDirectory($this->rootDir . '/temp'); - } - - public function bootWebApplication(): Nette\DI\Container - { - $this->initializeEnvironment(); - $this->setupContainer(); - return $this->configurator->createContainer(); - } - - private function initializeEnvironment(): void - { - // Nette akıllıdır ve geliştirme modu kendiliğinden 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: en iyi "İsviçre çakısı" hata ayıklama aracı. - $this->configurator->enableTracy($this->rootDir . '/log'); - - // RobotLoader: seçilen dizindeki tüm sınıfları otomatik yükler - $this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); - } - - private function setupContainer(): void - { - // Yapılandırma dosyalarını yükle - $this->configurator->addConfig($this->rootDir . '/config/common.neon'); - } -} -``` - - -index.php -========= - -Web uygulamalarında ilk dosya, [genel dizin |directory-structure#Genel dizin www/] `www/` içinde yer alan `index.php`'dir. Bootstrap sınıfına ortamı hazırlamasını ve DI konteynerini oluşturmasını söyler. Sonra konteynerden `Application` servisini alır ve o da web uygulamasını çalıştırır: - -```php -$bootstrap = new App\Bootstrap; -// Ortamı hazırla + DI konteynerini oluştur -$container = $bootstrap->bootWebApplication(); -// DI konteyneri bir Nette\Application\Application nesnesi oluşturur -$application = $container->getByType(Nette\Application\Application::class); -// Nette uygulamasını başlat ve gelen isteği işle -$application->run(); -``` - -.[note] -`$application` nesnesi, isteği işlerken [olaylar |nette:glossary#Olaylar] yayar: `onStartup`, `onRequest`, `onPresenter`, `onResponse`, `onShutdown` ve `onError` (işlenmemiş bir istisnada). Bunlara işleyici bağlayabilirsiniz; günlükleme veya uygulama geneli izleme için kullanışlıdır. - -Gördüğünüz gibi, ortamın kurulmasına ve bağımlılık enjeksiyonu (DI) konteynerinin oluşturulmasına [api:Nette\Bootstrap\Configurator] sınıfı yardım eder. Şimdi onu daha ayrıntılı tanıtacağız. - - -Geliştirme modu ve üretim modu -============================== - -Nette, bir geliştirme sunucusunda mı yoksa üretim sunucusunda mı çalıştığına göre farklı davranır: - -🛠️ Geliştirme modu: - - Yararlı bilgiler içeren Tracy hata ayıklama çubuğunu gösterir (SQL sorguları, çalışma süresi, kullanılan bellek) - - Hata durumunda, fonksiyon çağrılarını ve değişken içeriklerini gösteren ayrıntılı bir hata sayfası gösterir - - Latte şablonları, yapılandırma dosyaları vb. değiştiğinde önbelleği otomatik yeniler - - -🚀 Üretim modu: - - Hiçbir hata ayıklama bilgisi göstermez, tüm hatalar günlüğe yazılır - - Hata durumunda ErrorPresenter'ı veya genel bir "Server Error" sayfasını gösterir - - Önbellek asla otomatik yenilenmez! - - Hız ve güvenlik için iyileştirilmiştir - - -Mod seçimi otomatik algılamayla yapılır, bu yüzden genellikle bir şey yapılandırmaya veya modu elle değiştirmeye gerek yoktur: - -- geliştirme modu: localhost'ta (IP adresi `127.0.0.1` veya `::1`), proxy yoksa (yani HTTP header'ı algılanmıyorsa) -- üretim modu: diğer her yerde - -Geliştirme modunu başka durumlarda da etkinleştirmek istersek, örneğin belirli bir IP adresinden erişen programcılar için, `setDebugMode()` kullanırız: - -```php -$this->configurator->setDebugMode('23.75.345.200'); // bir IP adresi dizisi de verilebilir -``` - -IP adresini bir çerezle birleştirmenizi kesinlikle öneririz. `nette-debug` çerezinde gizli bir belirteç, örneğin `secret1234`, saklayın ve böylece geliştirme modunu, belirli bir IP adresinden erişen ve çerezinde bu belirteci de taşıyan programcılar için etkinleştirin: - -```php -$this->configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Geliştirme modunu, localhost için bile, tamamen kapatabiliriz de: - -```php -$this->configurator->setDebugMode(false); -``` - -`true` değerinin geliştirme modunu zorla açtığını, bunun da bir üretim sunucusunda **asla** olmaması gerektiğini unutmayın. - -Otomatik algılamayı içeride `Configurator::detectDebugMode()` statik metodu yürütür; onu kendiniz de çağırabilirsiniz, örneğin geliştirme modunu configurator dışında algılamak için. İsteğe bağlı bir IP adresi veya bilgisayar adı beyaz listesi kabul eder ve geçerli isteğin geliştirme modunda çalışıp çalışmayacağını döndürür: - -```php -$debug = Nette\Bootstrap\Configurator::detectDebugMode('23.75.345.200'); -``` - - -Hata ayıklama aracı Tracy -========================= - -Kolay hata ayıklama için mükemmel [Tracy |tracy:] aracını etkinleştireceğiz. Geliştirme modunda hataları görselleştirir, üretim modunda ise belirtilen dizine günlükler: - -```php -$this->configurator->enableTracy($this->rootDir . '/log'); -``` - - -Geçici dosyalar -=============== - -Nette; DI konteyneri, RobotLoader, şablonlar vb. için önbellek kullanır. Bu yüzden önbelleğin saklanacağı dizinin yolunu ayarlamak gerekir: - -```php -$this->configurator->setTempDirectory($this->rootDir . '/temp'); -``` - -Linux veya macOS'ta `log/` ve `temp/` dizinleri için [yazma izinlerini |nette:troubleshooting#Dizin İzinlerini Ayarlama] ayarlayın. - - -RobotLoader -=========== - -Genellikle sınıfları [RobotLoader |robot-loader:] ile otomatik yüklemek isteriz, bu yüzden onu başlatmamız ve `Bootstrap.php`'nin bulunduğu dizinden (yani `__DIR__`) ve tüm alt dizinlerinden sınıfları yüklemesini sağlamamız gerekir: - -```php -$this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); -``` - -Alternatif bir yaklaşım, sınıfları yalnızca PSR-4'e uygun olarak [Composer |best-practices:composer] üzerinden yüklemektir. - - -Saat dilimi -=========== - -Varsayılan saat dilimini configurator ile ayarlayabilirsiniz. - -```php -$this->configurator->setTimeZone('Europe/Prague'); -``` - - -DI konteynerinin yapılandırması -=============================== - -Başlatma sürecinin bir parçası, tüm uygulamanın kalbi olan DI konteynerinin, yani nesne factory'sinin oluşturulmasıdır. Aslında Nette tarafından üretilen ve önbellek dizininde saklanan bir PHP sınıfıdır. Factory, uygulamanın anahtar nesnelerini üretir; yapılandırma dosyalarıyla ona bunları nasıl oluşturup ayarlayacağını söyleriz ve böylece tüm uygulamanın davranışını etkileriz. - -Yapılandırma dosyaları genellikle [NEON formatında |neon:format] yazılır. Ayrı bir bölümde [neyin yapılandırılabileceğini |nette:configuring] okuyabilirsiniz. - -.[tip] -Geliştirme modunda konteyner, kod veya yapılandırma dosyaları her değiştiğinde otomatik güncellenir. Üretim modunda ise yalnızca bir kez üretilir ve performansı en üst düzeye çıkarmak için değişiklikler denetlenmez. - -`createContainer()` konteyneri kurup örneğini döndürürken, `loadContainer()` metodu yalnızca üretilen konteyner sınıfının adını döndürür; onu kendiniz örnekleyebilirsiniz. Bu, ileri düzey senaryolarda işe yarar. - -Yapılandırma dosyaları `addConfig()` ile yüklenir: - -```php -$this->configurator->addConfig($this->rootDir . '/config/common.neon'); -``` - -Daha fazla yapılandırma dosyası eklemek istersek, `addConfig()` fonksiyonunu birden çok kez çağırabiliriz. - -```php -$configDir = $this->rootDir . '/config'; -$this->configurator->addConfig($configDir . '/common.neon'); -$this->configurator->addConfig($configDir . '/services.neon'); -if (PHP_SAPI === 'cli') { - $this->configurator->addConfig($configDir . '/cli.php'); -} -``` - -`cli.php` adı yazım hatası değildir; yapılandırma, onu dizi olarak döndüren bir PHP dosyasında da yazılabilir. - -Başka yapılandırma dosyalarını [`includes` bölümünde |dependency-injection:configuration#Dosyaları Dahil Etme] de ekleyebiliriz. - -Yapılandırma dosyalarında aynı anahtarlara sahip öğeler geçerse üzerine yazılır, [dizilerde ise birleştirilir |dependency-injection:configuration#Birleştirme]. Sonra dahil edilen dosyanın önceliği öncekinden yüksektir. `includes` bölümünün listelendiği dosyanın önceliği, içinde dahil edilen dosyalardan yüksektir. - - -Statik parametreler -------------------- - -Yapılandırma dosyalarında kullanılan parametreler [`parameters` bölümünde |dependency-injection:configuration#Parametreler] tanımlanabilir ve ayrıca `addStaticParameters()` metoduyla (eski, artık kullanımdan kaldırılmış takma adı `addParameters()`) aktarılabilir veya üzerine yazılabilir. Önemli olan, farklı parametre değerlerinin ek DI konteynerlerinin, yani ek sınıfların üretilmesine yol açmasıdır. - -```php -$this->configurator->addStaticParameters([ - 'projectId' => 23, -]); -``` - -`projectId` parametresine yapılandırmada standart `%projectId%` gösterimiyle başvurulabilir. - - -Dinamik parametreler --------------------- - -Konteynere dinamik parametreler de ekleyebiliriz; bunların farklı değerleri, statik parametrelerin aksine, yeni DI konteynerlerinin üretilmesine yol açmaz. - -```php -$this->configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Böylece örneğin ortam değişkenlerini kolayca ekleyebiliriz; onlara yapılandırmada `%env.degisken%` gösterimiyle başvurulabilir. - -```php -$this->configurator->addDynamicParameters([ - 'env' => getenv(), -]); -``` - - -Varsayılan parametreler ------------------------ - -Yapılandırma dosyalarında şu parametreleri kullanabilirsiniz: - -- `%appDir%`, `Bootstrap.php` dosyasını içeren dizinin mutlak yoludur -- `%wwwDir%`, giriş dosyası `index.php`'yi içeren dizinin mutlak yoludur -- `%tempDir%`, geçici dosyalar dizininin mutlak yoludur -- `%vendorDir%`, Composer'ın kütüphaneleri kurduğu dizinin mutlak yoludur -- `%rootDir%`, projenin kök dizininin mutlak yoludur -- `%baseUrl%`, kök dizine giden mutlak URL'dir (çalışma zamanında çözülen dinamik bir parametre) -- `%debugMode%`, uygulamanın hata ayıklama modunda olup olmadığını belirtir -- `%consoleMode%`, isteğin komut satırından gelip gelmediğini belirtir - - -İçe aktarılan servisler ------------------------ - -Şimdi biraz derine iniyoruz. DI konteynerinin amacı nesne oluşturmak olsa da, ara sıra var olan bir nesneyi konteynere yerleştirme ihtiyacı doğabilir. Bunu, servisi `imported: true` bayrağıyla tanımlayarak yaparız. - -```neon -services: - myservice: - type: App\Model\MyCustomService - imported: true -``` - -Ve bootstrap'ta nesneyi konteynere yerleştiririz: - -```php -$this->configurator->addServices([ - 'myservice' => new App\Model\MyCustomService('foobar'), -]); -``` - - -Farklı ortamlar -=============== - -`Bootstrap` sınıfını ihtiyaçlarınıza göre çekinmeden değiştirin. Web projelerini ayırt etmek için `bootWebApplication()` metoduna parametre ekleyebilirsiniz. Ya da başka metotlar ekleyebiliriz; örneğin birim testleri için ortamı hazırlayan `bootTestEnvironment()`, komut satırından çağrılan betikler için `bootConsoleApplication()` vb. - -```php -public function bootTestEnvironment(): Nette\DI\Container -{ - Tester\Environment::setup(); // Nette Tester'ın başlatılması - $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/tr/components.texy b/application/tr/components.texy deleted file mode 100644 index 3817132cbc..0000000000 --- a/application/tr/components.texy +++ /dev/null @@ -1,504 +0,0 @@ -Etkileşimli bileşenler -********************** - -
    - -Bileşenler, sayfalara gömdüğümüz ayrı ve yeniden kullanılabilir nesnelerdir. Formlar, datagrid'ler, anketler, kısacası tekrar tekrar kullanmanın anlamlı olduğu her şey olabilirler. Şunları göstereceğiz: - -- bileşenler nasıl kullanılır? -- nasıl yazılır? -- sinyaller nedir? - -
    - -Nette'in yerleşik bir bileşen sistemi vardır. Buna benzer bir şey Delphi veya ASP.NET Web Forms kıdemlilerine tanıdık gelebilir; React ya da Vue.js de uzaktan benzer bir şey üzerine kuruludur. Ancak PHP framework'leri dünyasında bu eşsiz bir özelliktir. - -Aynı zamanda bileşenler, uygulama geliştirmeye yaklaşımı temelden etkiler. Sayfaları önceden hazırlanmış birimlerden oluşturabilirsiniz. Yönetim panelinizde bir datagrid mi lazım? Onu, Nette için açık kaynak eklentilerin (yalnızca bileşenlerin değil) deposu olan [Componette |https://componette.org/search/component] üzerinde bulun ve presenter'a eklemeniz yeterli. - -Presenter'a istediğiniz sayıda bileşen yerleştirebilirsiniz. Bazı bileşenlerin içine de başka bileşenler gömebilirsiniz. Böylece kökü presenter olan bir bileşen ağacı oluşur. - - -Factory metotları -================= - -Bileşenler presenter'a nasıl yerleştirilir ve sonra nasıl kullanılır? Genellikle factory metotlarıyla. - -Bileşen factory'si, bileşenleri yalnızca gerçekten gerektiğinde (lazy / talep üzerine) oluşturmanın zarif bir yoludur. Tüm sihir, `createComponent()` adlı bir metodu uygulamakta yatar; burada `` oluşturulan bileşenin adıdır ve metot bileşeni oluşturup döndürür. - -```php .{file:DefaultPresenter.php} -class DefaultPresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentPoll(): PollControl - { - $poll = new PollControl; - $poll->items = $this->items; - return $poll; - } -} -``` - -Tüm bileşenler ayrı metotlarda oluşturulduğu için kod daha anlaşılır olur. - -.[note] -Bileşen adları, metot adında büyük harfle yazılsalar da, her zaman küçük harfle başlar. - -Factory'leri asla doğrudan çağırmayız; bileşeni ilk kullandığımızda otomatik çağrılırlar. Bu sayede bileşen doğru anda ve yalnızca gerçekten gerekiyorsa oluşturulur. Bileşeni kullanmazsak (örneğin sayfanın yalnızca bir bölümünün aktarıldığı bir AJAX isteğinde veya şablon önbelleğe alınırken), hiç oluşturulmaz ve sunucu performansından tasarruf edilir. - -```php .{file:DefaultPresenter.php} -// bileşene erişiriz ve bu ilk seferse -// onu oluşturan createComponentPoll() çağrılır -$poll = $this->getComponent('poll'); -// alternatif sözdizimi: $poll = $this['poll']; -``` - -Şablonda bir bileşen [{control} |#Render] etiketiyle render edilebilir. Bu yüzden bileşenleri şablona elle aktarmaya gerek yoktur. - -```latte -

    Lütfen oy verin

    - -{control poll} -``` - -.[tip] -Sayısı değişken bileşenleri dinamik olarak oluşturmak için [Multiplier |multiplier] kullanın. - -`createComponent()` factory metotları yalnızca presenter'larda çalışmaz. Bir bileşeni başka bir bileşenin içine aynı şekilde yerleştirip ağaç halinde birleştirebilirsiniz; bu, örneğin bir bileşenin içinde ayrı render edilen bir form için kullanışlıdır. - - -Hollywood tarzı -=============== - -Bileşenler genellikle Hollywood tarzı demeyi sevdiğimiz taze bir teknik kullanır. Film seçmelerine katılanların sıkça duyduğu klişeyi mutlaka bilirsiniz: "Bizi aramayın, biz sizi ararız." İşte tam olarak mesele budur. - -Nette'te sürekli soru sormak zorunda kalmak yerine ("form gönderildi mi?", "geçerli miydi?", "kullanıcı bu düğmeye bastı mı?"), framework'e "bu olduğunda şu metodu çağır" dersiniz ve gerisini ona bırakırsınız. JavaScript programlıyorsanız bu programlama tarzına yakından aşinasınızdır. Belirli bir olay gerçekleştiğinde çağrılan fonksiyonlar yazarsınız. Ve dil onlara uygun parametreleri aktarır. - -Bu, uygulama yazmaya bakışı tamamen değiştirir. Framework'e ne kadar çok işi bırakabilirseniz, o kadar az işiniz olur. Ve o kadar az şeyi gözden kaçırırsınız. - - -Bileşen yazma -============= - -Bileşen terimiyle genellikle [api:Nette\Application\UI\Control] sınıfının bir torununu kastediyoruz. (Aslında "control" terimini kullanmak daha doğru olurdu, ama bunun bazı dillerde başka bir anlamı var ve "bileşen" daha çok yerleşti.) Presenter'ın kendisi, [api:Nette\Application\UI\Presenter], de `Control` sınıfının bir torunudur. - -```php .{file:PollControl.php} -use Nette\Application\UI\Control; - -class PollControl extends Control -{ -} -``` - - -Render -====== - -Bir bileşeni render etmek için `{control bilesenAdi}` etiketinin kullanıldığını zaten biliyoruz. Bu aslında bileşenin `render()` metodunu çağırır; render'ı orada biz üstleniriz. Tıpkı presenter'daki gibi, `$this->template` değişkeninde parametre aktardığımız bir [Latte şablonu|templates] elimizin altındadır. Presenter'dan farklı olarak, şablon dosyasını belirtmemiz ve render ettirmemiz gerekir: - -```php .{file:PollControl.php} -public function render(): void -{ - // şablona bazı parametreler ekle - $this->template->param = $value; - // ve render et - $this->template->render(__DIR__ . '/poll.latte'); -} -``` - -`{control}` etiketi, `render()` metoduna parametre aktarmayı sağlar: - -```latte -{control poll, $id, $message} -``` - -```php .{file:PollControl.php} -public function render(int $id, string $message): void -{ - // ... -} -``` - -Bazen bir bileşen, ayrı ayrı render etmek istediğimiz birkaç bölümden oluşabilir. Her biri için kendi render metodumuzu oluştururuz, burada örnekte `renderPaginator()`: - -```php .{file:PollControl.php} -public function renderPaginator(): void -{ - // ... -} -``` - -Şablonda ise onu şöyle çağırırız: - -```latte -{control poll:paginator} -``` - -Daha iyi anlamak için bu etiketin PHP koduna nasıl çevrildiğini bilmek iyidir. - -```latte -{control poll} -{control poll:paginator, 123, 'hello'} -``` - -şuna çevrilir: - -```php -$control->getComponent('poll')->render(); -$control->getComponent('poll')->renderPaginator(123, 'hello'); -``` - -`getComponent()` metodu `poll` bileşenini döndürür ve bu bileşen üzerinde `render()` metodu, ya da etikette iki nokta üst üstenin ardından farklı bir render metodu belirtilmişse `renderPaginator()`, çağrılır. - -.[caution] -Dikkat: parametrelerde köşeli parantez dışında **`=>`** geçerse, tüm parametreler bir diziye sarılır ve ilk argüman olarak aktarılır: - -```latte -{control poll, id: 123, message: 'hello'} -``` - -şuna çevrilir: - -```php -$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']); -``` - -Bir alt bileşenin render edilmesi: - -```latte -{control cartControl-someForm} -``` - -şuna çevrilir: - -```php -$control->getComponent("cartControl-someForm")->render(); -``` - -Bileşenler, presenter'lar gibi, şablonlara birkaç yararlı değişkeni otomatik aktarır: - -- `$basePath`, kök dizine giden mutlak URL yoludur (örneğin `/eshop`) -- `$baseUrl`, kök dizine giden mutlak URL'dir (örneğin `http://localhost/eshop`) -- `$user`, [kullanıcıyı temsil eden |security:authentication] bir nesnedir -- `$presenter`, geçerli presenter'dır -- `$control`, geçerli bileşendir -- `$flashes`, `flashMessage()` fonksiyonuyla gönderilen [mesajların |#Flash mesajları] dizisidir - - -Sinyal -====== - -Bir Nette uygulamasında gezinmenin, `Presenter:eylem` çiftlerine bağlantı vermekten veya yönlendirmekten oluştuğunu zaten biliyoruz. Peki ya yalnızca **geçerli sayfada** bir eylem gerçekleştirmek istersek? Örneğin bir tablodaki sütunların sıralamasını değiştirmek; bir öğeyi silmek; açık/koyu modu değiştirmek; bir formu göndermek; bir ankette oy vermek vb. - -Bu tür isteğe sinyal denir. Ve eylemler `action()` veya `render()` metotlarını çağırdığı gibi, sinyaller de `handle()` metotlarını çağırır. Eylem (veya görünüm) kavramı yalnızca presenter'ları ilgilendirirken, sinyaller tüm bileşenleri ilgilendirir. Dolayısıyla presenter'ları da, çünkü `UI\Presenter`, `UI\Control`'ün torunudur. - -```php -public function handleClick(int $x, int $y): void -{ - // ... sinyalin işlenmesi ... -} -``` - -Sinyal çağıran bir bağlantı her zamanki gibi oluşturulur; yani şablonda `n:href` niteliğiyle veya `{link}` etiketiyle, kodda `link()` metoduyla. Daha fazlası [URL bağlantıları oluşturma |creating-links#Sinyallere bağlantılar] bölümünde. - -```latte -buraya tıkla -``` - -Sinyal her zaman geçerli presenter ve eylem üzerinde çağrılır; başka bir presenter veya eylem üzerinde çağırmak mümkün değildir. - -Böylece sinyal, sayfanın tıpkı özgün istekteki gibi yeniden yüklenmesine yol açar, ama ek olarak sinyal işleme metodunu uygun parametrelerle çağırır. Metot yoksa, kullanıcıya 403 Forbidden hata sayfası olarak gösterilen bir [api:Nette\Application\UI\BadSignalException] istisnası fırlatılır. - - -Snippet'ler ve AJAX -=================== - -Sinyaller size biraz AJAX'ı hatırlatabilir: geçerli sayfada çağrılan işleyiciler. Ve haklısınız, sinyaller gerçekten sıklıkla AJAX ile çağrılır ve ardından tarayıcıya yalnızca sayfanın değişen bölümleri aktarılır. Bunlara snippet denir. Daha fazla bilgiyi [AJAX'a ayrılmış sayfada |ajax] bulabilirsiniz. - - -Flash mesajları -=============== - -Bir bileşenin, presenter'dan bağımsız kendi flash mesaj deposu vardır. Bunlar örneğin bir işlemin sonucunu bildiren mesajlardır. Flash mesajlarının önemli bir özelliği, yönlendirmeden sonra da şablonda erişilebilir olmalarıdır. Gösterildikten sonra bile 30 saniye daha etkin kalırlar; örneğin kullanıcı bir aktarım hatası yüzünden sayfayı yenilerse mesaj hemen kaybolmaz. - -Gönderimi [flashMessage |api:Nette\Application\UI\Control::flashMessage()] metodu üstlenir. İlk parametre mesajın metni (`string`, `Stringable`) veya mesajı temsil eden bir `stdClass` nesnesidir. İsteğe bağlı ikinci parametre onun tipidir (error, warning, info vb.). `flashMessage()` metodu, flash mesajın örneğini bir `stdClass` nesnesi olarak döndürür; ona başka bilgiler eklenebilir. - -```php -$this->flashMessage('Öğe silindi.'); -$this->redirect(/* ... */); // ve yönlendir -``` - -Bu mesajlar şablona `$flashes` değişkeninde `stdClass` nesneleri olarak sunulur; `message` (mesaj metni) ve `type` (mesaj tipi) özelliklerini içerirler ve sözü geçen kullanıcı bilgilerini de taşıyabilirler. Onları örneğin şöyle render ederiz: - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -Sinyalin işlenmesinden sonra yönlendirme -======================================== - -Bir bileşenin sinyalinin işlenmesinin ardından çoğu zaman bir yönlendirme gelir. Bu formlara benzer; onları gönderdikten sonra da, sayfa tarayıcıda yenilenirse verinin yeniden gönderilmesini engellemek için yönlendiririz. - -```php -$this->redirect('this'); // geçerli presenter ve eyleme yönlendirir -``` - -Bir bileşen yeniden kullanılabilir bir öğe olduğundan ve genellikle belirli presenter'larla doğrudan bağı olmaması gerektiğinden, `redirect()` ve `link()` metotları parametreyi otomatik olarak bileşen sinyali diye yorumlar: - -```php -$this->redirect('click'); // aynı bileşenin 'click' sinyaline yönlendirir -``` - -Başka bir presenter'a veya eyleme yönlendirmeniz gerekirse bunu presenter üzerinden yapabilirsiniz: - -```php -$this->getPresenter()->redirect('Product:show'); // başka bir presenter'a/eyleme yönlendirir -``` - - -Kalıcı parametreler -=================== - -Kalıcı parametreler, bileşenlerdeki durumu farklı istekler boyunca korumak için kullanılır. Değerleri, bir bağlantıya tıklandıktan sonra da aynı kalır. Oturum verilerinin aksine URL'de aktarılırlar. Ve bu, aynı sayfadaki başka bileşenlerde oluşturulan bağlantılar dahil, tamamen otomatik gerçekleşir. - -Örneğin içeriği sayfalamak için bir bileşeniniz var. Bir sayfada böyle birkaç bileşen olabilir. Ve bir bağlantıya tıklandıktan sonra tüm bileşenlerin kendi geçerli sayfalarında kalmasını istiyoruz. Bu yüzden sayfa numarasını (`page`) kalıcı parametre yaparız. - -Nette'te kalıcı parametre oluşturmak son derece basittir. Public bir özellik oluşturup onu nitelikle işaretlemeniz yeterlidir: (önceden `/** @persistent */` kullanılıyordu) - -```php -use Nette\Application\Attributes\Persistent; // bu satır önemli - -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; // public olmalı -} -``` - -Özellik için veri tipini (örneğin `int`) belirtmenizi öneririz, ayrıca bir varsayılan değer de verebilirsiniz. Parametre değerleri [doğrulanabilir |#Kalıcı parametrelerin doğrulanması]. - -Bağlantı oluştururken kalıcı parametrenin değeri değiştirilebilir: - -```latte -sonraki -``` - -Ya da *sıfırlanabilir*, yani URL'den kaldırılabilir. O zaman varsayılan değerini alır: - -```latte -sıfırla -``` - - -Kalıcı bileşenler -================= - -Yalnızca parametreler değil, bileşenler de kalıcı olabilir. Kalıcı parametreleri o zaman presenter'ın farklı eylemleri arasında, hatta birden fazla presenter arasında bile aktarılır. Kalıcı bileşenleri presenter sınıfı üzerinde bir nitelikle işaretleriz. Örneğin `calendar` ve `poll` bileşenlerini şöyle işaretleriz: - -```php -use Nette\Application\Attributes\Persistent; - -#[Persistent('calendar', 'poll')] -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Bu bileşenlerin içindeki alt bileşenleri işaretlemeye gerek yoktur; onlar da kalıcı olur. - -Eski `@persistent` anotasyonu hâlâ çalışır, ama kullanımdan kaldırılmıştır ve bir uyarı tetikler: - -```php -/** - * @persistent(calendar, poll) - */ -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Bağımlılıkları olan bileşenler -============================== - -Bağımlılıkları olan bileşenler, onları kullanacak presenter'ları "karıştırmadan" nasıl oluşturulur? Nette'teki DI konteynerinin akıllı özellikleri sayesinde, klasik servislerin kullanımında olduğu gibi, işin çoğu framework'e bırakılabilir. - -`PollFacade` servisine bağımlılığı olan bir bileşen örneğini ele alalım: - -```php -class PollControl extends Control -{ - public function __construct( - private int $id, // bileşeni oluşturduğumuz anketin ID'si - private PollFacade $facade, - ) { - } - - public function handleVote(int $voteId): void - { - $this->facade->vote($this->id, $voteId); - // ... - } -} -``` - -Klasik bir servis yazsaydık tartışacak bir şey olmazdı. DI konteyneri tüm bağımlılıkların aktarımını görünmez biçimde üstlenirdi. Ancak bileşenlerde bunları genellikle, presenter'da doğrudan [#Factory metotları] `createComponent…()` içinde yeni bir örnek oluşturarak hallederiz. Ama tüm bileşenlerin tüm bağımlılıklarını, yalnızca bileşenlere aktarmak için presenter'a aktarmak zahmetlidir. Bir de yazılacak kod miktarı... - -Mantıklı soru şu: neden bileşeni klasik bir servis olarak kaydedip presenter'a aktarmıyor ve sonra `createComponent…()` metodunda döndürmüyoruz? Ancak bu yaklaşım uygun değildir, çünkü gerektiğinde bileşeni birden fazla kez oluşturabilme olanağını isteriz. - -Doğru çözüm, bileşen için bir factory, yani bileşeni bizim için oluşturan bir sınıf yazmaktır: - -```php -class PollControlFactory -{ - public function __construct( - private PollFacade $facade, - ) { - } - - public function create(int $id): PollControl - { - return new PollControl($id, $this->facade); - } -} -``` - -Bu factory'yi yapılandırmada konteynerimize kaydederiz: - -```neon -services: - - PollControlFactory -``` - -ve son olarak presenter'ımızda kullanırız: - -```php -class PollPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private PollControlFactory $pollControlFactory, - ) { - } - - protected function createComponentPollControl(): PollControl - { - $pollId = 1; // kendi parametremizi aktarabiliriz - return $this->pollControlFactory->create($pollId); - } -} -``` - -İşin güzel yanı, Nette DI'nin böyle basit factory'leri [üretebilmesidir |dependency-injection:factory]; yani tüm kodunu yazmak yerine yalnızca arayüzünü yazmanız yeterli: - -```php -interface PollControlFactory -{ - public function create(int $id): PollControl; -} -``` - -Ve hepsi bu. Nette bu arayüzü içeride uygular ve onu presenter'a enjekte eder, biz de orada kullanırız. `$id` parametresini ve `PollFacade` sınıfının bir örneğini bileşenimize sihirli biçimde ekler. - - -Bileşenler derinlemesine -======================== - -Nette Application'daki bileşenler, sayfalara gömdüğümüz ve bu bölümün tamamının ayrıldığı, web uygulamasının yeniden kullanılabilir parçalarıdır. Peki böyle bir bileşenin yetenekleri tam olarak nelerdir? - -1) Bir şablonda render edilebilir -2) Bir AJAX isteğinde [kendisinin hangi bölümünü |ajax#Snippet'ler] render edeceğini bilir (snippet'ler) -3) Durumunu URL'de saklama yeteneğine sahiptir (kalıcı parametreler) -4) Kullanıcı eylemlerine tepki verme yeteneğine sahiptir (sinyaller) -5) Hiyerarşik bir yapı oluşturur (kökü presenter'dır) - -Bu işlevlerin her birini kalıtım zincirindeki sınıflardan biri üstlenir. Render'ı (1 + 2) [api:Nette\Application\UI\Control], [yaşam döngüsüne |presenters#Presenter'ın yaşam döngüsü] entegrasyonu (3, 4) [api:Nette\Application\UI\Component] sınıfı, hiyerarşik yapının oluşturulmasını (5) ise [Container ve Component |component-model:] sınıfları üstlenir. - -``` -Nette\ComponentModel\Component { IComponent } -| -+- Nette\ComponentModel\Container { IContainer } - | - +- Nette\Application\UI\Component { SignalReceiver, StatePersistent } - | - +- Nette\Application\UI\Control { Renderable } - | - +- Nette\Application\UI\Presenter { IPresenter } -``` - - -Bileşenin yaşam döngüsü ------------------------ - -[* lifecycle-component.svg *] *** *Bileşenin yaşam döngüsü* .<> - - -Kalıcı parametrelerin doğrulanması ----------------------------------- - -URL'lerden alınan [#Kalıcı parametreler] değerleri özelliklere `loadState()` metoduyla yazılır. Bu metot ayrıca özellik için belirtilen veri tipinin uyup uymadığını denetler; uymuyorsa 404 hatasıyla yanıt verir ve sayfa gösterilmez. - -Kalıcı parametrelere asla körü körüne güvenmeyin, çünkü kullanıcı onları URL'de kolayca değiştirebilir. Örneğin sayfa numarası `$this->page`'in 0'dan büyük olup olmadığını böyle denetleriz. Uygun bir yol, sözü geçen `loadState()` metodunu ezmektir: - -```php -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; - - public function loadState(array $params): void - { - parent::loadState($params); // $this->page burada ayarlanır - // ardından kendi değer denetimimiz gelir: - if ($this->page < 1) { - $this->error(); - } - } -} -``` - -Tersi süreci, yani değerlerin kalıcı özelliklerden toplanmasını, `saveState()` metodu üstlenir. - - -Presenter'a bağlanma --------------------- - -Bir bileşen presenter hiyerarşisinin parçası olduğu anda, `$onAnchor` dizisinde saklanan callback'leri çağrılır. O andan itibaren bileşenin presenter'ı elinin altındadır, güvenle bağlantı oluşturabilir, kalıcı parametreleri okuyabilir vb. - -```php -$control->onAnchor[] = function ($control): void { - // bileşenin artık presenter'ı elinin altında -}; -``` - - -Sinyaller derinlemesine ------------------------ - -Bir sinyal, sayfanın tıpkı özgün istekteki gibi yeniden yüklenmesine yol açar (AJAX ile çağrılması dışında) ve varsayılan uygulaması `Nette\Application\UI\Component` sınıfında bulunan `signalReceived($signal)` metodunu çağırır; bu uygulama `handle` sözcüklerinden oluşan bir metodu çağırmayı dener. Sonraki işleme ilgili nesneye kalmıştır. `Component`'ten kalıtım alan nesneler (yani `Control` ve `Presenter`), `handle` metodunu uygun parametrelerle çağırmayı deneyerek tepki verir. - -Başka bir deyişle: `handle` fonksiyonunun tanımı, istekle gelen tüm parametrelerle birlikte alınır, URL'deki parametreler argümanlara ada göre atanır ve metot çağrılmaya çalışılır. Örneğin URL'deki `id` parametresinin değeri `$id` argümanı olarak, URL'deki `something` ise `$something` olarak aktarılır vb. Metot yoksa `signalReceived` metodu bir [istisna |api:Nette\Application\UI\BadSignalException] fırlatır. - -Bir sinyal, URL'deki parametrelerin yanı sıra **isteğin POST gövdesinde** gönderilen parametreleri de okur. Bu işe yarar, çünkü sinyaller sıklıkla JavaScript ile çağrılır ve orada veriyi POST yöntemiyle göndermek doğaldır. Ancak aynı adlı bir parametre hem URL'den hem POST gövdesinden gelirse, **URL'deki değer önceliklidir**. Bu yüzden bir POST alanına URL veya rota parametresiyle aynı adı vermekten kaçının, yoksa URL değeri onu sessizce ezer. Sinyal parametreleri, eylem ve kalıcı parametrelerle ortak bir alanı paylaşır; bkz. [Ortak parametre alanı |presenters#Ortak parametre alanı]. - -Bir sinyali, `SignalReceiver` arayüzünü uygulayan ve bileşen ağacına bağlı olan her bileşen, presenter veya nesne alabilir. - -Sinyallerin başlıca alıcıları `Presenter`'lar ve `Control`'den kalıtım alan görsel bileşenler olacaktır. Sinyal, bir nesneye bir şey yapması gerektiğinin işareti olmayı amaçlar: bir anket kullanıcının oyunu saymalı, bir haber bloğu genişleyip iki kat fazla haber göstermeli, bir form gönderildi ve veriyi işlemeli vb. - -Sinyalin URL'si [Component::link() |api:Nette\Application\UI\Component::link()] metoduyla oluşturulur. `$destination` parametresi olarak `{sinyal}!` dizesini, `$args` olarak ise sinyale aktarmak istediğimiz argümanların dizisini veririz. Sinyal her zaman geçerli presenter ve eylem üzerinde, geçerli parametrelerle çağrılır; sinyal parametreleri yalnızca eklenir. Ayrıca **sinyali belirten `?do` parametresi** eklenir. - -Biçimi ya `{sinyal}` ya da `{sinyalAlicisi}-{sinyal}`'dir. `{sinyalAlicisi}`, presenter'daki bileşenin adıdır. Bu yüzden bileşen adında kısa çizgi kullanılamaz; o, bileşen adını ve sinyali ayırmaya yarar, ama bu yolla birden fazla bileşeni iç içe geçirmek mümkündür. - -[isSignalReceiver()|api:Nette\Application\UI\Presenter::isSignalReceiver()] metodu, bileşenin (ilk argüman) sinyalin (ikinci argüman) alıcısı olup olmadığını denetler. İkinci argüman atlanabilir; o zaman bileşenin herhangi bir sinyalin alıcısı olup olmadığı denetlenir. İkinci parametre `true` yapılırsa, belirtilen bileşenin veya torunlarından herhangi birinin alıcı olup olmadığı doğrulanır. - -`handle`'den önceki herhangi bir aşamada, sinyali [processSignal()|api:Nette\Application\UI\Presenter::processSignal()] metodunu çağırarak elle çalıştırabiliriz; bu metot sinyalin işlenmesini üstlenir: sinyalin alıcısı olarak belirlenen bileşeni alır (alıcı belirtilmemişse presenter'ın kendisidir) ve sinyali ona gönderir. - -Örnek: - -```php -if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) { - $this->processSignal(); -} -``` - -Bu, sinyali erkenden çalıştırır ve sinyal bir daha çağrılmaz. diff --git a/application/tr/configuration.texy b/application/tr/configuration.texy deleted file mode 100644 index 31ccd0b872..0000000000 --- a/application/tr/configuration.texy +++ /dev/null @@ -1,186 +0,0 @@ -Uygulama yapılandırması -*********************** - -.[perex] -Nette Application'ın yapılandırma seçeneklerine genel bakış. - - -Application -=========== - -```neon -application: - # Tracy BlueScreen'de "Nette Application" paneli gösterilsin mi? - debugger: ... # (bool) Tracy varsa etkin - - # üretimde istisnaları her zaman error-presenter işler; - # bu seçenek yalnızca aynı davranışı geliştirme modunda da açar - catchExceptions: ... # (bool) varsayılan false - yani geliştirmede kapalı, üretimde her zaman açık - - # error-presenter'ın adı - errorPresenter: Error # (string|array) varsayılan 'Nette:Error' - - # presenter'lar ve eylemler için takma adlar tanımlar - aliases: ... - - # presenter adının sınıfa çevrilme kurallarını tanımlar - mapping: ... - - # geçersiz bağlantı uyarıları bastırılsın mı? - # yalnızca geliştirme modunda etkilidir - silentLinks: ... # (bool) varsayılan false -``` - -`nette/application` 3.2 sürümünden itibaren bir çift error presenter tanımlanabilir: - -```neon -application: - errorPresenter: - 4xx: Error4xx # Nette\Application\BadRequestException için - 5xx: Error5xx # diğer istisnalar için -``` - -Bunları ayırmak yararlıdır, çünkü iki durum temelden farklıdır. `BadRequestException` (4xx kodları), uygulamanın sorunsuz olduğu ve yalnızca ziyaretçinin var olmayan bir şey istediği anlamına gelir. Bu yüzden sitenizin layout'unda dostça bir mesaj gösteren tam donanımlı bir presenter kullanabilirsiniz. Buna karşılık 5xx hatası, uygulamada bir şeyin bozulduğu ve nedenini bilmediğiniz anlamına gelir. 5xx presenter'ını olabildiğince yalın tutun ki render edilirken başka bir şey bozulmasın; ideal olarak veritabanına, layout'a veya oturum açmış kullanıcıya dokunmamalıdır. - -`silentLinks` seçeneği, bağlantı üretimi başarısız olduğunda (örneğin presenter var olmadığı için) Nette'in geliştirme modunda nasıl davranacağını belirler. Varsayılan `false` değeri, Nette'in `E_USER_WARNING` hatası tetiklemesi demektir. `true` yapmak bu hata mesajını bastırır. Üretim ortamında `E_USER_WARNING` her zaman tetiklenir. Bu davranış, presenter'ın [$invalidLinkMode |creating-links#Geçersiz bağlantılar] değişkeni ayarlanarak da etkilenebilir. - -[Takma adlar, sık kullanılan |creating-links#Takma adlar] presenter'lara başvurmayı kolaylaştırır. - -[Mapping, sınıf adının |directory-structure#Presenter mapping] presenter adından türetilme kurallarını tanımlar. - - -Presenter'ların otomatik kaydı ------------------------------- - -Nette, presenter'ları DI konteynerine servis olarak otomatik ekler, bu da oluşturulmalarını belirgin biçimde hızlandırır. Nette'in presenter'ları nasıl bulduğu yapılandırılabilir: - -```neon -application: - # presenter'lar Composer sınıf haritasında aransın mı? - scanComposer: ... # (bool) varsayılan true - - # sınıf ve dosya adının uyması gereken maske - scanFilter: ... # (string) varsayılan '*Presenter' - - # presenter'lar hangi dizinlerde aransın? - scanDirs: # (string[]|false) varsayılan '%appDir%' - - %vendorDir%/mymodule -``` - -`scanDirs` içinde listelenen dizinler varsayılan `%appDir%` değerini değiştirmez, ona eklenir; yani `scanDirs` hem `%appDir%` hem `%vendorDir%/mymodule` yollarını içerir. Varsayılan dizini atlamak istersek [ünlem işareti |dependency-injection:configuration#Birleştirme] kullanırız: - -```neon -application: - scanDirs!: - - %vendorDir%/mymodule -``` - -Dizin taraması, değeri `false` yaparak kapatılabilir. Presenter'lar o zaman servis olarak kaydedilmez, dolayısıyla [decorator |dependency-injection:configuration#Decorator] bölümünden ayarlanamazlar ve oluşturulmaları yavaşlar. Bu yüzden otomatik kaydı tamamen bastırmayı önermiyoruz, çünkü uygulamanın performansını düşürür. - - -Latte şablonları -================ - -Bu ayar, Latte'nin bileşenlerdeki ve presenter'lardaki davranışını genel olarak etkiler. - -```neon -latte: - # Tracy Bar'da Latte paneli ana şablon için (true) mi, tüm bileşenler için (all) mi gösterilsin? - debugger: ... # (true|false|'all') Tracy varsa etkin (yalnızca hata ayıklama modunda) - - # şablonları declare(strict_types=1) başlığıyla üret - strictTypes: ... # (bool) varsayılan false - - # [katı ayrıştırıcı modunu |latte:develop#strict mode] etkinleştir - strictParsing: ... # (bool) varsayılan false - - # değişkenlerin kapsamını döngü gövdesiyle sınırlar - scopedLoopVariables: ... # (bool) varsayılan false - - # çift etiketlerdeki iç içelikten kaynaklanan girintiyi kaldırır - dedent: ... # (bool) varsayılan false - - # [üretilen kodun denetimini |latte:develop#Checking Generated Code] etkinleştir - phpLinter: ... # (string) varsayılan null - - # yerel ayarı belirle - locale: cs_CZ # (string) varsayılan null - - # $this->template nesnesinin sınıfı - templateClass: App\MyTemplateClass # varsayılan Nette\Bridges\ApplicationLatte\DefaultTemplate -``` - -Yeni [uzantıları |latte:extending-latte#Latte Extension] şöyle ekleyebilirsiniz: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - - -Yönlendirme -=========== - -Temel ayarlar: - -```neon -routing: - # Tracy Bar'da yönlendirme paneli gösterilsin mi? - debugger: ... # (bool) Tracy varsa etkin (yalnızca hata ayıklama modunda) - - # router'ı DI konteynerine serileştir - cache: ... # (bool) varsayılan false -``` - -Yönlendirme genellikle [RouterFactory |routing#Rota koleksiyonu] sınıfında tanımlanır. Alternatif olarak rotalar yapılandırmada `maske: eylem` çiftleriyle de tanımlanabilir, ama bu yöntem pek esneklik sunmaz: - -```neon -routing: - routes: - 'detail/': Admin:Home:default - '/': Front:Home:default -``` - - -Sabitler -======== - -PHP sabitlerinin oluşturulması. - -```neon -constants: - Foobar: 'baz' -``` - -`Foobar` sabiti uygulama başladıktan sonra oluşturulur. - -.[note] -Sabitler genel olarak erişilebilir değişkenler olarak kullanılmamalıdır. Nesnelere değer aktarmak için [bağımlılık enjeksiyonu |dependency-injection:passing-dependencies] kullanın. - - -PHP -=== - -PHP direktiflerinin ayarlanması. Tüm direktiflere genel bakışı [php.net |https://www.php.net/manual/en/ini.list.php] adresinde bulabilirsiniz. - -```neon -php: - date.timezone: Europe/Prague -``` - - -DI servisleri -============= - -Bu servisler DI konteynerine eklenir: - -| Ad | Tip | Açıklama -|----------------------------|---------------------------------------------------|----------------------------------------- -| `application.application` | [api:Nette\Application\Application] | [uygulama çalıştırıcısı |how-it-works#Nette Application] -| `application.linkGenerator` | [api:Nette\Application\LinkGenerator] | [LinkGenerator |creating-links#LinkGenerator] -| `application.presenterFactory` | [api:Nette\Application\IPresenterFactory] | presenter factory -| `application.###` | [api:Nette\Application\UI\Presenter] | tek tek presenter'lar -| `routing.router` | [api:Nette\Routing\Router] | router -| `latte.latteFactory` | [api:Nette\Bridges\ApplicationLatte\LatteFactory] | `Latte\Engine` nesnesi için factory -| `latte.templateFactory` | [api:Nette\Application\UI\TemplateFactory] | [`$this->template` |templates] için factory diff --git a/application/tr/creating-links.texy b/application/tr/creating-links.texy deleted file mode 100644 index b07aec4313..0000000000 --- a/application/tr/creating-links.texy +++ /dev/null @@ -1,319 +0,0 @@ -URL bağlantıları oluşturma -************************** - -
    - -Nette'te bağlantı oluşturmak parmakla göstermek kadar basittir. Yalnızca nişan alın, gerisini framework sizin için yapar. Şunları göstereceğiz: - -- şablonlarda ve başka yerlerde nasıl bağlantı oluşturulur -- geçerli sayfaya giden bağlantı nasıl ayırt edilir -- geçersiz bağlantılarla ne yapılır - -
    - - -[Çift yönlü yönlendirme |routing] sayesinde, uygulamanızın daha sonra değişebilecek veya birleştirmesi karmaşık olabilecek URL'lerini şablonlara ya da koda hiçbir zaman sabit yazmanız gerekmez. Bağlantıda yalnızca presenter'ı ve eylemi belirtin, gerekli parametreleri verin; URL'yi framework kendisi üretecektir. Aslında bir fonksiyon çağırmaya çok benzer. Bunu seveceksiniz. - - -Presenter şablonunda -==================== - -Bağlantıları çoğu zaman şablonlarda oluştururuz ve `n:href` niteliği harika bir yardımcıdır: - -```latte -detay -``` - -`href` HTML niteliği yerine [n:niteliği |latte:syntax#n:nitelikleri] `n:href` kullandığımıza dikkat edin. Değeri, `href` niteliğinde olacağı gibi bir URL değil, presenter'ın ve eylemin adıdır. - -Bir bağlantıya tıklamak, basitçe söylersek, `ProductPresenter::renderShow()` metodunu çağırmak gibi bir şeydir. İmzasında parametreler varsa onu argümanlarla çağırabiliriz: - -```latte -ürün detayı -``` - -Adlandırılmış parametreler de aktarılabilir. Aşağıdaki bağlantı, `lang` parametresini `en` değeriyle aktarır: - -```latte -ürün detayı -``` - -`ProductPresenter::renderShow()` metodunun imzasında `$lang` yoksa, parametrenin değerini `$lang = $this->getParameter('lang')` ile veya bir [özellikten |presenters#İsteğin parametreleri] alabilir. - -Parametreler bir dizide saklanıyorsa, `...` operatörüyle açılabilirler: - -```latte -{var $args = [$product->id, lang => en]} -ürün detayı -``` - -[Kalıcı parametreler |presenters#Kalıcı parametreler] denilen parametreler de bağlantılarda otomatik olarak aktarılır. - -`n:href` niteliği HTML `` etiketleri için çok kullanışlıdır. Bağlantıyı başka bir yerde, örneğin metin içinde yazdırmak istersek `{link}` kullanırız: - -```latte -URL şudur: {link Home:default} -``` - - -Kodda -===== - -Presenter'da bağlantı oluşturmak için `link()` metodu kullanılır: - -```php -$url = $this->link('Product:show', $product->id); -``` - -Parametreler dizi olarak da aktarılabilir; burada adlandırılmış parametreler de belirtilebilir: - -```php -$url = $this->link('Product:show', [$product->id, 'lang' => 'en']); -``` - -Bağlantılar presenter olmadan da, [#LinkGenerator] ve onun `link()` metoduyla oluşturulabilir. - -Bazen bir bağlantıyı şimdi oluşturmanız, ama asıl URL'yi ancak daha sonra üretmeniz gerekir. Bunun için bir `Nette\Application\UI\Link` nesnesi döndüren `lazyLink()` metodu vardır. Avantajı, bu nesneyi örneğin bir şablona aktarabilmeniz ve render edilmeden önce parametrelerini `setParameter()` metoduyla hâlâ ayarlayabilmenizdir. URL'nin kendisi ancak nesne bir dizeye dönüştürüldüğünde birleştirilir: - -```php -$link = $this->lazyLink('Product:show', $id); -// ... -echo $link; // URL ancak burada üretilir -``` - - -Presenter'lara bağlantılar -========================== - -Bağlantının hedefi bir presenter ve eylem ise, sözdizimi şöyledir: - -``` -[//] [[[[:]modül:]presenter:]eylem | this] [#fragman] -``` - -Bu formatı tüm Latte etiketleri ve bağlantılarla çalışan tüm presenter metotları destekler, yani `n:href`, `{link}`, `{plink}`, `link()`, `lazyLink()`, `isLinkCurrent()`, `redirect()`, `redirectPermanent()`, `forward()`, `canonicalize()` ve ayrıca [#LinkGenerator]. Yani örneklerde `n:href` kullanılsa da, orada bu fonksiyonlardan herhangi biri olabilirdi. - -Temel biçim dolayısıyla `Presenter:eylem`'dir: - -```latte -ana sayfa -``` - -Geçerli presenter'ın bir eylemine bağlantı veriyorsak adını atlayabiliriz: - -```latte -ana sayfa -``` - -Hedef eylem `default` ise onu atlayabiliriz, ama iki nokta üst üste kalmalıdır: - -```latte -ana sayfa -``` - -Bağlantılar başka [modüllere |directory-structure#Presenter'lar ve şablonlar] de gidebilir. Burada bağlantılar, iç içe bir alt modüle göreli veya mutlak olmak üzere ayrılır. İlke, disk yollarına benzer, yalnızca eğik çizgi yerine iki nokta üst üste kullanılır. Geçerli presenter'ın `Front` modülünün parçası olduğunu varsayarsak şöyle yazarız: - -```latte -Front:Shop:Product:show bağlantısı -Admin:Product:show bağlantısı -``` - -Özel bir durum, hedef olarak `this` belirttiğimiz [kendine giden |#Geçerli sayfaya bağlantı] bağlantıdır. - -```latte -yenile -``` - -Diyez işareti `#` ardından gelen fragman ile sayfanın belirli bir bölümüne bağlantı verebiliriz: - -```latte -Home:default'a ve #main fragmanına bağlantı -``` - -.{data-version:3.3.0} -Fragman, `#` anahtarıyla argüman olarak dinamik de belirlenebilir. Değeri otomatik kodlanır ve hedefte belirtilen fragmandan önceliklidir: - -```php -$this->link('Home:default', ['#' => $fragment]); -``` - - -Mutlak yollar -============= - -`link()` veya `n:href` ile üretilen bağlantılar her zaman mutlak yollardır (yani `/` ile başlarlar), ama `https://domain` gibi protokol ve alan adı içeren mutlak URL'ler değildir. - -Mutlak bir URL üretmek için başına iki eğik çizgi ekleyin (örneğin `n:href="//Home:"`). Alternatif olarak, `$this->absoluteUrls = true` ayarlayarak presenter'ı yalnızca mutlak bağlantı üretmeye geçirebilirsiniz. - -Şablonda göreli bir yolu mutlak yola dönüştürmek için `|absoluteUrl` filtresi de kullanılabilir. - - -Geçerli sayfaya bağlantı -======================== - -`this` hedefi, geçerli sayfaya bir bağlantı oluşturur: - -```latte -yenile -``` - -Bu sırada `action()` veya (`action()` tanımlı değilse) `render()` metodunun imzasında belirtilen tüm parametreler aktarılır. Yani `id: 123` ile `Product:show` sayfasındaysak, `this` bağlantısı bu parametreyi de aktarır. - -Elbette parametreleri doğrudan belirtmek de mümkündür: - -```latte -yenile -``` - -`isLinkCurrent()` fonksiyonu, bağlantının hedefinin geçerli sayfayla aynı olup olmadığını denetler. Bu, örneğin şablonda bağlantıları ayırt etmek için kullanılabilir. - -Parametreleri `link()` metodununkiyle aynıdır, ama somut bir eylem yerine, o presenter'ın herhangi bir eylemi anlamına gelen `*` joker karakteri de kullanılabilir. - -```latte -{if !isLinkCurrent('Admin:login')} - Giriş -{/if} - -
  • - ... -
  • -``` - -Tek bir elemanda `n:href` ile birlikte kısaltılmış bir biçim kullanılabilir: - -```latte -... -``` - -`*` joker karakteri yalnızca eylemin yerine kullanılabilir, presenter'ın yerine değil. - -Belirli bir modülde veya onun alt modülünde olup olmadığımızı belirlemek için `isModuleCurrent(modulAdi)` metodunu kullanın. - -```latte -
  • - ... -
  • -``` - - -Bağlantı tabanını değiştirme .{data-version:3.2.7} -================================================== - -Varsayılan olarak göreli bağlantılar geçerli presenter'dan türetilir. Bu, `{linkBase}` ile değiştirilebilir: - -```latte -{linkBase Admin:Dashboard} -ürün detayı -``` - -Bağlantı `Admin:Dashboard:Product:show`'a gidecektir. Yalnızca göreli bağlantılar etkilenir; iki nokta üst üste ile başlayan mutlak bağlantılar ve geçerli presenter'a giden bağlantılar (`this`, `show`) değişmeden kalır. - -`{linkBase}` tüm şablon için geçerlidir ve özellikle layout şablonlarında kullanışlıdır; orada çağıran presenter'dan bağımsız olarak tutarlı bağlantılar sağlar. -Etiket şablonun başına konmalıdır, aksi halde `CompileException` fırlatır. - - -Sinyallere bağlantılar -====================== - -Bağlantının hedefi yalnızca bir presenter ve eylem olmak zorunda değildir; bir [sinyal |components#Sinyal] de olabilir (`handle()` metodunu çağırır). O zaman sözdizimi şöyledir: - -``` -[//] [alt-bileşen:]sinyal! [#fragman] -``` - -Sinyal, ünlem işaretiyle ayırt edilir: - -```latte -sinyal -``` - -Bir alt bileşenin (veya alt alt bileşenin) sinyaline de bağlantı oluşturabilirsiniz: - -```latte -sinyal -``` - - -Bileşende bağlantılar -===================== - -[Bileşenler|components] çevredeki presenter'larla hiçbir bağı olmaması gereken ayrı, yeniden kullanılabilir birimler olduğundan, burada bağlantılar biraz farklı çalışır. Latte niteliği `n:href` ve `{link}` etiketi, ayrıca `link()` gibi bileşen metotları **bağlantı hedefini her zaman sinyal adı olarak kabul eder**. Bu yüzden ünlem işareti eklemek bile gerekmez: - -```latte -sinyal, eylem değil -``` - -Bileşen şablonunda presenter'lara bağlantı vermek istersek `{plink}` etiketini kullanırız: - -```latte -ana sayfa -``` - -ya da kodda - -```php -$this->getPresenter()->link('Home:default') -``` - - -Takma adlar .{data-version:3.2.3} -================================= - -Bazen bir Presenter:eylem çiftine kolay akılda kalan bir takma ad vermek yararlı olabilir. Örneğin `Front:Home:default` ana sayfasını basitçe `home`, `Admin:Dashboard:default`'u ise `admin` diye adlandırmak. - -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 ise et işaretiyle yazılırlar, örneğin: - -```latte -yönetim -``` - -`redirect()` ve benzeri gibi bağlantılarla çalışan tüm metotlarda da desteklenirler. - - -Geçersiz bağlantılar -==================== - -Geçersiz bir bağlantı oluşturmamız olabilir; ya var olmayan bir presenter'a gittiği için, ya hedef metodun imzasında kabul ettiğinden fazla parametre aktardığı için, ya da hedef eylem için URL üretilemediği için. Geçersiz bağlantılarla nasıl başa çıkılacağı presenter'da `$this->invalidLinkMode` ile ayarlanır. Şu değerlerin (sabitlerin) bir bileşimini alabilir: - -- `Presenter::InvalidLinkSilent` - sessiz mod, URL olarak # karakterini döndürür -- `Presenter::InvalidLinkWarning` - bir E_USER_WARNING uyarısı fırlatılır; üretim modunda günlüğe yazılır, ama betiğin çalışmasını kesmez -- `Presenter::InvalidLinkTextual` - görsel uyarı, hatayı doğrudan bağlantının içine yazdırır -- `Presenter::InvalidLinkException` - InvalidLinkException fırlatır - -Varsayılan ayar, üretim modunda `InvalidLinkWarning`, geliştirme modunda ise `InvalidLinkWarning | InvalidLinkTextual`'dır. Üretim ortamında `InvalidLinkWarning` betiğin kesilmesine yol açmaz, ama uyarı günlüğe yazılır. Geliştirme ortamında [Tracy |tracy:] onu yakalar ve bir bluescreen gösterir. `InvalidLinkTextual`, URL olarak `#error:` karakterleriyle başlayan bir hata mesajı döndürerek çalışır. Böyle bağlantıların ilk bakışta göze çarpması için CSS'inize şunu ekleyin: - -```css -a[href^="#error:"] { - background: red; - color: white; -} -``` - -Geliştirme ortamında uyarı üretilmesini istemiyorsak, onları doğrudan [yapılandırmada|configuration] bastırabiliriz. - -```neon -application: - silentLinks: true -``` - - -LinkGenerator -============= - -`link()` metodundaki rahatlıkla, ama presenter olmadan nasıl bağlantı oluşturulur? İşte [api:Nette\Application\LinkGenerator] bunun içindir. - -LinkGenerator, constructor üzerinden size aktarılabilen ve sonra `link()` metoduyla bağlantı oluşturabileceğiniz bir servistir. - -Presenter'lara kıyasla bir fark vardır. LinkGenerator tüm bağlantıları doğrudan mutlak URL olarak oluşturur. Ayrıca "geçerli presenter" diye bir şey yoktur, bu yüzden hedef olarak yalnızca eylem adını `link('default')` belirtmek veya modüllere göreli yollar kullanmak mümkün değildir. - -Geçersiz bağlantılar her zaman `Nette\Application\UI\InvalidLinkException` fırlatır. diff --git a/application/tr/directory-structure.texy b/application/tr/directory-structure.texy deleted file mode 100644 index 5b65e0b64f..0000000000 --- a/application/tr/directory-structure.texy +++ /dev/null @@ -1,526 +0,0 @@ -Uygulamanın dizin yapısı -************************ - -
    - -Nette Framework projelerinde anlaşılır ve ölçeklenebilir bir dizin yapısı nasıl tasarlanır? Size kodunuzu düzenlemenize yardım edecek kanıtlanmış uygulamaları göstereceğiz. Şunları öğreneceksiniz: - -- uygulamayı dizinlere **mantıklı biçimde nasıl yapılandıracağınızı** -- yapıyı, proje büyüdükçe **iyi ölçeklenecek** şekilde nasıl tasarlayacağınızı -- **olası alternatiflerin** neler olduğunu ve avantajlarıyla dezavantajlarını - -
    - - -Nette Framework'ün kendisinin belirli bir yapıyı dayatmadığını belirtmek önemli. Her ihtiyaca ve tercihe kolayca uyarlanabilecek şekilde tasarlanmıştır. - - -Temel proje yapısı -================== - -Nette Framework sabit bir dizin yapısı dayatmasa da, [Web Project|https://github.com/nette/web-project] biçiminde kanıtlanmış bir varsayılan düzen vardır: - -/--pre -web-project/ -├── app/ ← uygulama dizini -├── assets/ ← SCSS, JS dosyaları, görseller..., alternatif olarak resources/ -├── bin/ ← komut satırı betikleri -├── config/ ← yapılandırma -├── log/ ← günlüklenen hatalar -├── temp/ ← geçici dosyalar, önbellek -├── tests/ ← testler -├── vendor/ ← Composer ile kurulan 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ırın veya taşıyın. Sonra yalnızca `Bootstrap.php` ve gerekirse `composer.json` içindeki dizin göreli yollarını düzeltmeniz yeterli. Başka bir şey gerekmez, karmaşık bir yeniden yapılandırma yok, sabitlerde değişiklik yok. Nette akıllı otomatik algılamaya sahiptir ve uygulamanın konumunu, temel URL'si dahil, kendiliğinden tanır. - - -Kodu düzenleme ilkeleri -======================= - -Yeni bir projeyi ilk kez incelediğinizde hızlıca yolunuzu bulabilmelisiniz. `app/Model/` dizinine tıkladığınızı ve şu yapıyı gördüğünüzü düşünün: - -/--pre -app/Model/ -├── Services/ -├── Repositories/ -└── Entities/ -\-- - -Bundan yalnızca projenin bazı servisler, repository'ler ve varlıklar kullandığını öğrenirsiniz. Uygulamanın asıl amacı hakkında hiçbir şey öğrenemezsiniz. - -Farklı bir yaklaşıma bakalım: **alan adına göre düzenleme**: - -/--pre -app/Model/ -├── Cart/ -├── Payment/ -├── Order/ -└── Product/ -\-- - -Burada durum farklı: ilk bakışta bunun bir e-ticaret sitesi olduğu bellidir. Dizin adlarının kendisi uygulamanın neler yapabildiğini ele verir: ödemelerle, siparişlerle ve ürünlerle çalışıyor. - -İlk yaklaşım (sınıf tipine göre düzenleme) pratikte birkaç sorun getirir: mantıksal olarak ilişkili kod farklı klasörlere dağılır ve aralarında gidip gelmeniz gerekir. Bu yüzden alan adına göre düzenleyeceğiz. - - -İsim alanları -------------- - -Dizin yapısının uygulamadaki isim alanlarına karşılık gelmesi âdettendir. Yani dosyaların fiziksel konumu isim alanlarıyla uyuşur. Örneğin `app/Model/Product/ProductRepository.php` konumundaki bir sınıfın isim alanı `App\Model\Product` olmalıdır. Bu ilke kodda gezinmeye yardım eder ve otomatik yüklemeyi basitleştirir. - - -Adlarda tekil mi çoğul mu -------------------------- - -Uygulamanın ana dizinlerinde tekil kullandığımıza dikkat edin: `app`, `config`, `log`, `temp`, `www`. Aynısı uygulamanın içi için de geçerlidir: `Model`, `Core`, `Presentation`. Bunun nedeni her birinin tek bir bütünlüklü kavramı temsil etmesidir. - -Benzer şekilde `app/Model/Product`, ürünlerle ilgili her şeyi temsil eder. Ona `Products` demiyoruz, çünkü ürünlerle dolu bir klasör değil (öyle olsaydı `nokia.php`, `samsung.php` gibi dosyalar içerirdi). Ürünlerle çalışmaya yarayan sınıfları içeren bir isim alanıdır: `ProductRepository.php`, `ProductService.php`. - -`app/Tasks` klasörü çoğuldur, çünkü ayrı çalıştırılabilir betiklerden oluşan bir küme içerir: `CleanupTask.php`, `ImportTask.php`. Her biri bağımsız bir birimdir. - -Tutarlılık için şunu öneririz: -- İşlevsel bir birimi temsil eden isim alanlarında tekil (birden fazla varlıkla çalışsa bile) -- Bağımsız birim koleksiyonlarında çoğul -- Kararsız kaldığınızda ya da düşünmek istemediğinizde tekili seçin - - -Genel dizin `www/` -================== - -Bu dizin, web'den erişilebilen tek dizindir (document-root). `www/` yerine sıkça `public/` adıyla karşılaşabilirsiniz; bu yalnızca bir uzlaşım meselesidir ve uygulamanın işleyişini etkilemez. Dizin şunları içerir: -- Uygulamanın [giriş noktası |bootstrapping#index.php] `index.php` -- mod_rewrite kurallarını içeren `.htaccess` dosyası (Apache için) -- Statik dosyalar (CSS, JavaScript, görseller) -- Yüklenen dosyalar - -Uygulamanın güvenliği için [document-root'un doğru yapılandırılmış olması |nette:troubleshooting#URL'den www Dizini Nasıl Değiştirilir ya da Kaldırılır?] çok önemlidir. - -.[note] -`node_modules/` klasörünü asla bu dizine koymayın; çalıştırılabilir olabilecek binlerce dosya içerir ve herkese açık olmamalıdır. - - -Uygulama dizini `app/` -====================== - -Bu, uygulama kodunu içeren ana dizindir. Temel yapı: - -/--pre -app/ -├── Core/ ← altyapı konuları -├── Model/ ← iş mantığı -├── Presentation/ ← presenter'lar ve şablonlar -├── Tasks/ ← komut betikleri -└── Bootstrap.php ← uygulamanın başlatıcı sınıfı -\-- - -`Bootstrap.php`, ortamı hazırlayan, yapılandırmayı yükleyen ve DI konteynerini oluşturan [uygulama başlatma sınıfıdır|bootstrapping]. - -Şimdi tek tek alt dizinlere daha ayrıntılı bakalım. - - -Presenter'lar ve şablonlar -========================== - -Uygulamanın sunum bölümü `app/Presentation` dizinindedir. Alternatifi daha kısa olan `app/UI`'dır. Burası tüm presenter'ların, şablonlarının ve ilişkili yardımcı sınıfların yeridir. - -Bu katmanı alan adına göre düzenleriz. E-ticaret sitesini, blogu ve API'yi birleştiren karmaşık bir projede yapı şöyle görünürdü: - -/--pre -app/Presentation/ -├── Shop/ ← e-ticaret frontend'i -│ ├── Product/ -│ ├── Cart/ -│ └── Order/ -├── Blog/ ← blog -│ ├── Home/ -│ └── Post/ -├── Admin/ ← yönetim -│ ├── Dashboard/ -│ └── Products/ -└── Api/ ← API uç noktaları - └── V1/ -\-- - -Buna karşılık basit bir blog için şu yapıyı kullanırdık: - -/--pre -app/Presentation/ -├── Front/ ← sitenin frontend'i -│ ├── Home/ -│ └── Post/ -├── Admin/ ← yönetim -│ ├── Dashboard/ -│ └── Posts/ -├── Error/ -└── Export/ ← RSS, site haritaları vb. -\-- - -`Home/` veya `Dashboard/` gibi klasörler presenter'ları ve şablonları içerir. `Front/`, `Admin/` veya `Api/` gibi klasörlere **modül** denir. Teknik olarak bunlar, uygulamayı mantıksal olarak bölmeye yarayan sıradan dizinlerdir. - -Presenter içeren her klasör, presenter dosyasının kendisini ve şablonlarını barındırır. Örneğin `Dashboard/` klasörü şunları içerir: - -/--pre -Dashboard/ -├── DashboardPresenter.php ← presenter -└── default.latte ← şablon -\-- - -Bu dizin yapısı sınıfların isim alanlarına yansır. Örneğin `DashboardPresenter`, `App\Presentation\Admin\Dashboard` isim alanındadır (bkz. [#Presenter mapping]): - -```php -namespace App\Presentation\Admin\Dashboard; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -`Admin` modülündeki `Dashboard` presenter'ına uygulamada iki nokta üst üste gösterimiyle `Admin:Dashboard` diye başvururuz. Onun `default` eylemine ise `Admin:Dashboard:default` denir. İç içe modüllerde birden fazla iki nokta üst üste kullanırız, örneğin `Shop:Order:Detail:default`. - - -Yapının esnek biçimde gelişmesi -------------------------------- - -Bu yapının büyük avantajlarından biri, projenin büyüyen ihtiyaçlarına ne kadar zarif uyum sağlamasıdır. Örnek olarak XML beslemeleri üreten bölümü ele alalım. Başlangıçta basit bir biçimimiz var: - -/--pre -Export/ -├── ExportPresenter.php ← tüm dışa aktarımlar için tek presenter -├── sitemap.latte ← site haritası şablonu -└── feed.latte ← RSS beslemesi şablonu -\-- - -Zamanla daha fazla besleme tipi eklenir ve onlar için daha fazla mantık gerekir... Sorun değil! `Export/` klasörü kolayca bir modüle dönüşür: - -/--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 pürüzsüzdür: yalnızca yeni alt klasörler oluşturun, kodu onlara bölün ve bağlantıları güncelleyin (örneğin `Export:feed`'den `Export:Feed:amazon`'a). Bu sayede yapıyı gerektikçe kademeli olarak genişletebiliriz; iç içelik düzeyi hiçbir şekilde sınırlı değildir. - -Örneğin yönetim panelinde sipariş yönetimiyle ilgili `OrderDetail`, `OrderEdit`, `OrderDispatch` gibi birçok presenter'ınız varsa, daha iyi düzen için `Order` adlı bir modül (klasör) oluşturabilirsiniz; bu modül `Detail`, `Edit`, `Dispatch` ve diğer presenter'ların (klasörlerini) içerecektir. - - -Şablonların konumu ------------------- - -Önceki örneklerde şablonların doğrudan presenter'ın bulunduğu klasörde olduğunu gördük: - -/--pre -Dashboard/ -├── DashboardPresenter.php ← presenter -├── DashboardTemplate.php ← isteğe bağlı şablon sınıfı -└── default.latte ← şablon -\-- - -Bu konum pratikte en kullanışlısı olduğunu kanıtlar: ilişkili tüm dosyalar elinizin altındadır. - -Alternatif olarak şablonları bir `templates/` alt klasörüne koyabilirsiniz. Nette her iki seçeneği de destekler. Şablonları tamamen `Presentation/` klasörünün dışına bile koyabilirsiniz. Şablon konumu seçenekleriyle ilgili her şeyi [Şablon arama |templates#Şablon arama] bölümünde bulabilirsiniz. - - -Yardımcı sınıflar ve bileşenler -------------------------------- - -Presenter'lar ve şablonlar sıklıkla başka yardımcı dosyalarla birlikte gelir. Onları kapsamlarına göre mantıklı biçimde yerleştiririz: - -1. **Doğrudan presenter'ın yanına**, o presenter'a özgü bileşenler söz konusuysa: - -/--pre -Product/ -├── ProductPresenter.php -├── ProductGrid.php ← ürün listelemesi bileşeni -└── FilterForm.php ← filtreleme formu -\-- - -2. **Modül için** - alfabetik olarak başta yer aldığı için elverişli olan `Accessory` klasörünü kullanmanızı öneririz: - -/--pre -Front/ -├── Accessory/ -│ ├── NavbarControl.php ← frontend bileşenleri -│ └── TemplateFilters.php -├── Product/ -└── Cart/ -\-- - -3. **Tüm uygulama için** - `Presentation/Accessory/` içinde: -/--pre -app/Presentation/ -├── Accessory/ -│ ├── LatteExtension.php -│ └── TemplateFilters.php -├── Front/ -└── Admin/ -\-- - -Alternatif olarak `LatteExtension.php` veya `TemplateFilters.php` gibi yardımcı sınıfları `app/Core/Latte/` altyapı klasörüne koyabilirsiniz. Bileşenleri de `app/Components` içine. Seçim ekip uzlaşımlarına bağlıdır. - - -Model - uygulamanın kalbi -========================= - -Model, uygulamanın tüm iş mantığını içerir. Onu düzenleme kuralı yine aynıdır: alan adına göre yapılandırmak: - -/--pre -app/Model/ -├── Payment/ ← ödemelerle ilgili her şey -│ ├── PaymentFacade.php ← ana giriş noktası -│ ├── PaymentRepository.php -│ ├── Payment.php ← varlık -├── Order/ ← siparişlerle ilgili her şey -│ ├── OrderFacade.php -│ ├── OrderRepository.php -│ ├── Order.php -└── Shipping/ ← kargoyla ilgili her şey -\-- - -Model'de genellikle şu sınıf tipleriyle karşılaşırsınız: - -**Facade'lar**: uygulamadaki belirli bir alana giden ana giriş noktasını temsil ederler. Bir orkestra şefi gibi davranır, tam kullanım senaryolarını ("sipariş oluştur" veya "ödemeyi işle") gerçekleştirmek için çeşitli servisler arasındaki iş birliğini koordine ederler. Facade, bu orkestrasyon katmanının altında uygulama ayrıntılarını uygulamanın geri kalanından gizler ve böylece ilgili alanla çalışmak için temiz bir arayüz sunar. - -```php -class OrderFacade -{ - public function createOrder(Cart $cart): Order - { - // doğrulama - // siparişin oluşturulması - // e-posta gönderimi - // istatistiklere yazma - } -} -``` - -**Servisler**: bir alan içindeki belirli iş operasyonlarına odaklanır. Tüm kullanım senaryolarını yöneten facade'ların aksine, bir servis somut bir iş mantığını (fiyat hesaplaması veya ödeme işleme gibi) uygular. Servisler genellikle durumsuzdur ve daha karmaşık işlemler için yapı taşı olarak facade'lar tarafından ya da daha basit görevler için doğrudan uygulamanın başka bölümleri tarafından kullanılabilir. - -```php -class PricingService -{ - public function calculateTotal(Order $order): Money - { - // fiyat hesaplaması - } -} -``` - -**Repository'ler**: veri deposuyla, genellikle bir veritabanıyla, tüm iletişimi üstlenirler. Görevleri varlıkları yüklemek, kaydetmek ve onları aramaya yarayan metotları uygulamaktır. Bir repository, uygulamanın geri kalanını veritabanının uygulama ayrıntılarından korur ve veriyle çalışmak için nesne yönelimli bir arayüz sunar. - -```php -class OrderRepository -{ - public function find(int $id): ?Order - { - } - - public function findByCustomer(int $customerId): array - { - } -} -``` - -**Varlıklar**: uygulamadaki başlıca iş kavramlarını temsil eden, kendi kimliği olan ve zamanla değişen nesneler. Genellikle bir ORM (Nette Database Explorer veya Doctrine gibi) ile veritabanı tablolarına eşlenen sınıflardır. Varlıklar, verileriyle ilgili iş kurallarını ve doğrulama mantığını içerebilir. - -```php -// 'orders' veritabanı tablosuna 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, - ]); - } -} -``` - -**Value Object'ler**: kendi kimliği olmayan değerleri temsil eden değişmez nesneler; örneğin bir para tutarı veya bir e-posta adresi. Aynı değerlere sahip iki value object örneği özdeş sayılır. - - -Altyapı kodu -============ - -`Core/` klasörü (ya da alternatif olarak `Infrastructure/`) uygulamanın teknik temelinin yuvasıdır. Altyapı kodu genellikle ş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ükleme ve izleme -│ ├── SentryLogger.php -│ └── FileLogger.php -├── Cache/ ← önbellek katmanı -│ └── FullPageCache.php -└── Integration/ ← dış servislerle entegrasyon - ├── Slack/ - └── Stripe/ -\-- - -Daha küçük projelerde doğal olarak düz bir yapı yeterlidir: - -/--pre -Core/ -├── RouterFactory.php -├── Authenticator.php -└── QueueMailer.php -\-- - -Bu, şöyle bir koddur: - -- Teknik altyapıyı üstlenir (yönlendirme, günlükleme, önbellekleme) -- Dış servisleri entegre eder (Sentry, Elasticsearch, Redis) -- Tüm uygulama için temel servisleri sağlar (posta, veritabanı) -- Çoğunlukla belirli bir alandan bağımsızdır; önbellek veya günlükleyici bir e-ticaret sitesinde de blogda da aynı çalışır. - -Belirli bir sınıfın buraya mı yoksa model'e mi ait olduğunu merak mı ediyorsunuz? Temel fark şudur: `Core/` içindeki kod: - -- Alan hakkında hiçbir şey bilmez (ürünler, siparişler, makaleler) -- Genellikle başka bir projeye taşınabilir -- "Nasıl çalıştığını" çözer (bir e-posta nasıl gönderilir), "ne yaptığını" değil (hangi e-posta gönderilir) - -Daha iyi anlamak için bir örnek: - -- `App\Core\MailerFactory` - e-posta göndermeye yarayan sınıfın örneklerini oluşturur, SMTP ayarlarını üstlenir -- `App\Model\OrderMailer` - siparişlerle ilgili e-postaları göndermek için `MailerFactory`'yi kullanır, şablonlarını ve ne zaman gönderilmeleri gerektiğini bilir - - -Komut betikleri -=============== - -Uygulamaların sıklıkla olağan HTTP isteklerinin dışında işler yapması gerekir; ister arka planda veri işleme, ister bakım, ister düzenli görevler olsun. Çalıştırmak için `bin/` dizinindeki basit betikler kullanılır, asıl uygulama mantığı ise `app/Tasks/` (veya `app/Commands/`) içine konur. - -Örnek: - -/--pre -app/Tasks/ -├── Maintenance/ ← bakım betikleri -│ ├── CleanupCommand.php ← eski verilerin silinmesi -│ └── DbOptimizeCommand.php ← veritabanı iyileştirmesi -├── Integration/ ← dış sistemlerle entegrasyon -│ ├── ImportProducts.php ← tedarikçi sisteminden içe aktarım -│ └── SyncOrders.php ← siparişlerin eşitlenmesi -└── Scheduled/ ← düzenli görevler - ├── NewsletterCommand.php ← bültenlerin gönderimi - └── ReminderCommand.php ← müşteri bildirimleri -\-- - -Model'e ne, komut betiklerine ne ait? Örneğin tek bir e-posta gönderme mantığı model'in parçasıdır, binlerce e-postanın toplu gönderimi ise `Tasks/`'a aittir. - -Görevler genellikle komut satırından veya cron ile çalıştırılır: `bin/` içindeki betik, [bootConsoleApplication() |bootstrapping#Farklı ortamlar] metoduyla DI konteynerini oluşturur ve gereken servisi ondan çeker. Bir HTTP isteğiyle de çalıştırılabilirler, ama güvenlik göz önünde bulundurulmalıdır. Görevi çalıştıran presenter'ın güvenliği sağlanmalıdır; örneğin yalnızca oturum açmış kullanıcılar için ya da güçlü bir belirteç ve izin verilen IP adreslerinden erişimle. Uzun süren görevlerde betiğin zaman sınırını artırmak ve oturumu kilitlememek için `session_write_close()` kullanmak gerekir. - - -Olası diğer dizinler -==================== - -Sözü geçen temel dizinlerin yanı sıra, projenin ihtiyaçlarına göre başka özel klasörler ekleyebilirsiniz. En yaygınlarına ve kullanımlarına bakalım: - -/--pre -app/ -├── Api/ ← sunum katmanından bağımsız API mantığı -├── Database/ ← migration betikleri ve test verisi için seeder'lar -├── Components/ ← tüm uygulamada paylaşılan görsel bileşenler -├── Event/ ← olay güdümlü bir mimari kullanılıyorsa yararlı -├── Mail/ ← e-posta şablonları ve ilgili mantık -└── Utils/ ← yardımcı sınıflar -\-- - -Uygulama genelindeki presenter'larda 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 listelemeleri için bileşenler -│ └── DataGrid.php -└── Navigation/ ← gezinme öğeleri - ├── Breadcrumbs.php - └── Menu.php -\-- - -Daha karmaşık mantığa sahip bileşenler buraya aittir. Bileşenleri birden fazla proje arasında paylaşmak isterseniz, onları ayrı bir Composer paketine çıkarmanız yerinde olur. - -`app/Mail` dizinine e-posta iletişiminin yönetimini koyabilirsiniz: - -/--pre -app/Mail/ -├── templates/ ← e-posta şablonları -│ ├── order-confirmation.latte -│ └── welcome.latte -└── OrderMailer.php -\-- - - -Presenter mapping -================= - -Mapping, sınıf adının presenter adından türetilme kurallarını tanımlar. Onları [yapılandırmada|configuration] `application › mapping` anahtarı altında belirtiriz. - -Bu sayfada presenter'ları `app/Presentation` (veya `app/UI`) klasörüne koyduğumuzu gösterdik. Nette Application 3.3'ten beri bu, yapılandırılması gerekmeyen varsayılan uzlaşımdır. Farklı bir yapı kullanıyorsanız veya mapping'i açıkça belirtmek istiyorsanız, varsayılan ayar şu satıra karşılık gelir: - -```neon -application: - mapping: App\Presentation\*\**Presenter -``` - -Mapping nasıl çalışır? Daha iyi anlamak için önce modülsüz bir uygulama düşünelim. Presenter sınıflarının `App\Presentation` isim alanına düşmesini istiyoruz, böylece `Home` presenter'ı `App\Presentation\HomePresenter` sınıfına eşlensin. Bu, şu yapılandırmayla sağlanır: - -```neon -application: - mapping: App\Presentation\*Presenter -``` - -Mapping, `App\Presentation\*Presenter` maskesindeki yıldızın presenter adı `Home` ile değiştirilmesiyle çalışır ve sonuçta `App\Presentation\HomePresenter` sınıf adı elde edilir. Basit! - -Ancak bu ve diğer bölümlerdeki örneklerde gördüğünüz gibi, presenter sınıflarını aynı adlı alt dizinlere koyuyoruz; örneğin `Home` presenter'ı `App\Presentation\Home\HomePresenter` sınıfına eşlenir. Bunu çift yıldız `**` kullanarak sağlarız (Nette Application 3.2.3 gerektirir): - -```neon -application: - mapping: App\Presentation\**Presenter -``` - -Şimdi presenter'ları modüllere eşlemeye geçiyoruz. Her modül için özel bir mapping 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` presenter'ı `App\Presentation\Front\Home\HomePresenter` sınıfına, `Api:OAuth` presenter'ı ise `App\Api\OAuthPresenter` sınıfına eşlenir. - -`Front` ve `Admin` modüllerinin mapping deseni benzer olduğundan ve büyük olasılıkla böyle daha çok modül olacağından, onların yerine geçen genel bir kural oluşturmak mümkündür. Sınıf maskesine modül için yeni bir yıldız eklenir: - -```neon -application: - mapping: - *: App\Presentation\*\**Presenter - Api: App\Api\*Presenter -``` - -Bu, daha derin iç içe dizin yapılarında da çalışır; örneğin `Admin:User:Edit` presenter'ında yıldızlı bölüm her modül düzeyi için yinelenir ve sonuçta `App\Presentation\Admin\User\Edit\EditPresenter` sınıfı elde edilir. - -Alternatif bir yazım, dize yerine üç bölümden oluşan bir dizi kullanmaktır. Yukarıda gösterilen örnekler için bu yazım öncekiyle 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 deleted file mode 100644 index ef92ef9b8a..0000000000 --- a/application/tr/how-it-works.texy +++ /dev/null @@ -1,200 +0,0 @@ -Uygulamalar nasıl çalışır? -************************** - -
    - -Şu anda Nette dokümantasyonunun temel bölümünü okuyorsunuz. Web uygulamalarının nasıl çalıştığının tüm ilkelerini, bir isteğin doğduğu andan PHP betiğinin çalışmasını bitirdiği ana kadar A'dan Z'ye öğreneceksiniz. Okuduktan sonra şunları anlayacaksınız: - -- her şeyin nasıl çalıştığını -- Bootstrap'ın, Presenter'ın ve DI konteynerinin ne olduğunu -- dizin yapısının nasıl göründüğünü - -
    - - -Dizin yapısı -============ - -[WebProject|https://github.com/nette/web-project] adlı örnek web uygulaması iskeletini açın. Okurken sözü geçen dosyalara bakabilirsiniz. - -Dizin yapısı aşağı yukarı şöyle görünür: - -/--pre -web-project/ -├── app/ ← uygulama dizini -│ ├── Core/ ← çalışma için gerekli çekirdek sınıflar -│ │ └── RouterFactory.php ← URL adreslerinin yapılandırması -│ ├── Presentation/ ← presenter'lar, şablonlar vb. -│ │ ├── @layout.latte ← layout şablonu -│ │ └── Home/ ← Home presenter'ının dizini -│ │ ├── HomePresenter.php ← Home presenter sınıfı -│ │ └── default.latte ← default eylemi için şablon -│ └── Bootstrap.php ← başlatıcı sınıf Bootstrap -├── assets/ ← kaynaklar (SCSS, TypeScript, kaynak görseller) -├── bin/ ← komut satırından çalıştırılan betikler -├── config/ ← yapılandırma dosyaları -│ ├── common.neon -│ └── services.neon -├── log/ ← günlüklenen hatalar -├── temp/ ← geçici dosyalar, önbellek, … -├── vendor/ ← Composer ile kurulan kütüphaneler -│ ├── ... -│ └── autoload.php ← kurulu tüm paketlerin otomatik yüklenmesi -├── www/ ← genel dizin, projenin document root'u -│ ├── assets/ ← derlenmiş statik dosyalar (CSS, JS, görseller, ...) -│ ├── .htaccess ← mod_rewrite kuralları -│ └── index.php ← uygulamayı başlatan ilk dosya -└── .htaccess ← www dışındaki tüm dizinlere erişimi yasaklar -\-- - -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ılamaya sahiptir ve uygulamanın konumunu, URL tabanı dahil, kendiliğinden tanır. - -Biraz daha büyük uygulamalarda presenter ve şablon klasörlerini [alt dizinlere |directory-structure#Presenter'lar ve şablonlar] ayırabilir ve sınıfları modül dediğimiz isim alanlarına gruplayabiliriz. - -`www/` dizini projenin genel dizinini, yani document-root'unu temsil eder. Uygulama tarafında başka bir şey yapılandırmaya gerek kalmadan yeniden adlandırabilirsiniz. Yalnızca document-root'un bu dizini göstermesi için [hosting'i yapılandırmanız |nette:troubleshooting#URL'den www Dizini Nasıl Değiştirilir ya da Kaldırılır?] gerekir. - -WebProject'i, Nette dahil, doğrudan [Composer |best-practices:composer] ile de indirebilirsiniz: - -```shell -composer create-project nette/web-project -``` - -Linux veya macOS'ta `log/` ve `temp/` dizinleri için [yazma izinlerini |nette:troubleshooting#Dizin İzinlerini Ayarlama] ayarlayın. - -WebProject uygulaması çalışmaya hazırdır; hiçbir şey yapılandırmaya gerek yoktur ve `www/` klasörüne giderek doğrudan tarayıcıda görüntüleyebilirsiniz. - - -HTTP isteği -=========== - -Her şey, kullanıcının tarayıcısında bir sayfa açmasıyla başlar. Tarayıcı sunucuya bir HTTP isteği gönderir. Bu istek, genel dizin `www/` içinde yer alan tek bir PHP dosyasını, yani `index.php`'yi hedefler. İsteğin `https://example.com/product/123` adresine geldiğini varsayalım. Uygun [sunucu yapılandırması |nette:troubleshooting#Güzel URL'ler İçin Sunucu Nasıl Yapılandırılır?] sayesinde bu URL de `index.php` dosyasına eşlenir ve o dosya çalıştırılır. - -Görevi şudur: - -1) ortamı hazırlamak -2) factory'yi elde etmek -3) isteği işleyen Nette uygulamasını çalıştırmak - -Ne factory'si? Traktör üretmiyoruz, web siteleri yapıyoruz! Sabırlı olun, birazdan açıklanacak. - -"Ortamın hazırlanması" derken, örneğin hataları günlüklemek veya görselleştirmek için harika bir araç olan [Tracy|tracy:]'nin etkinleştirilmesini kastediyoruz. Üretim sunucusunda hataları günlükler, geliştirme ortamında ise doğrudan gösterir. Dolayısıyla hazırlık, sitenin üretim modunda mı yoksa geliştirme modunda mı çalıştığının belirlenmesini de kapsar. Nette bunun için [akıllı otomatik algılama |bootstrapping#Geliştirme modu ve üretim modu] kullanır: siteyi localhost'ta çalıştırırsanız geliştirme modunda çalışır. Hiçbir şey yapılandırmanız gerekmez ve uygulama hem geliştirmeye hem de canlı yayına hemen hazırdır. Bu adımlar [Bootstrap sınıfı|bootstrapping] bölümünde ayrıntılı olarak yapılır ve anlatılır. - -Üçüncü nokta (evet, ikinciyi atladık, ama ona döneceğiz) uygulamanın başlatılmasıdır. Nette'te HTTP isteklerinin işlenmesinden `Nette\Application\Application` sınıfı (bundan sonra `Application`) sorumludur. Yani uygulamayı çalıştır dediğimizde, tam olarak bu sınıfın bir nesnesi üzerinde adı yerinde olan `run()` metodunu çağırmayı kastediyoruz. - -Nette bir akıl hocası gibi davranır ve sizi kanıtlanmış yöntemlere göre temiz uygulamalar yazmaya yönlendirir. Bunların en yerleşiklerinden biri kısaca DI denilen **bağımlılık enjeksiyonudur**. Şu anda sizi DI'yi açıklayarak yormak istemiyoruz; bunun için [ayrı bir bölüm|dependency-injection:introduction] var. Önemli sonucu şudur: anahtar nesneler genellikle **DI konteyneri** (veya DIC) olarak bilinen bir nesne factory'si tarafından oluşturulur. Evet, daha önce sözü geçen factory budur. `Application` nesnesini de bizim için o üretir, bu yüzden önce konteynere ihtiyacımız var. Onu `Configurator` sınıfıyla elde ederiz, `Application` nesnesini oluşturmasını sağlarız, üzerinde `run()` metodunu çağırırız ve böylece Nette uygulaması başlar. Tam olarak bu, [index.php |bootstrapping#index.php] dosyasında olan şeydir. - - -Nette Application -================= - -`Application` sınıfının tek bir görevi vardır: HTTP isteğine yanıt vermek. - -Nette'te yazılan uygulamalar, presenter denilen birçok parçaya bölünür (başka framework'lerde "controller" terimiyle karşılaşabilirsiniz, aslında aynı şeydir). Bunlar, her biri sitenin belirli bir sayfasını temsil eden sınıflardır: örneğin ana sayfa, bir e-ticaret sitesindeki ürün, giriş formu, site haritası beslemesi vb. Bir uygulamada birden binlerce presenter olabilir. - -`Application`, geçerli isteği hangi presenter'ın işleyeceğine karar vermesi için önce router denilen bileşene sorar. Sorumluluğu router belirler. Girdi olarak gelen `https://example.com/product/123` URL'sini inceler ve yapılandırmasına göre bu işin örneğin `Product` **presenter**'ına ait olduğuna, onun da `id: 123` olan ürün için `show` **eylemini** gerçekleştirmesi gerektiğine karar verir. Presenter + eylem çiftini iki nokta üst üste ile ayırarak yazmak iyi bir alışkanlıktır, örneğin `Product:show`. - -Böylece router, URL'yi `Presenter:eylem` çiftine + parametrelere dönüştürdü; bizim durumumuzda `Product:show` + `id: 123`. Böyle bir router'ın neye benzediğini `app/Core/RouterFactory.php` dosyasında görebilirsiniz, ayrıntılı olarak da [Yönlendirme |Routing] bölümünde anlatıyoruz. - -Devam edelim. `Application` artık presenter'ın adını biliyor ve ilerleyebilir. Bunu, `Product` presenter'ının kodunu içeren `ProductPresenter` sınıfının bir örneğini oluşturarak yapar. Daha doğrusu, presenter'ı oluşturmasını DI konteynerinden ister, çünkü nesne oluşturmak onun işidir. - -Presenter şöyle görünebilir: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ProductRepository $repository, - ) { - } - - public function renderShow(int $id): void - { - // model'den veriyi al ve şablona aktar - $this->template->product = $this->repository->getProduct($id); - } -} -``` - -İsteğin işlenmesini presenter devralır. Görev açıktır: `id: 123` ile `show` eylemini yürütmek. Presenter terminolojisinde bu, `$id` parametresinde `123` alan `renderShow()` metodunun çağrılması demektir. - -Bir presenter birden fazla eylemi işleyebilir, yani birden fazla `render()` metoduna sahip olabilir. Ancak presenter'ları tek veya olabildiğince az eylemle tasarlamanızı öneririz. - -Böylece `renderShow(123)` metodu çağrıldı. Kodu hayali bir örnektir, ama verinin şablona nasıl aktarıldığını, yani `$this->template`'e yazılarak aktarıldığını gösterir. - -Ardından presenter bir yanıt döndürür. Bu bir HTML sayfası, bir görsel, bir XML belgesi, diskten bir dosyanın gönderilmesi, JSON veya belki başka bir sayfaya yönlendirme olabilir. Önemlisi, nasıl yanıt verileceğini açıkça belirtmezsek (ki `ProductPresenter`'da durum budur), yanıt bir şablonun HTML sayfasına render edilmesi olacaktır. Neden? Çünkü vakaların %99'unda bir şablon render etmek isteriz. Bu yüzden presenter, işimizi kolaylaştırmak için bu davranışı varsayılan olarak benimser. Nette'in özü budur. - -Hangi şablonun render edileceğini belirtmemiz bile gerekmez; framework yolu kendiliğinden çıkarır. `show` eylemi durumunda, `ProductPresenter` sınıfıyla aynı dizinde yer alan `show.latte` şablonunu yüklemeyi dener. Layout'u da `@layout.latte` dosyasında bulmaya çalışır ([şablon arama |templates#Şablon arama] hakkında daha fazla ayrıntı). - -Sonra şablonlar render edilir. Bununla presenter'ın ve tüm uygulamanın görevi tamamlanır. Şablon yoksa 404 hata sayfası döndürülür. Presenter'lar hakkında daha fazlasını [Presenter'lar|presenters] sayfasında öğrenebilirsiniz. - -[* request-flow.svg *] - -Emin olmak için, tüm süreci biraz farklı bir URL ile özetleyelim: - -1) URL `https://example.com` -2) Uygulama başlar, DI konteyneri oluşturulur ve `Application::run()` çalıştırılır. -3) Router, URL'yi `Home:default` çiftine çözer. -4) `HomePresenter` sınıfının bir örneği oluşturulur. -5) `renderDefault()` metodu çağrılır (varsa). -6) Şablon, örneğin `default.latte`, layout ile birlikte, örneğin `@layout.latte`, render edilir. - - -Az önce birçok yeni kavramla karşılaşmış olabilirsiniz, ama bunların anlamlı olduğuna inanıyoruz. Nette'te uygulama geliştirmek son derece basittir. - - -Şablonlar -========= - -Şablonlardan söz açılmışken, Nette [Latte |latte:] şablon sistemini kullanır. Bu yüzden şablon dosyalarının uzantısı `.latte`'dir. Latte, hem PHP için en güvenli şablon sistemi olduğu hem de en sezgisel olduğu için tercih edilir. Yeni çok şey öğrenmeniz gerekmez; PHP bilgisi ve birkaç etiket yeterlidir. İhtiyacınız olan her şeyi [dokümantasyonda |templates] bulacaksınız. - -Şablonda başka presenter'lara ve eylemlere şöyle [bağlantı oluşturursunuz |creating-links]: - -```latte -ürün detayı -``` - -Gerçek URL yerine tanıdık `Presenter:eylem` çiftini yazın ve gerekli parametreleri ekleyin. İşin püf noktası, Nette'e bu niteliği işlemesini söyleyen `n:href`'tir. Şunu üretecektir: - -```latte -ürün detayı -``` - -URL üretimini yukarıda sözü geçen router üstlenir. Nette'teki router'lar olağanüstüdür, çünkü yalnızca URL'den `Presenter:eylem` çiftine dönüşümü değil, tersini de yapabilirler: presenter adından, eylemden ve parametrelerden URL üretebilirler. Bu sayede, Nette'te tamamlanmış bir uygulamanın tümündeki URL formatını, şablonlarda veya presenter'larda tek bir karakteri bile değiştirmeden, yalnızca router'ı değiştirerek tamamen dönüştürebilirsiniz. Bu ayrıca, aynı içeriğin farklı URL'lerde var olmasını otomatik olarak engelleyerek SEO'yu (arama motoru optimizasyonunu) iyileştiren, Nette'e özgü bir başka özellik olan kanonikleştirmeyi mümkün kılar. Birçok programcı bu yeteneği hayranlıkla karşılıyor. - - -Etkileşimli bileşenler -====================== - -Presenter'lar hakkında size bir şey daha söylememiz gerekiyor: yerleşik bir bileşen sistemine sahiptirler. Daha deneyimli olanlar buna benzer bir şeyi Delphi'den veya ASP.NET Web Forms'tan hatırlayabilir; React veya Vue.js de bir ölçüde ilgili kavramlar üzerine kurulmuştur. PHP framework'leri dünyasında bu tamamen eşsiz bir özelliktir. - -Bileşenler, sayfalara (yani presenter'lara) gömdüğümüz bağımsız, yeniden kullanılabilir birimlerdir. Bunlar [formlar |forms:in-presenter], [datagrid'ler |https://componette.org/contributte/datagrid/], menüler, anketler, kısacası yeniden kullanmanın anlamlı olduğu her şey olabilir. Kendi bileşenlerimizi oluşturabilir veya açık kaynak bileşenlerin [geniş seçkisinden |https://componette.org] yararlanabiliriz. - -Bileşenler, uygulama geliştirmeye yaklaşımı temelden etkiler. Sayfaları önceden hazırlanmış birimlerden oluşturmak için yeni olanaklar açarlar. Ve [Hollywood |components#Hollywood tarzı] ile de ortak bir yanları vardır. - - -DI konteyneri ve yapılandırma -============================= - -DI konteyneri, yani nesne factory'si, tüm uygulamanın kalbidir. - -Merak etmeyin, önceki satırların düşündürebileceği gibi sihirli bir kara kutu değildir. Gerçekte, Nette tarafından üretilen ve önbellek dizininde saklanan oldukça sıradan bir PHP sınıfıdır. `createServiceAbcd()` gibi adlandırılmış, her biri belirli bir nesneyi oluşturup döndürebilen birçok metot içerir. Evet, uygulamayı çalıştırmak için `index.php`'de ihtiyaç duyduğumuz `Nette\Application\Application` örneğini üreten bir `createServiceApplication__application()` metodu da vardır. Ayrıca tek tek presenter'ları oluşturan metotlar da bulunur, vb. - -DI konteynerinin oluşturduğu nesnelere, bir nedenle, servis denir. - -Bu sınıfın gerçekten özel yanı, onu sizin programlamamanızdır; framework programlar. Gerçekten PHP kodunu üretir ve diske kaydeder. Siz yalnızca konteynerin hangi nesneleri ve tam olarak nasıl oluşturabilmesi gerektiğine dair talimat verirsiniz. Bu talimatlar [yapılandırma dosyalarına |bootstrapping#DI konteynerinin yapılandırması] yazılır; bu dosyalar [NEON|neon:format] formatını kullanır ve bu yüzden `.neon` uzantısı taşır. - -Yapılandırma dosyaları yalnızca DI konteynerine talimat vermeye yarar. Yani örneğin [session |http:configuration#Oturum] bölümünde `expiration: 14 days` seçeneğini belirtirseniz, DI konteyneri oturumu temsil eden `Nette\Http\Session` nesnesini oluştururken onun `setExpiration('14 days')` metodunu çağırır ve böylece yapılandırmayı gerçeğe dönüştürür. - -Neyin [yapılandırılabileceğini |nette:configuring] ve [kendi servislerinizi nasıl tanımlayacağınızı |dependency-injection:services] anlatan koca bir bölüm sizi bekliyor. - -Servis oluşturmaya biraz daldığınızda [autowiring |dependency-injection:autowiring] terimiyle karşılaşacaksınız. Bu, hayatınızı inanılmaz kolaylaştıracak bir özelliktir. Nesneleri, siz hiçbir şey yapmadan, ihtiyaç duyduğunuz yere (örneğin sınıflarınızın constructor'larına) otomatik olarak aktarabilir. Nette'teki DI konteynerinin küçük bir mucize olduğunu keşfedeceksiniz. - - -Sırada ne var? -============== - -Nette uygulamalarının temel ilkelerini ele aldık. Şimdilik yüzeysel bir bakış oldu, ama yakında daha derine inecek ve zamanla harika web uygulamaları oluşturacaksınız. Peki bundan sonra nereye? [İlk uygulamanızı oluşturun|quickstart:] eğitimini denediniz mi? - -Yukarıda anlatılanların yanı sıra Nette, koca bir [kullanışlı sınıf|utils:] cephaneliği, bir [veritabanı katmanı|database:] ve daha fazlasını sunar. Dokümantasyonu tıklayarak gezmeyi deneyin. Ya da [blog|https://blog.nette.org]'a uğrayın. Birçok ilginç şey keşfedeceksiniz. - -Framework size bol keyif getirsin 💙 diff --git a/application/tr/multiplier.texy b/application/tr/multiplier.texy deleted file mode 100644 index 98a05df472..0000000000 --- a/application/tr/multiplier.texy +++ /dev/null @@ -1,63 +0,0 @@ -Multiplier: dinamik bileşenler -****************************** - -.[perex] -Etkileşimli bileşenleri dinamik olarak oluşturmak için bir araç. - -Tipik bir örnekle başlayalım: bir e-ticaret sitesinde ürün listesi düşünün; her ürün için bir "Sepete ekle" formu istiyorsunuz. Olası bir yaklaşım, tüm listeyi tek bir forma sarmaktır. Ancak çok daha kullanışlı bir yöntemi [api:Nette\Application\UI\Multiplier] sunar. - -Multiplier, birden fazla bileşen için rahatça bir factory tanımlamanızı sağlar. İç içe bileşenler ilkesiyle çalışır: [api:Nette\ComponentModel\Container]'dan kalıtım alan her bileşen başka bileşenler içerebilir. - -.[tip] -Dokümantasyondaki [bileşen modeli |components#Bileşenler derinlemesine] bölümüne bakın. - -Multiplier'ın özü, constructor'a verilen bir callback yardımıyla çocuklarını dinamik olarak oluşturabilen bir ebeveyn gibi davranmasıdır. Örneğe bakın: - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function () { - $form = new Nette\Application\UI\Form; - $form->addInteger('amount', 'Miktar:') - ->setRequired(); - $form->addSubmit('send', 'Sepete ekle'); - return $form; - }); -} -``` - -Artık şablonda formu her ürün için kolayca render edebiliriz ve her biri gerçekten benzersiz bir bileşen olacaktır. - -```latte -{foreach $items as $item} -

    {$item->title}

    - {$item->description} - - {control "shopForm-$item->id"} -{/foreach} -``` - -`{control}` etiketinde verilen argüman şu anlama gelen bir formatı izler: - -1. `shopForm` bileşenini al. -2. Ondan `$item->id` adlı çocuğu al. - -**1.** noktasının ilk çağrısında `shopForm` bileşeni henüz yoktur, bu yüzden onun factory'si `createComponentShopForm` çağrılır. Sonra elde edilen bileşen (bir Multiplier örneği) üzerinde, somut form için factory çağrılır; bu da Multiplier'ın constructor'ına verdiğimiz anonim fonksiyondur. - -Foreach döngüsünün bir sonraki turunda `createComponentShopForm` metodu tekrar çağrılmaz (bileşen zaten vardır). Ancak farklı bir çocuk aradığımız için (her turda `$item->id` farklı olacağından), anonim fonksiyon tekrar çağrılır ve yeni bir form döndürür. - -Geriye tek bir şey kalıyor: formun sepete doğru ürünü eklediğinden emin olmak; şu anda form her ürün için aynı. Burada bize Multiplier'ın (ve genel olarak Nette Framework'teki her bileşen factory'sinin) bir özelliği yardımcı olur: her factory, ilk argüman olarak oluşturulan bileşenin adını alır. Multiplier factory'si ayrıca ikinci argüman olarak Multiplier örneğinin kendisini alır. Bizim durumumuzda ilk argüman `$item->id` olacak, ki tam da ihtiyacımız olan bilgi budur. Yani formun oluşturulmasını biraz değiştirmemiz yeterli: - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function (string $itemId) { - $form = new Nette\Application\UI\Form; - $form->addInteger('amount', 'Miktar:') - ->setRequired(); - $form->addHidden('itemId', $itemId); - $form->addSubmit('send', 'Sepete ekle'); - return $form; - }); -} -``` diff --git a/application/tr/presenters.texy b/application/tr/presenters.texy deleted file mode 100644 index eba4206c70..0000000000 --- a/application/tr/presenters.texy +++ /dev/null @@ -1,595 +0,0 @@ -Presenter'lar -************* - -
    - -Nette'te presenter'ların ve şablonların nasıl yazıldığını inceleyeceğiz. Okuduktan sonra şunları anlayacaksınız: - -- presenter'ların nasıl çalıştığını -- kalıcı parametrelerin ne olduğunu -- şablonların nasıl render edildiğini - -
    - -[Presenter'ın |how-it-works#Nette Application] bir web uygulamasının belirli bir sayfasını (ana sayfa, e-ticaret sitesindeki bir ürün, giriş formu, site haritası beslemesi vb.) temsil eden bir sınıf olduğunu artık biliyoruz. Bir uygulamada birden binlerce presenter olabilir. Başka framework'lerde bunlara controller da denir. - -Genellikle presenter terimiyle, web arayüzleri üretmeye uygun olan ve bu bölümün geri kalanının odağı olacak [api:Nette\Application\UI\Presenter] sınıfının bir torununu kastederiz. Genel anlamda presenter, [api:Nette\Application\IPresenter] arayüzünü uygulayan herhangi bir nesnedir. - - -Presenter'ın yaşam döngüsü -========================== - -Presenter'ın görevi bir isteği işlemek ve bir yanıt döndürmektir (bu bir HTML sayfası, bir görsel, bir yönlendirme vb. olabilir). - -Yani önce ona bir istek verilir. Bu doğrudan HTTP isteği değil, HTTP isteğinin router yardımıyla dönüştürüldüğü bir [api:Nette\Application\Request] nesnesidir. Bu nesneyle genellikle doğrudan uğraşmayız, çünkü presenter isteğin işlenmesini şimdi inceleyeceğimiz başka metotlara akıllıca devreder. - -[* lifecycle.svg *] *** Presenter'ın yaşam döngüsü .<> - -Şema, varsa yukarıdan aşağıya sırayla çağrılan metotların listesini gösterir. Hiçbiri zorunlu değildir; tek bir metodu olmayan tamamen boş bir presenter'ınız olabilir ve üzerine basit bir statik site kurabilirsiniz. - - -`__construct()` ---------------- - -Constructor, nesnenin oluşturulduğu anda çağrıldığı için tam olarak presenter'ın yaşam döngüsüne ait değildir. Ancak önemi nedeniyle onu da anıyoruz. Constructor ([inject metoduyla|best-practices:inject-method-attribute] birlikte) bağımlılıkları aktarmaya yarar. - -Presenter, uygulamanın iş mantığını üstlenmemeli, veritabanına yazmamalı veya ondan okumamalı, hesaplama yapmamalıdır. Bu, model dediğimiz katmandaki sınıfların işidir. Örneğin bir `ArticleRepository` sınıfı makaleleri yüklemekten ve kaydetmekten sorumlu olabilir. Presenter'ın onunla çalışabilmesi için ona [bağımlılık enjeksiyonuyla aktarılması |dependency-injection:passing-dependencies] gerekir: - - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articles, - ) { - } -} -``` - - -`startup()` ------------ - -İstek alınır alınmaz `startup()` metodu çağrılır. Onu özellikleri ilklendirmek, kullanıcı izinlerini denetlemek vb. için kullanabilirsiniz. Bu metodun her zaman üst sınıfını çağırması gerekir: `parent::startup()`. - - -`action(args...)` .{toc: action()} -------------------------------------------------- - -`render()` metoduna benzer. `render()` ardından render edilecek belirli bir şablon için veri hazırlamayı amaçlarken, `action()` bir isteği, ardından mutlaka bir şablon render etmeden işler. Örneğin veriyi işleyebilir, kullanıcının oturumunu açıp kapatabilir vb. ve sonra [başka bir yere yönlendirebilir |#Yönlendirme]. - -Önemli olan, `action()`'in `render()`'den *önce* çağrılmasıdır. Bu, eylem metodunun içinde isteğin gidişatını değiştirebilmemizi sağlar; örneğin `setView('digerGorunum')` ile render edilecek şablonu, hatta çağrılacak `render()` metodunu değiştirerek. - -.{data-version:3.2.3} -`switch('digerEylem')` metoduyla tamamen başka bir eyleme bile geçebilirsiniz. Bu, geçerli metodu keser ve onun yerine yeni eylemin `action()` ve `render()` metotlarını çalıştırır (ve otomatik [kanonikleştirmeyi|#Kanonikleştirme] kapatır). İsteğin kendisi devam eder; yalnızca o an çalışan metot kesilir. - -Metoda istekten gelen parametreler aktarılır. Bu parametreler için tip belirtmek mümkündür ve önerilir, örneğin `actionShow(int $id, ?string $slug = null)`. `id` parametresi eksikse veya tam sayı değilse, presenter [404 hatası |#Hata 404 vb.] döndürür ve sonlanır. - - -`handle(args...)` .{toc: handle()} --------------------------------------------------- - -Bu metot, [bileşenlere |components#Sinyal] ayrılmış bölümde öğreneceğimiz sinyalleri işler. Öncelikle bileşenler ve AJAX isteklerinin işlenmesi içindir. - -Metoda, `action()`'de olduğu gibi, tip denetimi dahil, istekten gelen parametreler aktarılır. - - -`beforeRender()` ----------------- - -`beforeRender` metodu, adının da söylediği gibi, her `render()` metodundan önce çağrılır. Ortak şablon ayarları, layout'a değişken aktarımı ve benzeri işler için kullanılır. - - -`render(args...)` .{toc: render()} -------------------------------------------------- - -Burada şablonu ardından render edilmek üzere hazırlar, ona veri aktarırız vb. - -Metoda, `action()`'de olduğu gibi, tip denetimi dahil, istekten gelen parametreler aktarılır. - -```php -public function renderShow(int $id): void -{ - // model'den veriyi al ve şablona aktar - $this->template->article = $this->articles->getById($id); -} -``` - - -`afterRender()` ---------------- - -`afterRender` metodu, adının yine söylediği gibi, her `render()` metodundan sonra çağrılır. Oldukça ender kullanılır. - - -`shutdown()` ------------- - -Presenter'ın yaşam döngüsünün sonunda çağrılır. - - -Olaylar -------- - -Presenter'ın yaşam döngüsünün parçası olarak çağrılan `startup()`, `beforeRender()` ve `shutdown()` metotlarının yanı sıra, otomatik çağrılacak başka fonksiyonlar da tanımlanabilir. Presenter, [olaylar |nette:glossary#Olaylar] denilen şeyleri tanımlar; işleyicilerini `$onStartup`, `$onRender` ve `$onShutdown` dizilerine eklersiniz. - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct() - { - $this->onStartup[] = function () { - // ... - }; - } -} -``` - -`$onStartup` dizisindeki işleyiciler `startup()` metodundan hemen önce, `$onRender` işleyicileri `beforeRender()` ile `render()` arasında ve son olarak `$onShutdown` işleyicileri `shutdown()`'dan hemen önce çağrılır. - - -**Devam etmeden önce bir öneri:** Gördüğünüz gibi bir presenter birden fazla eylemi/görünümü işleyebilir, yani birden fazla `render()` metoduna sahip olabilir. Ancak presenter'ları tek veya olabildiğince az eylemle tasarlamanızı öneririz. - - -Yanıt gönderme -============== - -Presenter'ın yanıtı genellikle [bir şablonun HTML sayfasına render edilmesidir|templates], ama bir dosya, JSON gönderimi, hatta başka bir sayfaya yönlendirme de olabilir. - -Yaşam döngüsünün herhangi bir noktasında, aşağıdaki metotlardan birini kullanarak bir yanıt gönderip presenter'ı aynı anda sonlandırabiliriz: - -- `redirect()`, `redirectPermanent()`, `redirectUrl()` ve `forward()` bir [yönlendirme |#Yönlendirme] gerçekleştirir -- `error()` presenter'ı [bir hata nedeniyle |#Hata 404 vb.] sonlandırır -- `sendJson($data)` presenter'ı sonlandırır ve [veriyi |#JSON gönderme] JSON formatında gönderir -- `sendTemplate()` presenter'ı sonlandırır ve [şablonu |templates] hemen render eder -- `sendResponse($response)` presenter'ı sonlandırır ve [kendi yanıtınızı |#Yanıtlar] gönderir -- `terminate()` presenter'ı yanıtsız sonlandırır - -Bu metotların her biri, sessiz sonlandırma istisnası `Nette\Application\AbortException`'ı fırlatarak presenter'ı hemen sonlandırır. - -Bu metotlardan hiçbirini çağırmazsanız, presenter otomatik olarak şablonu render etmeye geçer. Neden? Çünkü vakaların %99'unda bir şablon render etmek isteriz, bu yüzden presenter işimizi kolaylaştırmak için bu davranışı varsayılan olarak benimser. - - -Bağlantı oluşturma -================== - -Presenter'ın, başka presenter'lara URL bağlantıları oluşturmaya yarayan bir `link()` metodu vardır. İlk parametre hedef presenter ve eylemdir, ardından dizi olarak da aktarılabilen argümanlar gelir: - -```php -$url = $this->link('Product:show', $id); - -$url = $this->link('Product:show', [$id, 'lang' => 'en']); -``` - -Şablonda başka presenter'lara ve eylemlere bağlantılar şöyle oluşturulur: - -```latte -ürün detayı -``` - -Gerçek URL yerine tanıdık `Presenter:eylem` çiftini yazın ve gerekli parametreleri ekleyin. İşin püf noktası, Latte'ye bu niteliği işlemesini ve gerçek URL'yi üretmesini söyleyen `n:href`'tir. Nette'te URL'leri hiç düşünmeniz gerekmez, yalnızca presenter'ları ve eylemleri. - -Daha fazla bilgiyi [URL bağlantıları oluşturma|creating-links] bölümünde bulabilirsiniz. - - -Yönlendirme -=========== - -Başka bir presenter'a geçmek için `redirect()` ve `forward()` metotları kullanılır. Sözdizimleri [link() |#Bağlantı oluşturma] metodununkine çok benzer. - -`forward()` metodu, HTTP yönlendirmesi olmadan hemen yeni presenter'a geçer: - -```php -$this->forward('Product:show'); -``` - -HTTP kodu 302 ile geçici yönlendirme örneği (geçerli istek yöntemi POST ise 303): - -```php -$this->redirect('Product:show', $id); -``` - -HTTP kodu 301 ile kalıcı yönlendirme için şunu kullanın: - -```php -$this->redirectPermanent('Product:show', $id); -``` - -`redirectUrl()` metoduyla uygulamanın dışındaki başka bir URL'ye yönlendirebilirsiniz. HTTP kodu ikinci parametre olarak belirtilebilir; varsayılan 302'dir (geçerli istek yöntemi POST ise 303). - -```php -$this->redirectUrl('https://nette.org'); -``` - -Yönlendirme, sessiz sonlandırma istisnası denilen `Nette\Application\AbortException`'ı fırlatarak presenter'ın çalışmasını hemen sonlandırır. - -Yönlendirmeden önce [#Flash mesajları], yani yönlendirmeden sonra şablonda gösterilecek mesajlar gönderilebilir. - - -Flash mesajları -=============== - -Bunlar genellikle bir işlemin sonucunu bildiren mesajlardır. Flash mesajlarının önemli bir özelliği, yönlendirmeden sonra da şablonda erişilebilir kalmalarıdır. Gösterildikten sonra 30 saniye daha etkin kalırlar; örneğin kullanıcı bir aktarım hatası yüzünden sayfayı yenilerse mesaj hemen kaybolmaz. - -[flashMessage() |api:Nette\Application\UI\Control::flashMessage()] metodunu çağırmanız yeterli, onu şablona aktarmayı presenter üstlenir. İlk parametre mesajın metni, isteğe bağlı ikinci parametre ise tipidir (örneğin error, warning, info). `flashMessage()` metodu flash mesajın örneğini döndürür, böylece ek bilgiler eklenebilir. - -```php -$this->flashMessage('Öğe silindi.'); -$this->redirect(/* ... */); // ve yönlendir -``` - -Şablonda bu mesajlar `$flashes` değişkeninde, `message` (mesaj metni), `type` (mesaj tipi) ve muhtemelen daha önce sözü geçen kullanıcı bilgilerini içeren `stdClass` nesneleri olarak sunulur. Onları şöyle render ederiz: - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -Hata 404 vb. -============ - -İstek yerine getirilemiyorsa, örneğin göstermek istediğimiz makale veritabanında yoksa, `error(string $message = '', int $httpCode = 404)` metoduyla 404 hatası fırlatırız. - -```php -public function renderShow(int $id): void -{ - $article = $this->articles->getById($id); - if (!$article) { - $this->error(); - } - // ... -} -``` - -HTTP hata kodu ikinci parametre olarak aktarılabilir; varsayılan 404'tür. Metot, bir `Nette\Application\BadRequestException` fırlatarak çalışır; ardından `Application` denetimi error presenter'a devreder. Bu, oluşan hatayı bildiren bir sayfa göstermekle görevli bir presenter'dır. Error presenter, [uygulama yapılandırmasında|configuration] ayarlanır. - - -JSON gönderme -============= - -`sendJson($data)` metodu, verilen veriyi JSON'a kodlar, HTTP yanıtı olarak gönderir ve presenter'ı sonlandırır. Örnek: - -```php -public function actionData(): void -{ - $data = ['hello' => 'nette']; - $this->sendJson($data); -} -``` - - -İsteğin parametreleri .{data-version:3.1.14} -============================================ - -Presenter, ve her bileşen de, parametrelerini HTTP isteğinden alır. Değerlerini `getParameter($name)` veya `getParameters()` metotlarıyla elde edebilirsiniz. Değerler dizelerdir veya dize dizileridir, yani doğrudan URL'den alınan ham veridir. - -Daha fazla rahatlık için parametrelere özellikler üzerinden erişmenizi öneririz. Onları `#[Parameter]` niteliğiyle işaretlemeniz yeterlidir: - -```php -use Nette\Application\Attributes\Parameter; // bu satır önemli - -class HomePresenter extends Nette\Application\UI\Presenter -{ - #[Parameter] - public string $theme; // public olmalı -} -``` - -Özellik için veri tipini (örneğin `string`) belirtmenizi öneririz; Nette değeri buna göre otomatik dönüştürür. Parametre değerleri ayrıca [doğrulanabilir |#Parametrelerin doğrulanması]. - -Bağlantı oluştururken parametrenin değerini doğrudan ayarlayabilirsiniz: - -```latte -tıkla -``` - - -Kalıcı parametreler -=================== - -Kalıcı parametreler, durumu farklı istekler boyunca korumak için kullanılır. Değerleri, bir bağlantıya tıklandıktan sonra da aynı kalır. Oturum verilerinin aksine URL'de aktarılırlar. Ve bu tamamen otomatik gerçekleşir, bu yüzden onları `link()` veya `n:href` içinde açıkça belirtmeye gerek yoktur. - -Örnek bir kullanım? Çok dilli bir uygulamanız olduğunu düşünün. Geçerli dil, URL'nin her zaman parçası olması gereken bir parametredir. Ama onu her bağlantıya yazmak inanılmaz sıkıcı olurdu. Bu yüzden onu `lang` kalıcı parametresi yaparsınız ve otomatik olarak taşınır. Şahane! - -Nette'te kalıcı parametre oluşturmak son derece basittir. Public bir özellik oluşturup onu nitelikle işaretlemeniz yeterlidir: (önceden `/** @persistent */` kullanılıyordu) - -```php -use Nette\Application\Attributes\Persistent; // bu satır önemli - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; // public olmalı -} -``` - -`$this->lang` örneğin `'en'` değerini taşıyorsa, `link()` veya `n:href` ile oluşturulan bağlantılar da `lang=en` parametresini içerecektir. Ve bağlantıya tıklandıktan sonra `$this->lang` yine `'en'` olacaktır. - -Özellik için veri tipini (örneğin `string`) belirtmenizi öneririz, ayrıca bir varsayılan değer de verebilirsiniz. Parametre değerleri [doğrulanabilir |#Parametrelerin doğrulanması]. - -Kalıcı parametreler genellikle ilgili presenter'ın tüm eylemleri arasında aktarılır. Onları birden fazla presenter arasında da aktarmak için, ya: - -- presenter'ların kalıtım aldığı ortak bir atada -- ya da presenter'ların kullandığı bir trait'te tanımlanmaları gerekir: - -```php -trait LanguageAware -{ - #[Persistent] - public string $lang; -} - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - use LanguageAware; -} -``` - -Bağlantı oluştururken kalıcı parametrenin değeri değiştirilebilir: - -```latte -Çekçe detay -``` - -Ya da *sıfırlanabilir*, yani URL'den kaldırılabilir. O zaman varsayılan değerini alır: - -```latte -tıkla -``` - - -Ortak parametre alanı -===================== - -İsteğin parametreleri, [kalıcı parametreler |#Kalıcı parametreler] ve `action`, `render` ile `handle` (sinyal) metotlarının parametreleri tek bir alanı paylaşır; her biri adıyla tanımlanır. Aynı ad birden fazlasında geçerse, hepsi bir ve aynı değere karşılık gelir. - -Bu sıkça avantaja çevrilir. Örneğin `lang` kalıcı parametresi ile bir eylem ya da sinyal metodunun `$lang` argümanı bir ve aynıdır; kalıcı bir parametrenin geçerli değerini, onu yalnızca metot imzasına yazarak okuyabilirsiniz: - -```php -#[Persistent] -public string $lang; - -public function handleSearch(string $query, string $lang): void -{ - // $lang, lang kalıcı parametresinin geçerli değerini taşır -} -``` - -Bu alan paylaşıldığı için, bilinçli olarak aynı değeri paylaşmalarını istemiyorsanız parametre adlarını benzersiz tutun. Bu, ayrıca isteğin POST gövdesinden de parametre okuyan sinyaller için de geçerlidir; bkz. [Sinyaller derinlemesine |components#Sinyaller derinlemesine]. - - -Etkileşimli bileşenler -====================== - -Presenter'ların yerleşik bir bileşen sistemi vardır. Bileşenler, presenter'lara gömdüğümüz ayrı ve yeniden kullanılabilir birimlerdir. [Formlar |forms:in-presenter], datagrid'ler, menüler, kısacası tekrar tekrar kullanmanın anlamlı olduğu her şey olabilirler. - -Bileşenler presenter'lara nasıl gömülür ve sonra nasıl kullanılır? Bunu [Bileşenler |components] bölümünde öğreneceksiniz. Hatta Hollywood ile neyi ortak yaptıklarını da öğreneceksiniz. - -Peki bileşenleri nereden bulurum? [Componette |https://componette.org/search/component] üzerinde, framework topluluğundan gönüllülerin katkıda bulunduğu açık kaynak bileşenleri ve Nette için birçok başka eklentiyi bulacaksınız. - - -Daha derine -=========== - -.[tip] -Bu bölümde şu ana kadar ele aldıklarımız çoğu kullanım için yeterli olacaktır. Aşağıdaki kısımlar, presenter'lara daha derinlemesine dalmak ve kesinlikle her şeyi bilmek isteyenler içindir. - - -Parametrelerin doğrulanması ---------------------------- - -URL'lerden alınan [#İsteğin parametreleri] ve [#Kalıcı parametreler] değerleri özelliklere `loadState()` metoduyla yazılır. Bu metot ayrıca özellikte belirtilen veri tipinin uyup uymadığını denetler; uymuyorsa 404 hatasıyla yanıt verir ve sayfa gösterilmez. - -URL'den gelen parametrelere asla körü körüne güvenmeyin, çünkü kullanıcı onları kolayca değiştirebilir. Örneğin `$this->lang` dilinin desteklenenler arasında olup olmadığını böyle doğrularız. Bunun uygun bir yolu, sözü geçen `loadState()` metodunu ezmektir: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; - - public function loadState(array $params): void - { - parent::loadState($params); // $this->lang burada ayarlanır - // ardından kendi değer denetimimiz gelir: - if (!in_array($this->lang, ['en', 'cs'])) { - $this->error(); - } - } -} -``` - - -İsteğin kaydedilmesi ve geri yüklenmesi ---------------------------------------- - -Presenter'ın işlediği istek, presenter'ın `getRequest()` metodunun döndürdüğü bir [api:Nette\Application\Request] nesnesidir. - -Geçerli istek oturuma kaydedilebilir, ya da tersine oturumdan geri yüklenip presenter'a yeniden yürüttürülebilir. Bu, örneğin kullanıcı bir formu doldururken oturumunun süresi dolduğunda işe yarar. Veri kaybını önlemek için, giriş sayfasına yönlendirmeden önce geçerli isteği `$reqId = $this->storeRequest()` ile oturuma kaydederiz. Bu, isteğin tanımlayıcısını kısa bir dize olarak döndürür; onu giriş presenter'ına parametre olarak aktarırız. - -Girişten sonra `$this->restoreRequest($reqId)` metodunu çağırırız; bu metot isteği oturumdan alır. POST istekleri ona iletilir, diğerleri (GET) ise isteğin URL'sine yönlendirilir. Metot, isteğin şu anda oturum açmış olan kullanıcının kendisi tarafından oluşturulduğunu doğrular. Başka bir kullanıcı giriş yaparsa veya anahtar geçersizse hiçbir şey yapmaz ve program her zamanki gibi devam eder. - -[Önceki sayfaya nasıl dönülür |best-practices:restore-request] kılavuzuna bakın. - - -Kanonikleştirme ---------------- - -Presenter'ların, daha iyi SEO'ya (arama motoru optimizasyonuna) katkıda bulunan gerçekten mükemmel bir özelliği vardır. Aynı içeriğin farklı URL'lerde bulunmasını otomatik olarak engellerler. Belirli bir hedefe birden fazla URL gidiyorsa, örneğin `/index` ve `/index?page=1`, framework bunlardan birini birincil (kanonik) sayar ve diğerlerini HTTP kodu 301 ile ona yönlendirir. Bu sayede arama motorları sayfalarınızı iki kez indekslemez ve sayfa sıralamanızı seyreltmez. - -Bu sürece kanonikleştirme denir. Kanonik URL, [router|routing] tarafından üretilen, genellikle koleksiyondaki ilk uyan rotanın URL'sidir. - -Kanonikleştirme varsayılan olarak etkindir ve `$this->autoCanonicalize = false` ile kapatılabilir. - -AJAX veya POST istekleri sırasında yönlendirme yapılmaz, çünkü bu veri kaybına yol açabilir ya da SEO açısından ek bir değer sunmaz. - -Kanonikleştirmeyi `canonicalize()` metoduyla elle de tetikleyebilirsiniz. `link()` metoduna benzer şekilde ona presenter'ı, eylemi ve parametreleri verirsiniz. Bir bağlantı üretir ve onu geçerli URL adresiyle karşılaştırır. Farklılarsa üretilen bağlantıya yönlendirir. - -```php -public function actionShow(int $id, ?string $slug = null): void -{ - $realSlug = $this->facade->getSlugForId($id); - // $slug, $realSlug'dan farklıysa yönlendirir - $this->canonicalize('Product:show', [$id, $realSlug]); -} -``` - -SEO dostu URL'ler üretmek için rota filtrelerini `canonicalize()` ile birleştiren eksiksiz bir desen için [Slug'lu güzel URL'ler |best-practices:pretty-urls] bölümüne bakın. - - -Yanıtlar --------- - -Presenter'ın döndürdüğü yanıt, [api:Nette\Application\Response] arayüzünü uygulayan bir nesnedir. Hazır birkaç yanıt vardır: - -- [api:Nette\Application\Responses\CallbackResponse] - bir callback gönderir -- [api:Nette\Application\Responses\FileResponse] - dosyayı gönderir -- [api:Nette\Application\Responses\ForwardResponse] - forward() -- [api:Nette\Application\Responses\JsonResponse] - JSON gönderir -- [api:Nette\Application\Responses\RedirectResponse] - yönlendirme -- [api:Nette\Application\Responses\TextResponse] - metin gönderir -- [api:Nette\Application\Responses\VoidResponse] - boş yanıt - -Yanıtlar `sendResponse()` metoduyla gönderilir: - -```php -use Nette\Application\Responses; - -// Düz metin -$this->sendResponse(new Responses\TextResponse('Hello Nette!')); - -// Dosya gönderir -$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf')); - -// Callback gönderir -$callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) { - if ($httpResponse->getHeader('Content-Type') === 'text/html') { - echo '

    Hello

    '; - } -}; -$this->sendResponse(new Responses\CallbackResponse($callback)); -``` - -Kendi yanıtınızı da yazabilirsiniz. HTTP isteğini ve yanıtını alan tek bir `send()` metoduna sahip `Nette\Application\Response` arayüzünü uygulamanız yeterlidir. Bu, örneğin bellekte tutmak istemediğiniz veriyi akışla gönderirken işe yarar: - -```php -class CsvResponse implements Nette\Application\Response -{ - public function __construct( - private string $fileName, - private iterable $rows, - ) { - } - - public function send(Nette\Http\IRequest $request, Nette\Http\IResponse $response): void - { - $response->setContentType('text/csv', 'utf-8'); - $response->sendAsFile($this->fileName); - - $handle = fopen('php://output', 'w'); - foreach ($this->rows as $row) { - fputcsv($handle, $row); - } - - fclose($handle); - } -} -``` - -Sonra onu presenter'da her zamanki gibi gönderirsiniz: `$this->sendResponse(new CsvResponse('export.csv', $rows));` - - -HTTP önbelleklemesi -------------------- - -`lastModified()` metodu, HTTP önbelleklemesinden kolayca yararlanmayı sağlar. Ona içeriğin son değiştirilme tarih ve saatini (marka zamanı, dize veya `DateTimeInterface` nesnesi olarak), isteğe bağlı olarak bir ETag doğrulayıcısını (içeriğin geçerli sürümünü tanımlayan kısa bir dize, örneğin hash'i) ve bir sona erme süresini verirsiniz. Tarayıcı zaten uyan bir sürüme sahipse, presenter `304 Not Modified` yanıtı gönderir ve sonlanır; böylece sayfa gereksiz yere render edilmez veya aktarılmaz: - -```php -public function renderArticle(int $id): void -{ - $article = $this->articles->getById($id); - $this->lastModified($article->updatedAt); - // ... -} -``` - - -Şablonun tamamlanması .{data-version:3.3.0} -------------------------------------------- - -Presenter bir şablonu render ederken, `sendTemplate()` metodu render'dan hemen önce `completeTemplate()`'i çağırır. Bu metot `#[TemplateVariable]` niteliğiyle işaretlenmiş değişkenleri doldurur ve şablon dosyasını bulur (varsayılan değişkenler, şablon oluşturulurken zaten `TemplateFactory` tarafından ayarlanır). Tüm görünümlerde ortak değişkenler eklemek veya farklı bir dosya belirlemek için bu protected metodu ezebilirsiniz: - -```php -protected function completeTemplate(Nette\Application\UI\Template $template): void -{ - parent::completeTemplate($template); - $template->siteName = 'Uygulamam'; -} -``` - - -`#[Requires]` ile erişimi kısıtlama .{data-version:3.2.3} ---------------------------------------------------------- - -`#[Requires]` niteliği, presenter'lara ve metotlarına erişimi kısıtlamak için gelişmiş seçenekler sunar. HTTP metotlarını belirtmek, AJAX isteği zorunlu kılmak, aynı origin'e kısıtlamak ve yalnızca forward yoluyla erişime izin vermek için kullanılabilir. Nitelik hem presenter sınıflarına hem de `action()`, `render()`, `handle()` ve `createComponent()` gibi tek tek metotlara uygulanabilir. - -Şu kısıtlamaları belirtebilirsiniz: -- HTTP metotlarına: `#[Requires(methods: ['GET', 'POST'])]` -- AJAX isteği zorunluluğu: `#[Requires(ajax: true)]` -- yalnızca aynı origin'den erişim: `#[Requires(sameOrigin: true)]` -- yalnızca forward yoluyla erişim: `#[Requires(forward: true)]` -- belirli eylemlere kısıtlama: `#[Requires(actions: 'default')]` - -.[note] -3.3 sürümünden beri aynı origin eşleşmesi, tarayıcının `Sec-Fetch-Site` header'ı kullanılarak doğrulanır (önceden bir SameSite çerezi üzerinden); bu daha güvenilirdir ve şemanın, alan adının ve portun tam eşleşmesini denetler. - -Ayrıntıları [Requires niteliği nasıl kullanılır |best-practices:attribute-requires] kılavuzunda bulabilirsiniz. - - -HTTP metodunun denetimi ------------------------ - -Nette'teki presenter'lar, öncelikle güvenlik nedeniyle, gelen her isteğin HTTP metodunu otomatik doğrular. Varsayılan olarak `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH` metotlarına izin verilir. - -Ek olarak örneğin `OPTIONS` metoduna da izin vermek isterseniz `#[Requires]` niteliğini kullanın (Nette Application v3.2.3'ten beri): - -```php -#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])] -class MyPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -3.1.13 sürümünden beri doğrulama, istekte belirtilen metodun `$presenter->allowedMethods` dizisinde bulunup bulunmadığını denetleyen `checkHttpMethod()` içinde yapılır. 3.2.3 sürümünden itibaren bu yaklaşım `#[Requires]` lehine kullanımdan kaldırılmıştır. Metodu şöyle ezebilirsiniz: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - protected function checkHttpMethod(): void - { - $this->allowedMethods[] = 'OPTIONS'; - parent::checkHttpMethod(); - } -} -``` - -Şunu vurgulamak önemli: `OPTIONS` metodunu etkinleştirirseniz, ardından onu presenter'ınızda uygun şekilde işlemeniz gerekir. Bu metot sıklıkla preflight isteği olarak kullanılır; tarayıcı, isteğin CORS (Cross-Origin Resource Sharing) politikasına göre izin verilebilir olup olmadığını belirlemek gerektiğinde asıl istekten önce onu otomatik gönderir. Metodu etkinleştirip doğru yanıtı uygulamazsanız, bu tutarsızlıklara ve olası güvenlik sorunlarına yol açabilir. - - -Kullanımdan kaldırılan eylemleri işaretleme .{data-version:3.2.3} ------------------------------------------------------------------ - -`#[Deprecated]` niteliği; eylemleri, sinyalleri veya tüm presenter'ları kullanımdan kaldırılmış ve ileride silinmek üzere planlanmış diye işaretler. Uygulamanın kullanımdan kaldırılmış bölümlerine bağlantı üretilirken Nette, geliştiricileri uyarmak için bir uyarı fırlatır. - -Niteliği ya tüm presenter sınıfına ya da tek tek `action()`, `render()` ve `handle()` metotlarına uygulayabilirsiniz. - - -İleri okuma -=========== - -- [Inject metotları ve nitelikleri |best-practices:inject-method-attribute] -- [Presenter'ları trait'lerden oluşturma |best-practices:presenter-traits] -- [Presenter'lara ayar aktarma |best-practices:passing-settings-to-presenters] -- [Önceki sayfaya nasıl dönülür |best-practices:restore-request] diff --git a/application/tr/routing.texy b/application/tr/routing.texy deleted file mode 100644 index bf1556e382..0000000000 --- a/application/tr/routing.texy +++ /dev/null @@ -1,749 +0,0 @@ -Yönlendirme -*********** - -
    - -Router, URL adresleriyle ilgili her şeyi üstlenir, böylece onları düşünmek zorunda kalmazsınız. Size şunları göstereceğiz: - -- URL'lerin istediğiniz gibi görünmesi için router nasıl yapılandırılır -- SEO ve yönlendirme üzerine konuşacağız -- ve kendi router'ınızı nasıl yazacağınızı göstereceğiz - -
    - - -İnsana daha dostça URL'ler (havalı veya güzel URL de denir) daha kullanışlıdır, akılda daha iyi kalır ve SEO'ya olumlu katkı yapar. Nette bunu göz önünde bulundurur ve geliştiricilerin ihtiyaçlarını tam olarak karşılar. Uygulamanız için istediğiniz URL yapısını tam olarak tasarlayabilirsiniz. Üstelik uygulama zaten tamamlandığında bile tasarlayabilirsiniz, çünkü kodda veya şablonlarda hiçbir değişiklik gerektirmez. Tüm presenter'lara anotasyon olarak dağılmak yerine [tek bir yerde |#Entegrasyon], router'da zarif biçimde tanımlanır. - -Nette'teki router olağanüstüdür, çünkü **çift yönlüdür**. Hem HTTP isteklerinden URL'leri çözebilir hem de bağlantı oluşturabilir. Böylece [Nette Application |how-it-works#Nette Application] içinde çok önemli bir rol oynar: yalnızca geçerli isteği hangi presenter'ın ve eylemin yürüteceğine karar vermekle kalmaz, şablonlarda vb. [URL üretmek |creating-links] için de kullanılır. - -Ancak router'ın kullanımı bununla sınırlı değildir; onu presenter'ların hiç kullanılmadığı uygulamalarda, REST API'lerde vb. de kullanabilirsiniz. Daha fazla ayrıntı [#Bağımsız kullanım] bölümünde. - - -Rota koleksiyonu -================ - -Bir uygulamadaki URL adreslerinin yapısını tanımlamanın en hoş yolunu [api:Nette\Application\Routers\RouteList] sınıfı sunar. Tanım, rota denilen şeylerin listesinden oluşur; yani basit bir API ile URL adreslerinin maskeleri ve onlara bağlı presenter'lar ile eylemler. Rotaları hiçbir şekilde adlandırmamız gerekmez. - -```php -$router = new Nette\Application\Routers\RouteList; -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('article/', 'Article:view'); -// ... -``` - -Örnek şunu gösterir: tarayıcıda `https://domain.com/rss.xml` açarsak, `rss` eylemiyle `Feed` presenter'ı gösterilir. `https://domain.com/article/12` ise `view` eylemiyle `Article` presenter'ını gösterir vb. Uygun bir rota bulunamazsa Nette Application, kullanıcıya 404 Not Found hata sayfası olarak gösterilen bir [BadRequestException |api:Nette\Application\BadRequestException] fırlatarak yanıt verir. - - -Rotaların sırası ----------------- - -Rotaların listelenme **sırası** kesinlikle **çok önemlidir**, çünkü yukarıdan aşağıya sırayla değerlendirilirler. Kural şudur: rotaları **özelden genele** doğru bildiririz: - -```php -// YANLIŞ: 'rss.xml' ilk rota tarafından yakalanır ve bu dize sanılır -$router->addRoute('', 'Article:view'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// DOĞRU -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('', 'Article:view'); -``` - -Bağlantı üretilirken de rotalar yukarıdan aşağıya değerlendirilir: - -```php -// YANLIŞ: 'Feed:rss' bağlantısı 'admin/feed/rss' olarak üretilir -$router->addRoute('admin//', 'Admin:default'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// DOĞRU -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('admin//', 'Admin:default'); -``` - -Rotaları doğru kurmanın biraz beceri gerektirdiğini sizden saklamayacağız. Bunda ustalaşana kadar [yönlendirme paneli |#Router'ın hata ayıklaması] işinize yarayacak bir araç olacak. - - -Maske ve parametreler ---------------------- - -Maske, sitenin kök dizininden itibaren göreli yolu tanımlar. En basit maske statik bir URL'dir: - -```php -$router->addRoute('products', 'Products:default'); -``` - -Maskeler sıklıkla **parametre** denilen şeyleri içerir. Bunlar açılı parantez içine alınır (örneğin ``) ve hedef presenter'a, örneğin `renderShow(int $year)` metoduna veya `$year` kalıcı parametresine aktarılır: - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -Örnek şunu gösterir: tarayıcıda `https://example.com/chronicle/2020` açarsak, `show` eylemiyle ve `year: 2020` parametresiyle `History` presenter'ı gösterilir. - -Parametreler için doğrudan maskede varsayılan bir değer belirtebiliriz; bu onları isteğe bağlı yapar: - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -Rota artık `https://example.com/chronicle/` URL'sini de kabul edecek ve yine `year: 2020` parametresiyle `History:show`'u gösterecektir. - -Elbette presenter ve eylem adları da parametre olabilir. Örneğin: - -```php -$router->addRoute('/', 'Home:default'); -``` - -Belirtilen rota örneğin `/article/edit` veya `/catalog/list` biçimindeki URL'leri kabul eder ve onları sırasıyla `Article:edit` ile `Catalog:list` presenter'ları ve eylemleri olarak anlar. - -Aynı zamanda `presenter` ve `action` parametrelerine `Home` ve `default` varsayılan değerlerini verir, bu da onları da isteğe bağlı yapar. Böylece rota `/article` gibi bir URL'yi de kabul eder ve onu `Article:default` olarak anlar. Ya da tersine, `Product:default`'a giden bir bağlantı `/product` yolunu, varsayılan `Home:default`'a giden bir bağlantı ise `/` yolunu üretir. - -Maske yalnızca sitenin kök dizininden itibaren göreli yolu değil, eğik çizgiyle başlıyorsa mutlak yolu, hatta iki eğik çizgiyle başlıyorsa tüm mutlak URL'yi de tanımlayabilir: - -```php -// document root'a göreli -$router->addRoute('/', /* ... */); - -// mutlak yol (alan adına göreli) -$router->addRoute('//', /* ... */); - -// alan adı dahil mutlak URL (şemaya göreli) -$router->addRoute('//.example.com//', /* ... */); - -// şema dahil mutlak URL -$router->addRoute('https://.example.com//', /* ... */); -``` - - -Doğrulama ifadeleri -------------------- - -Her parametre için bir [düzenli ifadeyle|https://www.php.net/manual/en/reference.pcre.pattern.syntax.php] doğrulama koşulu belirtilebilir. Örneğin `id` parametresi için, `\d+` regex'iyle yalnızca rakam içerebileceğini belirtiriz: - -```php -$router->addRoute('/[/]', /* ... */); -``` - -Tüm parametreler için varsayılan düzenli ifade `[^/]+`'dir, yani eğik çizgi dışındaki her şey. Bir parametrenin eğik çizgi de kabul etmesi gerekiyorsa ifadeyi `.+` yaparız: - -```php -// https://example.com/a/b/c kabul eder, path 'a/b/c' olur -$router->addRoute('', /* ... */); -``` - - -İsteğe bağlı diziler --------------------- - -Maskede isteğe bağlı bölümler köşeli parantezle işaretlenebilir. Maskenin herhangi bir bölümü isteğe bağlı olabilir ve parametre içerebilir: - -```php -$router->addRoute('[/]', /* ... */); - -// Şu yolları kabul eder: -// /en/download => lang => en, name => download -// /download => lang => null, name => download -``` - -Bir parametre isteğe bağlı bir dizinin parçası olduğunda, doğal olarak kendisi de isteğe bağlı olur. Belirtilmiş bir varsayılan değeri yoksa null olacaktır. - -İsteğe bağlı bölümler alan adında da olabilir: - -```php -$router->addRoute('//[.]example.com//', /* ... */); -``` - -Diziler iç içe geçirilebilir ve istenildiği gibi birleştirilebilir: - -```php -$router->addRoute( - '[[-]/][/page-]', - 'Home:default', -); - -// Şu yolları kabul eder: -// /en/hello -// /en-us/hello -// /hello -// /hello/page-12 -``` - -URL üretilirken en kısa seçenek yeğlenir, bu yüzden atlanabilecek her şey atlanır. Bu nedenle örneğin `index[.html]` rotası `/index` yolunu üretir. Bu davranış, sol köşeli parantezin ardına ünlem işareti konarak tersine çevrilebilir: - -```php -// /hello ve /hello.html kabul eder, /hello üretir -$router->addRoute('[.html]', /* ... */); - -// /hello ve /hello.html kabul eder, /hello.html üretir -$router->addRoute('[!.html]', /* ... */); -``` - -Köşeli parantezsiz isteğe bağlı parametreler (yani varsayılan değeri olanlar) aslında şöyle sarılmış gibi davranır: - -```php -$router->addRoute('//', /* ... */); - -// buna karşılık gelir: -$router->addRoute('[/[/[]]]', /* ... */); -``` - -Sondaki eğik çizginin davranışını etkilemek, örneğin `/home/` yerine `/home` üretilmesini istemek istersek, bu şöyle sağlanabilir: - -```php -$router->addRoute('[[/[/]]]', /* ... */); -``` - - -Joker karakterler ------------------ - -Mutlak URL maskesinde, örneğin geliştirme ve üretim ortamları arasında farklı olabilecek alan adını maskeye yazmak zorunda kalmamak için şu joker karakterleri kullanabiliriz: - -- `%tld%` = üst düzey alan adı, örneğin `com` veya `org` -- `%sld%` = ikinci düzey alan adı, örneğin `example` -- `%domain%` = alt alan adları olmadan alan adı, örneğin `example.com` -- `%host%` = tüm host, örneğin `www.example.com` -- `%basePath%` = kök dizine giden yol - -```php -$router->addRoute('//www.%domain%/%basePath%//', /* ... */); -$router->addRoute('//www.%sld%.%tld%/%basePath%//', /* ... */); -``` - - -Gelişmiş yazım --------------- - -Genellikle `Presenter:eylem` biçiminde yazılan rota hedefi, tek tek parametreleri ve varsayılan değerlerini tanımlayan bir dizi kullanılarak da yazılabilir: - -```php -$router->addRoute('/[/]', [ - 'presenter' => 'Home', - 'action' => 'default', -]); -``` - -Daha ayrıntılı belirtim için, varsayılan değerlerin yanı sıra başka parametre özelliklerini de (örneğin bir doğrulama düzenli ifadesini, bkz. `id` parametresi) ayarlayabildiğimiz daha da genişletilmiş bir biçim kullanılabilir: - -```php -use Nette\Routing\Route; - -$router->addRoute('/[/]', [ - 'presenter' => [ - Route::Value => 'Home', - ], - 'action' => [ - Route::Value => 'default', - ], - 'id' => [ - Route::Pattern => '\d+', - ], -]); -``` - -Şunu belirtmek önemlidir: dizide tanımlanan parametreler yol maskesinde listelenmiyorsa, değerleri değiştirilemez; URL'de soru işaretinden sonra belirtilen sorgu parametreleriyle bile. - -Bu, **sabit parametreler** için yararlıdır; belirli bir sayfaya kısa, akılda kalan bir URL vermek. Örneğin `/tos`'un her zaman `id: 123` ile `Article:view`'u açması için: - -```php -$router->addRoute('tos', [ - 'presenter' => 'Article', - 'action' => 'view', - 'id' => 123, -]); -``` - - -Filtreler ve çeviriler ----------------------- - -Uygulamanın kaynak kodunu İngilizce yazarız, ama sitenin Çekçe URL'leri olması gerekiyorsa, şöyle basit bir yönlendirme: - -```php -$router->addRoute('/', 'Home:default'); -``` - -İngilizce URL'ler üretecektir, örneğin `/product/123` veya `/cart`. URL'deki presenter'ların ve eylemlerin Çekçe sözcüklerle temsil edilmesini istiyorsak (örneğin `/produkt/123` veya `/kosik`), bir çeviri sözlüğü kullanabiliriz. Onu yazmak için ikinci parametrenin "daha ayrıntılı" seçeneğine ihtiyacımız var: - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterTable => [ - // URL'deki dize => presenter - 'produkt' => 'Product', - 'kosik' => 'Cart', - 'katalog' => 'Catalog', - ], - ], - 'action' => [ - Route::Value => 'default', - Route::FilterTable => [ - 'seznam' => 'list', - ], - ], -]); -``` - -Çeviri sözlüğündeki birden fazla anahtar aynı presenter'a gidebilir. Bu, onun için çeşitli takma adlar oluşturur. Son anahtar kanonik seçenek sayılır (yani üretilen URL'de yer alacak olan). - -Çeviri tablosu bu şekilde herhangi bir parametre için kullanılabilir. Bir çeviri yoksa özgün değer alınır. Bu davranışı `Route::FilterStrict => true` ekleyerek değiştirebiliriz; o zaman rota, değer sözlükte yoksa URL'yi reddeder. - -Dizi biçimindeki çeviri sözlüğünün yanı sıra kendi çeviri fonksiyonlarınız da devreye sokulabilir. - -```php -use Nette\Routing\Route; - -$router->addRoute('//', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterIn => function (string $s): string { /* ... */ }, - Route::FilterOut => function (string $s): string { /* ... */ }, - ], - 'action' => 'default', - 'id' => null, -]); -``` - -`Route::FilterIn` fonksiyonu, URL'deki parametre ile sonra presenter'a aktarılan dize arasında dönüşüm yapar; `FilterOut` fonksiyonu ise ters yöndeki dönüşümü sağlar. - -`presenter`, `action` ve `module` parametrelerinin, PascalCase veya camelCase stili ile URL'lerde kullanılan kebab-case arasında dönüşüm yapan önceden tanımlı filtreleri vardır. Parametrelerin varsayılan değeri, uygulamaya aktarıldığı biçimde yazılır (presenter ve module için PascalCase, action için camelCase); yani örneğin bir presenter söz konusuysa `` yazarız, `` değil. - - -Genel filtreler ---------------- - -Belirli parametreler için tasarlanmış filtrelerin yanı sıra, tüm parametrelerin ilişkisel dizisini alan, onu istediği gibi değiştirip döndürebilen genel filtreler de tanımlayabiliriz. Genel filtreler boş anahtar altında tanımlanır. - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => 'Home', - 'action' => 'default', - '' => [ - Route::FilterIn => function (array $params): array { /* ... */ }, - Route::FilterOut => function (array $params): array { /* ... */ }, - ], -]); -``` - -Genel filtreler, rotanın davranışını kesinlikle her şekilde değiştirme olanağı sunar. Onları örneğin parametreleri başka parametrelere göre değiştirmek için kullanabiliriz. Örneğin `` ve ``'ı `` parametresinin geçerli değerine göre çevirmek için. - -Bir parametrenin kendi filtresi tanımlıysa ve bir de genel filtre varsa, kendi `FilterIn`'i genelden önce, buna karşılık genel `FilterOut` kendi filtreden önce çalıştırılır. Böylece genel filtrenin içinde `presenter` ve `action` parametrelerinin değerleri sırasıyla PascalCase ve camelCase stilinde yazılıdır. - -Bu filtrelerin pratik bir kullanımı için, hiçbir şablonu değiştirmeden `/article/123-ekmek-nasil-pisirilir` gibi SEO dostu URL'ler üretmeyi anlatan [Slug'lu güzel URL'ler |best-practices:pretty-urls] bölümüne bakın. - - -OneWay bayrağı --------------- - -Tek yönlü rotalar, uygulamanın artık üretmediği ama hâlâ kabul ettiği eski URL'lerin işlevselliğini korumaya yarar. Onları `OneWay` bayrağıyla işaretleriz: - -```php -// eski URL /product-info?id=123 -$router->addRoute('product-info', 'Product:detail', oneWay: true); -// yeni URL /product/123 -$router->addRoute('product/', 'Product:detail'); -``` - -Eski URL'ye erişildiğinde presenter otomatik olarak yeni URL'ye yönlendirir, böylece arama motorları bu sayfaları iki kez indekslemez (bkz. [#SEO ve kanonikleştirme]). - - -Callback'lerle dinamik yönlendirme ----------------------------------- - -Callback'lerle dinamik yönlendirme, rotalara doğrudan, ilgili yol ziyaret edildiğinde çalıştırılan fonksiyonlar (callback'ler) atamanızı sağlar. Bu esnek işlevsellik, uygulamanız için çeşitli uç noktaları hızlı ve verimli biçimde oluşturmanıza olanak tanır: - -```php -$router->addRoute('test', function () { - echo '/test adresindesiniz'; -}); -``` - -Maskede, callback'inize otomatik aktarılan parametreler de tanımlayabilirsiniz: - -```php -$router->addRoute('', function (string $lang) { - echo match ($lang) { - 'cs' => 'Sitemizin Çekçe sürümüne hoş geldiniz!', - 'en' => 'Sitemizin İngilizce sürümüne hoş geldiniz!', - }; -}); -``` - -Maskeden gelen parametrelerin yanı sıra callback, DI konteynerinden servisler de alabilir. Bunlar parametre tipine göre aktarılır. Ayrıca `$presenter` parametresi, rotayı işleyen [MicroPresenter |api:NetteModule\MicroPresenter] örneğini alır: - -```php -$router->addRoute('', function (string $lang, Nette\Http\Request $httpRequest, NetteModule\MicroPresenter $presenter) { - // ... -}); -``` - - -Modüller --------- - -Ortak bir [modüle |directory-structure#Presenter'lar ve şablonlar] ait birden fazla rotamız varsa `withModule()` kullanırız. Verilen modül, gruptaki her rotanın presenter'ının önüne otomatik eklenir ve URL'den tamamen kaybolur: - -```php -$router = new RouteList; -$router->withModule('Forum') // sonraki rotalar Forum modülünün parçasıdır - ->addRoute('rss', 'Feed:rss') // presenter Forum:Feed olacak - ->addRoute('/') - - ->withModule('Admin') // sonraki rotalar Forum:Admin modülünün parçasıdır - ->addRoute('sign:in', 'Sign:in'); -``` - -Bir alternatif de `module` parametresidir; o da sabit bir modül belirler ve onu URL'nin dışında tutar: - -```php -// manage/dashboard/default URL'si Admin:Dashboard presenter'ına eşlenir -$router->addRoute('manage//', [ - 'module' => 'Admin', -]); -``` - -Her presenter adı ancak modülüyle birlikte tamdır, örneğin `Front:Admin:ProductList`. Böyle bir tam ad bir URL parametresine düştüğünde, router onu iki basit kuralla kodlar: her iki nokta üst üste `:` (modül ayırıcısı) bir **noktaya**, PascalCase bir addaki her sözcük sınırı ise bir **kısa çizgiye** dönüşür. Yani `Front:Admin:ProductList`, URL'de `front.admin.product-list` olarak görünür ve aynı şekilde geri çözülür. Modüler bir uygulamanın, yukarıdaki araçlar olmadan, noktalarla dolu URL'ler üretmesinin nedeni budur. - -Hem `withModule()` hem de `module` parametresi bundan tam olarak şu yüzden kaçınır: presenter adı URL'ye ulaşmadan önce bilinen modül ön ekini ondan soyarlar; modül sabit olduğundan hiç kodlanması gerekmez. - -Bazen modülün kendisinin değişmesini ve URL'de görünmesini isteriz, bu yüzden doğrudan maskede ``'e başvururuz. Çok önemli bir ayrıntıya dikkat edin: **`` tüm modül yolunu yakalar**, yani presenter adındaki son iki nokta üst üsteye kadar olan her şeyi. `Shop:Admin:Product` presenter'ı için bu, `Shop:Admin` modülü ve `Product` presenter'ı demektir; iki nokta üst üste noktaya dönüştüğünden şunu elde ederiz: - - -Alt alan adları ---------------- - -Rota koleksiyonları alt alan adlarına göre bölünebilir: - -```php -$router = new RouteList; -$router->withDomain('example.com') - ->addRoute('rss', 'Feed:rss') - ->addRoute('/'); -``` - -Alan adında [#Joker karakterler] de kullanılabilir: - -```php -$router = new RouteList; -$router->withDomain('example.%tld%') - // ... -``` - - -Yol ön eki ----------- - -Rota koleksiyonları URL'deki yola göre bölünebilir: - -```php -$router = new RouteList; -$router->withPath('eshop') - ->addRoute('rss', 'Feed:rss') // /eshop/rss URL'siyle eşleşir - ->addRoute('/'); // /eshop// URL'siyle eşleşir -``` - - -Bileşimler ----------- - -Yukarıdaki gruplamalar birbirleriyle birleştirilebilir: - -```php -$router = (new RouteList) - ->withDomain('admin.example.com') - ->withModule('Admin') - ->addRoute(/* ... */) - ->addRoute(/* ... */) - ->end() - ->withModule('Images') - ->addRoute(/* ... */) - ->end() - ->end() - ->withDomain('example.com') - ->withPath('export') - ->addRoute(/* ... */) - // ... -``` - - -Sorgu parametreleri -------------------- - -Maskeler sorgu parametreleri de (URL'de soru işaretinden sonraki parametreler) içerebilir. Bunlar için doğrulama ifadesi tanımlanamaz, ama presenter'a aktarıldıkları ad değiştirilebilir: - -```php -// 'cat' sorgu parametresini uygulamada 'categoryId' adıyla kullanmak istiyoruz -$router->addRoute('product ? id= & cat=', /* ... */); -``` - - -Foo parametreleri ------------------ - -Şimdi daha derine iniyoruz. Foo parametreleri aslında bir düzenli ifadeyle eşleşmeyi sağlayan adsız parametrelerdir. Bir örnek, `/index`, `/index.html`, `/index.htm` ve `/index.php` kabul eden bir rotadır: - -```php -$router->addRoute('index', /* ... */); -``` - -URL üretilirken kullanılacak dizeyi açıkça tanımlamak da mümkündür. Dize doğrudan soru işaretinin ardına konmalıdır. Aşağıdaki rota öncekine benzer, ama `/index` yerine `/index.html` üretir, çünkü üretim değeri olarak `.html` dizesi ayarlanmıştır: - -```php -$router->addRoute('index', /* ... */); -``` - - -Entegrasyon -=========== - -Oluşturduğumuz router'ı uygulamaya entegre etmek için DI konteynerine ondan söz etmemiz gerekir. En kolayı, router nesnesini oluşturacak bir factory hazırlamak ve yapılandırmada konteynere onu kullanmasını söylemektir. Bunun için `App\Core\RouterFactory::createRouter()` metodunu yazdığımızı varsayalım: - -```php -namespace App\Core; - -use Nette\Application\Routers\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute(/* ... */); - return $router; - } -} -``` - -Sonra [yapılandırmaya |dependency-injection:services] şunu yazarız: - -```neon -services: - - App\Core\RouterFactory::createRouter -``` - -Örneğin bir veritabanına olan bağımlılıklar, factory metoduna [autowiring|dependency-injection:autowiring] ile parametreleri olarak aktarılır: - -```php -public static function createRouter(Nette\Database\Connection $db): RouteList -{ - // ... -} -``` - - -SimpleRouter -============ - -Rota koleksiyonundan çok daha basit bir router [SimpleRouter |api:Nette\Application\Routers\SimpleRouter]'dır. Onu, URL biçimiyle ilgili özel gereksinimlerimiz olmadığında, `mod_rewrite` (veya alternatifleri) kullanılamıyorsa ya da henüz güzel URL'lerle uğraşmak istemiyorsak kullanırız. - -Aşağı yukarı şu biçimde adresler üretir: - -``` -http://example.com/?presenter=Product&action=detail&id=123 -``` - -`SimpleRouter` constructor'ının parametresi varsayılan bir presenter ve eylemdir; yani örneğin `http://example.com/` adresini ek parametre olmadan açtığımızda çalıştırılacak eylem. - -```php -// varsayılan presenter 'Home', eylem 'default' olacak -$router = new Nette\Application\Routers\SimpleRouter('Home:default'); -``` - -SimpleRouter'ı doğrudan [yapılandırmada |dependency-injection:services] tanımlamanızı öneririz: - -```neon -services: - - Nette\Application\Routers\SimpleRouter('Home:default') -``` - - -SEO ve kanonikleştirme -====================== - -Framework, aynı içeriğin farklı URL'lerde bulunmasını engelleyerek SEO'ya (arama motoru optimizasyonuna) katkı yapar. Belirli bir hedefe birden fazla adres gidiyorsa, örneğin `/index` ve `/index.html`, framework ilkini birincil (kanonik) sayar ve diğerlerini HTTP kodu 301 ile ona yönlendirir. Bu sayede arama motorları sayfaları iki kez indekslemez ve sayfa sıralamalarını seyreltmez. - -Bu sürece kanonikleştirme denir. Kanonik URL, router tarafından üretilen, yani koleksiyondaki OneWay bayrağı olmayan ilk uyan rotanın URL'sidir. Bu yüzden koleksiyonda **önce birincil rotaları** listeleriz. - -Kanonikleştirmeyi presenter gerçekleştirir; daha fazlası [kanonikleştirme |presenters#Kanonikleştirme] bölümünde. - - -HTTPS -===== - -HTTPS protokolünü kullanmak için onu hosting'de etkinleştirmek ve sunucuyu doğru yapılandırmak gerekir. - -Tüm sitenin HTTPS'e yönlendirilmesi sunucu düzeyinde, örneğin uygulamamızın kök dizinindeki `.htaccess` dosyasıyla ve HTTP kodu 301 ile ayarlanmalıdır. Ayarlar hosting'e göre değişebilir ve aşağı yukarı şöyle görünür: - -``` - - RewriteEngine On - ... - RewriteCond %{HTTPS} off - RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] - ... - -``` - -Router, URL'leri sayfanın yüklendiği protokolün aynısıyla üretir, bu yüzden başka bir şey ayarlamaya gerek yoktur. - -Ancak istisnai olarak farklı rotaların farklı protokoller altında çalışmasına ihtiyaç duyarsak, bunu rota maskesinde belirtiriz: - -```php -// HTTP adresi üretecek -$router->addRoute('http://%host%//', /* ... */); - -// HTTPS adresi üretecek -$router->addRoute('https://%host%//', /* ... */); -``` - - -Router'ın hata ayıklaması -========================= - -[Tracy Bar |tracy:]'da görünen yönlendirme paneli, rotaların listesini ve ayrıca router'ın URL'den elde ettiği parametreleri gösteren yararlı bir yardımcıdır. - -✓ simgeli yeşil çubuk, geçerli URL'yi işleyen rotayı temsil eder; mavi renk ve ≈ simgesi ise, yeşil olan onların önüne geçmeseydi URL'yi işleyecek olan rotaları gösterir. Ayrıca geçerli presenter'ı ve eylemi görürüz. - -[* routing-debugger.webp *] - -Aynı zamanda [kanonikleştirme |#SEO ve kanonikleştirme] nedeniyle beklenmedik bir yönlendirme olursa, panelin *redirect* çubuğuna bakmak yararlıdır; orada router'ın URL'yi başlangıçta nasıl anladığını ve neden yönlendirdiğini öğrenebilirsiniz. - -.[note] -Router'ın hata ayıklamasını yaparken, yönlendirmelerin orada saklanmaması için tarayıcıda Developer Tools'u açmanızı (Ctrl+Shift+I veya Cmd+Option+I) ve Network panelinde önbelleği kapatmanızı öneririz. - - -Performans -========== - -Rotaların sayısı router'ın hızını etkiler. Sayıları kesinlikle birkaç düzineyi aşmamalıdır. Sitenizin URL yapısı fazla karmaşıksa, kendi [#Kendi router'ınız]'ınızı yazabilirsiniz. - -Router'ın örneğin bir veritabanına bağımlılığı yoksa ve factory'si argüman almıyorsa, derlenmiş biçimini doğrudan DI konteynerine serileştirebilir ve böylece uygulamayı biraz hızlandırabiliriz. - -```neon -routing: - cache: true -``` - - -Kendi router'ınız -================= - -Aşağıdaki satırlar çok ileri düzey kullanıcılar içindir. Kendi router'ınızı oluşturabilir ve onu doğal olarak rota koleksiyonuna entegre edebilirsiniz. Router, iki metotlu [api:Nette\Routing\Router] arayüzünün bir uygulamasıdır: - -```php -use Nette\Http\IRequest as HttpRequest; -use Nette\Http\UrlScript; - -class MyRouter implements Nette\Routing\Router -{ - public function match(HttpRequest $httpRequest): ?array - { - // ... - } - - public function constructUrl(array $params, UrlScript $refUrl): ?string - { - // ... - } -} -``` - -`match` metodu, yalnızca URL'nin değil header'ların vb. de elde edilebildiği geçerli istek [$httpRequest |http:request]'i, presenter adını ve parametrelerini içeren bir diziye işler. İsteği işleyemezse null döndürür. İsteği işlerken en azından presenter'ı döndürmemiz gerekir; eylem isteğe bağlıdır ve belirtilmezse `default` olur. Presenter adı tamdır ve varsa modülleri de içerir: - -```php -[ - 'presenter' => 'Front:Home', - 'action' => 'default', -] -``` - -`constructUrl` metodu ise tam tersine, parametre dizisinden sonuçtaki mutlak URL'yi kurar. Geçerli URL olan [`$refUrl`|api:Nette\Http\UrlScript] parametresindeki bilgilerden yararlanabilir. - -Onu rota koleksiyonuna `add()` ile ekleyin: - -```php -$router = new Nette\Application\Routers\RouteList; -$router->add($myRouter); -$router->addRoute(/* ... */); -// ... -``` - - -Bağımsız kullanım -================= - -Bağımsız kullanımdan kastımız, router'ın yeteneklerinden Nette Application ve presenter'ları kullanmayan bir uygulamada yararlanmaktır. Bu bölümde gösterdiğimiz hemen her şey onun için de geçerlidir, şu farklarla: - -- rota koleksiyonları için [api:Nette\Routing\RouteList] sınıfını kullanırız -- basit router olarak [api:Nette\Routing\SimpleRouter] sınıfını -- `Presenter:eylem` çifti var olmadığından [#Gelişmiş yazım] kullanırız - -Yine router'ı bizim için kuracak bir metot oluştururuz, örneğin: - -```php -namespace App\Core; - -use Nette\Routing\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute('rss.xml', [ - 'controller' => 'RssFeedController', - ]); - $router->addRoute('article/', [ - 'controller' => 'ArticleController', - ]); - // ... - return $router; - } -} -``` - -Önerdiğimiz gibi bir DI konteyneri kullanıyorsanız, metodu yine yapılandırmaya ekleyin ve sonra router'ı HTTP isteğiyle birlikte konteynerden alın: - -```php -$router = $container->getByType(Nette\Routing\Router::class); -$httpRequest = $container->getByType(Nette\Http\IRequest::class); -``` - -Ya da nesneleri doğrudan oluşturun: - -```php -$router = App\Core\RouterFactory::createRouter(); -$httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); -``` - -Şimdi geriye yalnızca router'ın işini yapmasına izin vermek kalıyor: - -```php -$params = $router->match($httpRequest); -if ($params === null) { - // uyan rota bulunamadı, 404 hatası gönder - exit; -} - -// elde edilen parametreleri işle -$controller = $params['controller']; -// ... -``` - -Ve tersine, bir bağlantı kurmak için router'ı kullanın: - -```php -$params = ['controller' => 'ArticleController', 'id' => 123]; -$url = $router->constructUrl($params, $httpRequest->getUrl()); -``` - - -{{composer: nette/routing}} -{{repo: nette/routing}} -{{api: https://api.nette.org/routing/}} diff --git a/application/tr/templates.texy b/application/tr/templates.texy deleted file mode 100644 index 635ca43481..0000000000 --- a/application/tr/templates.texy +++ /dev/null @@ -1,419 +0,0 @@ -Şablonlar -********* - -.[perex] -Nette, [Latte |latte:] şablon sistemini kullanır. Latte kullanılır, çünkü PHP için en güvenli ve aynı zamanda en sezgisel şablon sistemidir. Yeni çok şey öğrenmeniz gerekmez; PHP bilgisi ve birkaç etiket yeterlidir. - -Bir sayfanın layout şablonu + somut eylemin şablonundan oluşması yaygındır. Bir layout şablonu şöyle görünebilir; `{block}` bloklarına ve `{include}` etiketine dikkat edin: - -```latte - - - - {block title}Uygulamam{/block} - - -
    ...
    - {include content} -
    ...
    - - -``` - -Eylem şablonu ise şöyle olurdu: - -```latte -{block title}Ana sayfa{/block} - -{block content} -

    Ana sayfa

    -... -{/block} -``` - -Layout'ta `{include content}` yerine eklenen `content` bloğunu tanımlar ve ayrıca layout'taki `{block title}`'ın üzerine yazan `title` bloğunu yeniden tanımlar. Sonucu gözünüzde canlandırmaya çalışın. - - -Şablon arama ------------- - -Presenter'larda hangi şablonun render edileceğini belirtmeniz gerekmez; framework yolu kendiliğinden çıkarır ve sizi yazmaktan kurtarır. - -Her presenter'ın kendi dizinine sahip olduğu bir dizin yapısı kullanıyorsanız, şablonu bu dizine eylemin (yani görünümün) adıyla koymanız yeterlidir. Örneğin `default` eylemi için `default.latte` şablonunu kullanın: - -/--pre -app/ -└── Presentation/ - └── Home/ - ├── HomePresenter.php - └── default.latte -\-- - -Presenter'ların tek bir dizinde bir arada olduğu ve şablonların bir `templates` klasöründe bulunduğu bir yapı kullanıyorsanız, onu ya `..latte` ya da `/.latte` dosyasına kaydedin: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── Home/ - │ └── default.latte ← 1. seçenek - └── Home.default.latte ← 2. seçenek -\-- - -`templates` dizini bir düzey yukarıya, yani presenter sınıflarının bulunduğu dizinle aynı düzeye de konabilir. - -Şablon bulunamazsa presenter [404 - sayfa bulunamadı hatasıyla |presenters#Hata 404 vb.] yanıt verir. - -Görünümü `$this->setView('digerGorunum')` ile değiştirebilirsiniz. Şablon dosyasını `$this->template->setFile('/path/to/template.latte')` ile doğrudan belirtmek de mümkündür. - -.[note] -Şablonların arandığı dosyalar, olası dosya adlarının dizisini döndüren [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()] metodu ezilerek değiştirilebilir. - - -Layout şablonu arama --------------------- - -Nette layout dosyasını da otomatik arar. - -Her presenter'ın kendi dizinine sahip olduğu bir dizin yapısı kullanıyorsanız, layout'u yalnızca ona özgüyse presenter'ın klasörüne, birden fazla presenter için ortaksa bir düzey yukarıya koyun: - -/--pre -app/ -└── Presentation/ - ├── @layout.latte ← ortak layout - └── Home/ - ├── @layout.latte ← yalnızca Home presenter'ı için - ├── HomePresenter.php - └── default.latte -\-- - -Presenter'ların tek bir dizinde toplandığı ve şablonların bir `templates` klasöründe bulunduğu bir yapı kullanıyorsanız, layout şu konumlarda beklenir: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── @layout.latte ← ortak layout - ├── Home/ - │ └── @layout.latte ← yalnızca Home için, 1. seçenek - └── Home.@layout.latte ← yalnızca Home için, 2. seçenek -\-- - -Presenter bir modülde yer alıyorsa, modül iç içeliğine göre dizin düzeylerinde daha yukarıya doğru da arama yapılır. - -Layout'un adı `$this->setLayout('layoutAdmin')` ile değiştirilebilir, o zaman `@layoutAdmin.latte` dosyasında beklenir. Layout şablonu dosyasını `$this->setLayout('/path/to/template.latte')` ile doğrudan da belirtebilirsiniz. - -`$this->setLayout(false)` veya şablonun içindeki `{layout none}` etiketi, layout aramasını kapatır. - -.[note] -Layout şablonlarının arandığı dosyalar, olası dosya adlarının dizisini döndüren [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()] metodu ezilerek değiştirilebilir. - - -Şablon değişkenleri -------------------- - -Değişkenler şablonlara `$this->template`'e yazılarak aktarılır. Şablonda yerel değişken olarak erişilebilir olurlar: - -```php -$this->template->article = $this->articles->getById($id); -``` - -Bir özelliğin değerini şablona otomatik olarak değişken şeklinde aktarmak için onu `#[TemplateVariable]` niteliğiyle ve public görünürlükle işaretleyin: .{data-version:3.2.9} - -```php -use Nette\Application\Attributes\TemplateVariable; - -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - #[TemplateVariable] - public string $siteName = 'Blogum'; -} -``` - -Şablona aynı adlı bir değişken aktarırsanız, `#[TemplateVariable]` onun üzerine yazmaz. - - -Varsayılan değişkenler ----------------------- - -Presenter'lar ve bileşenler şablonlara birkaç yararlı değişkeni otomatik aktarır: - -- `$basePath`, kök dizine giden mutlak URL yoludur (örneğin `/eshop`) -- `$baseUrl`, kök dizine giden mutlak URL'dir (örneğin `http://localhost/eshop`) -- `$user`, [kullanıcıyı temsil eden |security:authentication] bir nesnedir -- `$presenter`, geçerli presenter'dır -- `$control`, geçerli bileşen veya presenter'dır -- `$flashes`, `flashMessage()` fonksiyonuyla gönderilen [mesajların |presenters#Flash mesajları] dizisidir - -Kendi şablon sınıfınızı kullanıyorsanız, bu değişkenler onlar için bir özellik oluşturursanız aktarılır. - - -Tip güvenli şablonlar ---------------------- - -Sağlam uygulamalar geliştirirken, şablonun hangi değişkenleri beklediğini ve bunların tiplerini açıkça tanımlamak yararlıdır. Bu, PHP'de tip denetimi, IDE'nizde akıllı ipuçları sağlar ve statik analizin hataları yakalamasını mümkün kılar. - -Böyle bir listeyi nasıl tanımlarsınız? Basitçe, şablon değişkenlerini temsil eden özelliklere sahip bir sınıf olarak. Onu presenter'a benzer şekilde, sonuna `Template` ekleyerek adlandırın: - -```php -/** - * @property-read ArticleTemplate $template - */ -class ArticlePresenter extends Nette\Application\UI\Presenter -{ -} - -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - public Model\Article $article; - public Nette\Security\User $user; - - // ve diğer değişkenler -} -``` - -Presenter'daki `$this->template` nesnesi artık `ArticleTemplate` sınıfının bir örneği olacaktır. Böylece PHP, yazma sırasında bildirilen tipleri denetler. - -Nette şablon sınıfını otomatik seçer. Önce `Template` adlı bir sınıf arar, örneğin `edit` eylemi için `ArticleEditTemplate`, ve ancak o yoksa `Template`'e geri döner. - -`@property-read` anotasyonu IDE ve statik analiz içindir, kod tamamlamayı sağlar; bkz. "PhpStorm ve $this⁠-⁠>⁠template için kod tamamlama":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template. - -[* phpstorm-completion.webp *] - -Kod tamamlamayı doğrudan şablonlarda da kullanabilirsiniz. PhpStorm için Latte eklentisini kurmanız ve şablonun başında şablon parametre sınıfının adını belirtmeniz yeterlidir; ayrıntılar [Latte: tip sistemi |latte:type-system] bölümünde: - -```latte -{templateType App\Presentation\Article\ArticleTemplate} -... -``` - -Aynısı bileşenler için de geçerlidir. Adlandırma kuralına uyun ve `FifteenControl` gibi bir bileşen için `FifteenTemplate` parametre sınıfını oluşturun. - -Farklı bir parametre sınıfı kullanmanız gerekirse `createTemplate()` metodunu kullanın: - -```php -public function renderDefault(): void -{ - $template = $this->createTemplate(SpecialTemplate::class); - $template->foo = 123; - // ... - $this->sendTemplate($template); -} -``` - -.{data-version:3.3.0} -Şablonun render edilmeden önce nasıl tamamlanacağını etkilemeniz gerekirse, örneğin tüm eylemlerde ortak değişkenler eklemek için, presenter'da `completeTemplate()` metodunu ezebilirsiniz. Şablon render edilmeden hemen önce çağrılır: - -```php -protected function completeTemplate(Nette\Application\UI\Template $template): void -{ - parent::completeTemplate($template); - $template->siteName = 'Blogum'; -} -``` - - -Bağlantı oluşturma ------------------- - -Şablonda başka presenter'lara ve eylemlere bağlantılar şöyle oluşturulur: - -```latte -ürün detayı -``` - -`n:href` niteliği HTML `` etiketleri için çok kullanışlıdır. Bağlantıyı başka bir yerde, örneğin metin içinde yazdırmak istersek `{link}` kullanırız: - -```latte -URL şudur: {link Home:default} -``` - -Daha fazla bilgiyi [URL bağlantıları oluşturma|creating-links] bölümünde bulabilirsiniz. - - -Özel filtreler, etiketler vb. ------------------------------ - -Latte şablon sistemi özel filtreler, fonksiyonlar, etiketler ve başka öğelerle genişletilebilir. Hızlı geçici çözümlerden tüm uygulamalar için mimari kalıplara uzanan üç yaklaşım vardır. - -**Presenter metotlarında geçici olarak** - -En hızlı yaklaşım, filtreleri veya fonksiyonları doğrudan presenter ya da bileşen kodunda eklemektir. Presenter'larda bunun için `beforeRender()` veya `render()` metotları uygundur: - -```php -protected function beforeRender(): void -{ - // filtre ekleme - $this->template->addFilter('money', fn($val) => '$' . number_format($val, 2)); - - // fonksiyon ekleme - $this->template->addFunction('isWeekend', fn($date) => $date->format('N') >= 6); -} -``` - -Şablonda: - -```latte -

    Fiyat: {$price|money}

    - -{if isWeekend($now)} ... {/if} -``` - -Daha karmaşık mantık için `Latte\Engine` nesnesini doğrudan yapılandırabilirsiniz: - -```php -protected function beforeRender(): void -{ - $latte = $this->template->getLatte(); - $latte->setFeature(Latte\Feature::MigrationWarnings); -} -``` - -**Nitelikleri kullanarak** - -Daha zarif bir yaklaşım, filtreleri ve fonksiyonları doğrudan presenter'ın veya bileşenin [şablon parametre sınıfında|#Tip güvenli şablonlar] metot olarak tanımlamak ve niteliklerle işaretlemektir: - -```php -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - #[Latte\Attributes\TemplateFilter] - public function money(float $val): string - { - return '$' . number_format($val, 2); - } - - #[Latte\Attributes\TemplateFunction] - public function isWeekend(DateTimeInterface $date): bool - { - return $date->format('N') >= 6; - } -} -``` - -Latte, bu niteliklerle işaretlenmiş metotları otomatik bulur ve kaydeder. Şablonlardaki filtre veya fonksiyon adı metot adıyla aynıdır. Bu metotlar public olmalıdır. - -**Uzantılarla genel olarak** - -Önceki yaklaşımlar, yalnızca belirli presenter'larda veya bileşenlerde gereken filtre ve fonksiyonlar için uygundur, uygulama geneli için değil. Tüm uygulama için bir [uzantı |latte:extending-latte#Latte Extension] oluşturmak en iyi sonucu verir. Bu sınıf, projenizdeki tüm Latte uzantılarını tek yerde toplar. Kısa bir örnek: - -```php -namespace App\Presentation\Accessory; - -final class LatteExtension extends Latte\Extension -{ - public function __construct( - private App\Model\Facade $facade, - private Nette\Security\User $user, - // ... - ) { - } - - public function getFilters(): array - { - return [ - 'timeAgoInWords' => $this->filterTimeAgoInWords(...), - 'money' => $this->filterMoney(...), - // ... - ]; - } - - public function getFunctions(): array - { - return [ - 'canEditArticle' => - fn($article) => $this->facade->canEditArticle($article, $this->user->getId()), - // ... - ]; - } - - private function filterTimeAgoInWords(DateTimeInterface $time): string - { - // ... - } - - // ... -} -``` - -Uzantıyı [yapılandırma |configuration#Latte şablonları] üzerinden kaydedin: - -```neon -latte: - extensions: - - App\Presentation\Accessory\LatteExtension -``` - -Uzantılar birkaç avantaj sunar: bağımlılık enjeksiyonu desteği, uygulamanızın model katmanına erişim ve tüm uzantıların tek yerden yönetimi. Ayrıca özel etiketleri, sağlayıcıları, compiler pass'leri ve daha fazlasını desteklerler. - - -Tüm şablonların ayarlanması ---------------------------- - -Tüm şablonları oluşturan `TemplateFactory` servisi, public bir `$onCreate` callback dizisi sunar. Bunlar herhangi bir şablon her oluşturulduğunda çağrılır, böylece uygulamadaki tüm şablonlar için filtreleri, fonksiyonları veya değişkenleri tek bir yerden ayarlayabilirsiniz. Her callback, yeni oluşturulan şablonu alır. `TemplateFactory` servisini [enjekte ettirin |dependency-injection:passing-dependencies] ve callback'leri, örneğin uygulama başlarken kaydedin: - -```php -$templateFactory->onCreate[] = function (Nette\Bridges\ApplicationLatte\Template $template): void { - $template->addFilter('money', fn($val) => '$' . number_format($val, 2)); -}; -``` - - -Çeviri ------- - -Çok dilli bir uygulama programlıyorsanız, büyük olasılıkla şablondaki bazı metinleri farklı dillerde çıkarmanız gerekecek. Nette Framework bunun için tek bir `translate()` metoduna sahip [api:Nette\Localization\Translator] çeviri arayüzünü tanımlar. Genellikle bir dize olan `$message` mesajını ve başka parametreleri kabul eder. Görevi çevrilmiş dizeyi döndürmektir. Nette'in varsayılan bir uygulaması yoktur; ihtiyacınıza göre [Componette |https://componette.org/search/localization] üzerinde bulunan hazır çözümlerden seçebilirsiniz. Çevirmenin nasıl yapılandırılacağını dokümantasyonları anlatır. - -Şablonlara, [aktarılmasını sağladığımız |dependency-injection:passing-dependencies] çevirmen `setTranslator()` metoduyla ayarlanabilir: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator); -} -``` - -Alternatif olarak çevirmen [yapılandırma |configuration#Latte şablonları] ile ayarlanabilir: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -Sonra çevirmen örneğin bir `|translate` filtresi olarak, `translate()` metoduna aktarılan ek parametrelerle birlikte kullanılabilir (bkz. `foo, bar`): - -```latte -
    {='Sepet'|translate} -{$item|translate} -{$item|translate, foo, bar} -``` - -Ya da alt çizgi etiketi olarak: - -```latte -{_'Sepet'} -{_$item} -{_$item, foo, bar} -``` - -Şablonun bir bölümünü çevirmek için `{translate}` çift etiketi vardır (Latte 2.11'den beri, önceden `{_}` etiketi kullanılıyordu): - -```latte -{translate}Sipariş{/translate} -{translate foo, bar}Sipariş{/translate} -``` - -Çevirmen normalde şablon render edilirken, çalışma zamanında çağrılır. Ancak Latte 3 sürümü, tüm statik metinleri daha şablon derlenirken çevirebilir. Bu performanstan tasarruf sağlar, çünkü her dize yalnızca bir kez çevrilir ve elde edilen çeviri derlenmiş biçime yazılır. Böylece önbellek dizininde şablonun her dil için bir tane olmak üzere birden fazla derlenmiş sürümü oluşur. Bunun için dili ikinci parametre olarak belirtmeniz yeterlidir: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator, $lang); -} -``` - -Statik metin, örneğin `{_'merhaba'}` veya `{translate}merhaba{/translate}` demektir. `{_$foo}` gibi statik olmayan metinler çalışma zamanında çevrilmeye devam eder. diff --git a/application/tr/upgrading.texy b/application/tr/upgrading.texy deleted file mode 100644 index 64046aa60b..0000000000 --- a/application/tr/upgrading.texy +++ /dev/null @@ -1,47 +0,0 @@ -Yükseltme -********* - - -Sürüm 3.0'a yükseltme -===================== - -Nette 3.0, metot parametrelerine ve dönüş değerlerine tür bildirimleri ekler. Nette'ten kalıtım alan bir sınıfta (örneğin bir presenter'da veya bileşende) böyle bir metodu ezerseniz, aynı tür bildirimlerini eklemeniz gerekir; aksi halde PHP "Declaration must be compatible" hatası verir. - -`Nette\Application\IRouter` arayüzü değişti. `match()` metodu artık bir `Nette\Application\Request` nesnesi yerine bir parametre dizisi döndürür, `constructUrl()` de böyle bir dizi kabul eder. - -Nette artık her sinyalin aynı origin'den (yani aynı alan adı ve alt alan adından) gönderildiğini denetler. Bu same-origin politikası, olası saldırı vektörlerini azaltmaya yardımcı olan kritik bir güvenlik mekanizmasıdır. Başka origin'lere izin vermek isterseniz, işleyici metoduna `@crossOrigin` anotasyonunu ekleyin: - -```php -/** - * @crossOrigin - */ -public function handleXy(): void -{ -} -``` - -Aynısı form gönderimleri için de geçerlidir. Başka origin'lerden gönderime izin vermek isterseniz şöyle yapın: - -```php -$form = new Nette\Application\UI\Form; -$form->allowCrossOrigin(); -``` - -`Nette\ComponentModel\Component` sınıfının constructor'ı yıllardır kullanılmıyordu ve 3.0 sürümünde kaldırıldı. Bu bir BC break'tir: `Nette\Application\UI\Presenter`'dan kalıtım alan bir bileşende veya presenter'da üst sınıfın constructor'ını çağırıyorsanız, çağrıyı kaldırmanız gerekir. - - -Sürüm 2.4'e yükseltme -===================== - -- `Route` ve `SimpleRouter` artık siteye erişilirken kullanılan HTTP/HTTPS şemasının aynısını üretir. Belirli bir protokol gerektiren bir rota, şemayla birlikte tanımlanabilir, örneğin `Route('http://domain.cz/')`. -- render/action metotlarının bool tipindeki parametrelerinde (yani varsayılan değeri true veya false olanlarda) ve kalıcı parametrelerde artık `false` ile `null` birbirinden ayrılıyor. Parametre URL'de yoksa değeri artık `null` (önceden `false`). -- `Presenter::getReflection()` tarafından döndürülen sınıf artık `Nette\Reflection\ClassType`'ın torunu değil, `getReflection()->getMethod()` da artık `Nette\Reflection\Method`'un torunu değil. -- `SECURED` bayrağı ve `Route::$defaultFlags` kullanımdan kaldırıldı. - - -Sürüm 2.3'e yükseltme -===================== - -- rotalar ve presenter adları **büyük-küçük harfe duyarlıdır**. Presenter adında yanlış harf kullanırsanız Nette sizi uyarır; performans nedeniyle Route maskesi denetlenmez, bu yüzden onu elle doğrulayın. -- `Route::addStyle()` ve `Route::setStyleProperty()` kullanımdan kaldırıldı ve artık `E_USER_DEPRECATED` tetikliyor. -- `.phtml` şablon uzantısı ve eski bağlantı sözdizimi artık desteklenmiyor. diff --git a/assets/cs/@home.texy b/assets/cs/@home.texy deleted file mode 100644 index 9409d66d2b..0000000000 --- a/assets/cs/@home.texy +++ /dev/null @@ -1,478 +0,0 @@ -Nette Assets -************ - -
    - -Už vás nebaví ručně spravovat statické soubory ve vašich webových aplikacích? Zapomeňte na natvrdo zapsané cesty, řešení zneplatnění cache nebo starosti s verzováním souborů. Nette Assets transformuje způsob, jakým pracujete s obrázky, styly, skripty a dalšími statickými zdroji. - -- **Chytré verzování** zajistí, že prohlížeče vždy načtou nejnovější soubory -- **Automatická detekce** typů a rozměrů souborů -- **Bezproblémová integrace** s Latte pomocí intuitivních tagů -- **Flexibilní architektura** podporující souborové systémy, CDN a Vite -- **Líné načítání** pro optimální výkon - -
    - - -Proč Nette Assets? -================== - -Práce se statickými soubory často znamená opakující se kód náchylný k chybám. Ručně konstruujete URL, přidáváte parametry verzí pro invalidaci cache a řešíte různé typy souborů odlišně. To vede ke kódu jako: - -```latte -Logo - -``` - -S Nette Assets veškerá tato složitost mizí: - -```latte -{* Vše automatizováno - URL, verzování, rozměry *} - - - -{* Nebo jen *} -{asset 'css/style.css'} -``` - -To je vše! Knihovna automaticky: -- Přidává parametry verzí na základě času poslední úpravy souboru -- Detekuje rozměry obrázků a zahrnuje je do HTML -- Generuje správný HTML element pro každý typ souboru -- Zpracovává vývojové i produkční prostředí - - -Instalace -========= - -Nainstalujte Nette Assets pomocí [Composeru|best-practices:composer]: - -```shell -composer require nette/assets -``` - -Vyžaduje PHP 8.1 nebo vyšší a perfektně funguje s Nette Frameworkem, ale lze jej použít i samostatně. - - -První kroky -=========== - -Nette Assets funguje ihned po instalaci bez jakékoli konfigurace. Umístěte své statické soubory do adresáře `www/assets/` a začněte je používat: - -```latte -{* Zobrazí obrázek s automatickými rozměry *} -{asset 'logo.png'} - -{* Zahrne šablonu stylů s verzováním *} -{asset 'style.css'} - -{* Načte skript *} -{asset 'app.js'} -``` - -Pro větší kontrolu nad generovaným HTML použijte atribut `n:asset` nebo funkci `asset()`. - - -Jak to funguje -============== - -Nette Assets je postaveno na třech základních konceptech, které jej činí výkonným, ale zároveň jednoduchým na použití: - - -Assety - chytré soubory ------------------------ - -**Asset** představuje jakýkoli statický soubor ve vaší aplikaci. Každý soubor se stává objektem s užitečnými vlastnostmi jen pro čtení: - -```php -$image = $assets->getAsset('photo.jpg'); -echo $image->url; // '/assets/photo.jpg?v=1699123456' -echo $image->file; // '/var/www/assets/photo.jpg' (lokální cesta, nebo null) -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' -``` - -Různé typy souborů poskytují různé vlastnosti: -- **Obrázky**: šířka, výška, alternativní text, líné načítání -- **Skripty**: typ modulu, integrity hashe, crossorigin -- **Styly**: media queries, integrity -- **Audio/Video**: délka, rozměry (jen video) -- **Fonty**: správné přednačítání s CORS - -Knihovna automaticky detekuje typy souborů a vytváří odpovídající třídu assetu. - - -Mappery - odkud soubory pocházejí ---------------------------------- - -**Mapper** ví, jak najít soubory a vytvořit pro ně URL. Můžete mít více mapperů pro různé účely - lokální soubory, CDN, cloudové úložiště nebo build nástroje (každý z nich má jméno). Vestavěný `FilesystemMapper` zpracovává lokální soubory, zatímco `ViteMapper` se integruje s moderními build nástroji. - -Mappery jsou definovány v [konfiguraci |configuration]. - - -Registry - vaše hlavní rozhraní -------------------------------- - -**Registry** spravuje všechny mappery a poskytuje hlavní API: - -```php -// Nechte si registry předat do své služby -public function __construct( - private Nette\Assets\Registry $assets -) {} -``` - -```php -// Získejte assety z různých mapperů -$logo = $this->assets->getAsset('images:logo.png'); // mapper 'images' -$app = $this->assets->getAsset('app:main.js'); // mapper 'app' -$style = $this->assets->getAsset('style.css'); // používá výchozí mapper -``` - -Registry automaticky vybírá správný mapper a kvůli výkonu kešuje výsledky. - - -Práce s assety v PHP -==================== - -Registry poskytuje dvě metody pro získávání assetů: - -```php -// Vyvolá Nette\Assets\AssetNotFoundException, pokud soubor neexistuje -$logo = $assets->getAsset('logo.png'); - -// Vrátí null, pokud soubor neexistuje -$banner = $assets->tryGetAsset('banner.jpg'); -if ($banner) { - echo $banner->url; -} -``` - - -Určení mapperů --------------- - -Můžete explicitně zvolit, který mapper použít: - -```php -// Použít výchozí mapper -$file = $assets->getAsset('document.pdf'); - -// Použít konkrétní mapper s prefixem -$image = $assets->getAsset('images:photo.jpg'); - -// Použít konkrétní mapper se syntaxí pole -$script = $assets->getAsset(['scripts', 'app.js']); -``` - - -Vlastnosti a typy assetů ------------------------- - -Každý typ assetu poskytuje relevantní vlastnosti jen pro čtení: - -```php -// Vlastnosti obrázku -$image = $assets->getAsset('photo.jpg'); -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' - -// Vlastnosti skriptu -$script = $assets->getAsset('app.js'); -echo $script->type; // null ('module' u Vite entry pointů) - -// Vlastnosti audia -$audio = $assets->getAsset('song.mp3'); -echo $audio->duration; // délka v sekundách - -// Všechny assety lze přetypovat na řetězec (vrací URL) -$url = (string) $assets->getAsset('document.pdf'); -``` - -.[note] -Vlastnosti jako rozměry nebo délka se načítají líně pouze při přístupu, což udržuje knihovnu rychlou. - -.[tip] -Pro přesnou statickou analýzu nainstalujte rozšíření [nette/phpstan-rules |tools:phpstan-rules#Assety]. PHPStan pak zná konkrétní typ každého assetu, takže `getAsset('photo.jpg')` chápe jako `ImageAsset` a přístup k `->width` nehlásí chybu. - - -Použití assetů v šablonách Latte -================================ - -Nette Assets poskytuje intuitivní integraci s [Latte|latte:] pomocí tagů a funkcí. - - -`{asset}` ---------- - -Tag `{asset}` vykresluje kompletní HTML elementy: - -```latte -{* Vykreslí: *} -{asset 'hero.jpg'} - -{* Vykreslí: *} -{asset 'app.js'} - -{* Vykreslí: *} -{asset 'style.css'} -``` - -Tag automaticky: -- Detekuje typ assetu a generuje odpovídající HTML -- Zahrnuje verzování pro invalidaci cache -- Přidává rozměry pro obrázky -- Nastavuje správné atributy (typ, media atd.) - -Při použití uvnitř HTML atributů nebo uvnitř elementů ` -``` - - -Vlastní mappery -=============== - -Vytvořte vlastní mappery pro speciální potřeby, jako je cloudové úložiště nebo dynamické generování: - -```php -use Nette\Assets\Mapper; -use Nette\Assets\Asset; -use Nette\Assets\Helpers; - -class CloudStorageMapper implements Mapper -{ - public function __construct( - private CloudClient $client, - private string $bucket, - ) {} - - public function getAsset(string $reference, array $options = []): Asset - { - if (!$this->client->exists($this->bucket, $reference)) { - throw new Nette\Assets\AssetNotFoundException("Asset '$reference' not found"); - } - - $url = $this->client->getPublicUrl($this->bucket, $reference); - return Helpers::createAssetFromUrl($url); - } -} -``` - -Registrace v konfiguraci: - -```neon -assets: - mapping: - cloud: CloudStorageMapper(@cloudClient, 'my-bucket') -``` - -Použijte jako jakýkoli jiný mapper: - -```latte -{asset 'cloud:user-uploads/photo.jpg'} -``` - -Metoda `Helpers::createAssetFromUrl()` automaticky vytvoří správný typ assetu na základě přípony souboru. - -Typy assetů, které implementují rozhraní `Nette\Assets\HtmlRenderable` (obrázky, skripty, styly a podobně), umí `{asset}` vykreslit jako kompletní HTML element. Ostatní typy souborů se stanou `GenericAsset` (například PDF), který nelze vykreslit jako HTML element, poskytuje ale URL (a další metadata). Pokus o vykreslení takového assetu jako HTML elementu vyhodí `Nette\InvalidArgumentException`; jeho URL ale stále můžete použít uvnitř atributu. - - -Další četba -=========== - -- [Nette Assets: Konečně jednotné API pro vše od obrázků po Vite |https://blog.nette.org/cs/predstaveni-nette-assets] - -{{maintitle: Nette Assets: správa statických souborů v PHP aplikacích}} -{{description: Žádné natvrdo zapsané cesty ani starosti se zneplatněním cache. Nette Assets spravuje obrázky, styly a skripty ve vaší PHP aplikaci, v kódu i v šablonách Latte.}} diff --git a/assets/cs/@left-menu.texy b/assets/cs/@left-menu.texy deleted file mode 100644 index e7579384f8..0000000000 --- a/assets/cs/@left-menu.texy +++ /dev/null @@ -1,14 +0,0 @@ -Nette Assets -************ -- [Úvod |@home] -- [Vite |vite] -- [Konfigurace |Configuration] - - -Další četba -*********** -- [Dokumentace Nette |nette:] -- [Aplikace v Nette |application:how-it-works] -- [Utilities |utils:] -- [Návody a postupy |best-practices:] -- [Řešení problémů |nette:troubleshooting] diff --git a/assets/cs/@meta.texy b/assets/cs/@meta.texy deleted file mode 100644 index 462d9add80..0000000000 --- a/assets/cs/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokumentace}} diff --git a/assets/cs/configuration.texy b/assets/cs/configuration.texy deleted file mode 100644 index 1db1a731d9..0000000000 --- a/assets/cs/configuration.texy +++ /dev/null @@ -1,199 +0,0 @@ -Konfigurace Assets -****************** - -.[perex] -Přehled možností konfigurace pro Nette Assets. - - -```neon -assets: - # základní cesta pro řešení relativních cest mapperů - basePath: ... # (string) výchozí hodnota je %wwwDir% - - # základní URL pro řešení relativních URL mapperů - baseUrl: ... # (string) výchozí hodnota je %baseUrl% - - # povolit globální verzování assetů? - versioning: ... # (bool) výchozí hodnota je true - - # definuje mappery assetů - mapping: ... # (array) výchozí hodnota je cesta 'assets' -``` - -`basePath` nastavuje výchozí adresář souborového systému pro řešení relativních cest v mapperech. Ve výchozím nastavení používá webový adresář (`%wwwDir%`). - -`baseUrl` nastavuje výchozí URL prefix pro řešení relativních URL v mapperech. Ve výchozím nastavení používá kořenovou URL (`%baseUrl%`). - -Možnost `versioning` globálně řídí, zda jsou k URL assetů přidávány parametry verzí pro invalidaci cache. Jednotlivé mappery mohou toto nastavení přepsat. - - -Mappery -------- - -Mappery lze konfigurovat třemi způsoby: jednoduchou řetězcovou notací, detailní notací pole nebo jako službu (pomocí entity notace `NazevTridy(...)` nebo odkazu `@nazevSluzby`). - -Nejjednodušší způsob, jak definovat mapper: - -```neon -assets: - mapping: - default: assets # Vytvoří filesystem mapper pro %wwwDir%/assets/ - images: img # Vytvoří filesystem mapper pro %wwwDir%/img/ - scripts: js # Vytvoří filesystem mapper pro %wwwDir%/js/ -``` - -Každý mapper vytvoří `FilesystemMapper`, který: -- Hledá soubory v `%wwwDir%/` -- Generuje URL jako `%baseUrl%/` -- Dědí globální nastavení verzování -- Najde jen soubory uvnitř tohoto adresáře: odkaz jako `../secret.txt` vyhodí `Nette\Assets\AssetNotFoundException` .{data-version:1.0.8} - - -Pro větší kontrolu použijte detailní notaci: - -```neon -assets: - mapping: - images: - # adresář, kde jsou soubory uloženy - path: ... # (string) volitelné, výchozí hodnota je základní cesta (basePath) - - # URL prefix pro generované odkazy - url: ... # (string) volitelné, výchozí hodnota je cesta - - # povolit verzování pro tento mapper? - versioning: ... # (bool) volitelné, dědí globální nastavení - - # automaticky přidat příponu(y) při hledání souborů - extension: ... # (string|array) volitelné, výchozí hodnota je null -``` - -Jak se řeší konfigurační hodnoty: - -Řešení cest: - - Relativní cesty jsou řešeny z `basePath` (nebo `%wwwDir%`, pokud `basePath` není nastaveno) - - Absolutní cesty jsou použity tak, jak jsou - -Řešení URL: - - Relativní URL jsou řešeny z `baseUrl` (nebo `%baseUrl%`, pokud `baseUrl` není nastaveno) - - Absolutní URL (se schématem nebo `//`) jsou použity tak, jak jsou (URL začínající `//` dostane schéma z `baseUrl`) - - Pokud `url` není uvedeno, použije se hodnota `path` - - -```neon -assets: - basePath: /var/www/project/www - baseUrl: https://example.com/assets - - mapping: - # Relativní cesta a URL - images: - path: img # Vyřešeno na: /var/www/project/www/img - url: images # Vyřešeno na: https://example.com/assets/images - - # Absolutní cesta a URL - uploads: - path: /var/shared/uploads # Použito tak, jak je: /var/shared/uploads - url: https://cdn.example.com # Použito tak, jak je: https://cdn.example.com - - # Uvedena pouze cesta - styles: - path: css # Cesta: /var/www/project/www/css - # URL: https://example.com/assets/css -``` - - -Vlastní mappery ---------------- - -Pro vlastní mappery se odkažte na existující službu pomocí `@nazevSluzby`, nebo je definujte přímo zápisem `NazevTridy(argumenty)` či holým názvem třídy: - -```neon -services: - s3mapper: App\Assets\S3Mapper(%s3.bucket%) - -assets: - mapping: - cloud: @s3mapper # odkaz na službu (od verze 1.0.8) - database: App\Assets\DatabaseMapper(@database.connection) -``` - - -Vite Mapper ------------ - -Vite mapper vyžaduje pouze přidání `type: vite`. Zde je kompletní seznam konfiguračních možností: - -```neon -assets: - mapping: - default: - # typ mapperu (vyžadováno pro Vite) - type: vite # (string) vyžadováno, musí být 'vite' - - # výstupní adresář Vite buildu - path: ... # (string) volitelné, výchozí hodnota je základní cesta (basePath) - - # URL prefix pro sestavené assety - url: ... # (string) volitelné, výchozí hodnota je cesta - - # umístění souboru manifestu Vite - manifest: ... # (string) volitelné, relativně k path, výchozí hodnota je /.vite/manifest.json - - # konfigurace Vite dev serveru - devServer: ... # (bool|string) volitelné, výchozí hodnota je true - - # verzování souborů ve veřejném adresáři - versioning: ... # (bool) volitelné, dědí globální nastavení - - # automatická přípona pro soubory ve veřejném adresáři - extension: ... # (string|array) volitelné, výchozí hodnota je null -``` - -Možnost `devServer` řídí, jak se assety načítají během vývoje: - -- `true` (výchozí) - Automaticky detekuje běžící Vite dev server (pomocí souboru `.vite/nette.json`, který Nette Vite plugin vytváří ve výstupním adresáři buildu). Pokud je dev server spuštěn **a vaše aplikace je v debug režimu**, assety se z něj načítají s podporou hot module replacement. Pokud dev server neběží, assety se načítají ze sestavených souborů ve veřejném adresáři. -- `false` - Zcela zakáže integraci dev serveru. Assety se vždy načítají ze sestavených souborů. -- Vlastní URL (např. `https://localhost:5173`) - Ručně zadejte URL dev serveru včetně protokolu a portu. Užitečné, když dev server běží na jiném hostiteli nebo portu. Stejně jako automatická detekce se uplatní jen v debug režimu; v produkci se vždy použijí sestavené soubory. - -Možnosti `versioning` a `extension` se vztahují pouze na soubory ve veřejném adresáři Vite, které nejsou zpracovány Vite. - - -Ruční konfigurace ------------------ - -Pokud nepoužíváte Nette DI, nakonfigurujte mappery ručně: - -```php -use Nette\Assets\Registry; -use Nette\Assets\FilesystemMapper; -use Nette\Assets\ViteMapper; - -$registry = new Registry; - -// Přidat filesystem mapper -$registry->addMapper('images', new FilesystemMapper( - baseUrl: 'https://example.com/img', - basePath: __DIR__ . '/www/img', - extensions: ['webp', 'jpg', 'png'], - versioning: true, -)); - -// Přidat Vite mapper -$registry->addMapper('app', new ViteMapper( - baseUrl: '/build', - basePath: __DIR__ . '/www/build', - manifestPath: __DIR__ . '/www/build/.vite/manifest.json', - devServer: 'https://localhost:5173', -)); -``` - -Kterýkoli zaregistrovaný mapper můžete také získat podle jména metodou `getMapper()`: - -```php -$mapper = $registry->getMapper('images'); // vrátí zaregistrovaný mapper -$default = $registry->getMapper(); // vrátí mapper 'default' -``` - -{{maintitle: Konfigurace Nette Assets: mappery, cesty a verzování}} -{{description: Konfigurace Nette Assets v NEON: základní cesta a URL, globální verzování a mappery, které určují, kde jsou vaše statické soubory, včetně Vite.}} diff --git a/assets/cs/vite.texy b/assets/cs/vite.texy deleted file mode 100644 index f1d984507b..0000000000 --- a/assets/cs/vite.texy +++ /dev/null @@ -1,624 +0,0 @@ -Integrace s Vite -**************** - -
    - -Moderní JavaScriptové aplikace vyžadují sofistikované build nástroje. Nette Assets poskytuje prvotřídní integraci s [Vite |https://vite.dev/], nástrojem pro frontend build nové generace. Získejte bleskově rychlý vývoj s Hot Module Replacement (HMR) a optimalizované produkční buildy bez potíží s konfigurací. - -- **Nulová konfigurace** - automatické propojení mezi Vite a PHP šablonami -- **Kompletní správa závislostí** - jeden tag zpracovává všechny assety -- **Hot Module Replacement** - okamžité aktualizace JavaScriptu a CSS -- **Optimalizované produkční buildy** - code splitting a tree shaking - -
    - - -Nette Assets se bezproblémově integruje s Vite, takže získáte všechny tyto výhody, zatímco budete psát své šablony jako obvykle. - - -Nastavení Vite -============== - -Pojďme nastavit Vite krok za krokem. Nebojte se, pokud s build nástroji teprve začínáte - vše vysvětlíme! - - -Krok 1: Instalace Vite ----------------------- - -Nejprve nainstalujte Vite a Nette plugin do vašeho projektu: - -```shell -npm install -D vite @nette/vite-plugin -``` - -Tím se nainstaluje Vite a speciální plugin, který pomáhá Vite perfektně fungovat s Nette. - - -Krok 2: Struktura projektu --------------------------- - -Standardní přístup je umístit zdrojové soubory assetů do složky `assets/` v kořenovém adresáři projektu a zkompilované verze do `www/assets/`: - -/--pre -web-project/ -├── assets/ ← zdrojové soubory (SCSS, TypeScript, zdrojové obrázky) -│ ├── public/ ← statické soubory (kopírovány tak, jak jsou) -│ │ └── favicon.ico -│ ├── images/ -│ │ └── logo.png -│ ├── app.js ← hlavní vstupní bod -│ └── style.css ← vaše styly -└── www/ ← veřejný adresář (document root) - ├── assets/ ← zde budou zkompilované soubory - └── index.php -\-- - -Složka `assets/` obsahuje vaše zdrojové soubory - kód, který píšete. Vite tyto soubory zpracuje a umístí zkompilované verze do `www/assets/`. - - -Krok 3: Konfigurace Vite ------------------------- - -Vytvořte soubor `vite.config.ts` v kořenovém adresáři projektu. Tento soubor říká Vite, kde má najít vaše zdrojové soubory a kam má umístit zkompilované. - -Nette Vite plugin přichází s chytrými výchozími nastaveními, která zjednodušují konfiguraci. Předpokládá, že vaše front-end zdrojové soubory jsou v adresáři `assets/` (možnost `root`) a zkompilované soubory jdou do `www/assets/` (možnost `outDir`). Stačí uvést [vstupní bod |#Vstupní body]: - -```js -import { defineConfig } from 'vite'; -import nette from '@nette/vite-plugin'; - -export default defineConfig({ - plugins: [ - nette({ - entry: 'app.js', - }), - ], -}); -``` - -Plugin pod kapotou kromě `root` a `outDir` nastavuje ještě několik dalších voleb Vite, aby vše do sebe zapadlo: `base` na `''` (assety se servírují přímo z document rootu), `build.manifest` na `true` (aby Nette Assets uměly namapovat hashované názvy souborů) a `build.assetsDir` na `''` (zkompilované soubory jdou přímo do `outDir`, bez podsložky `static/`). Kteroukoli z nich můžete přepsat. - -.[note] -Výchozí `outDir` (`www/assets`) vyžaduje, aby adresář `www/` už existoval. Pokud neexistuje, plugin skončí chybou *"The output directory ... does not exist"*. - -Pokud chcete zadat jiný název adresáře pro sestavení vašich assetů, budete muset změnit několik možností: - -```js -export default defineConfig({ - root: 'assets', // kořenový adresář zdrojových assetů - - build: { - outDir: '../www/assets', // kam jdou zkompilované soubory - }, - - // ... další konfigurace ... -}); -``` - -.[note] -Cesta `outDir` je považována za relativní k `root`, proto je na začátku `../`. - - -Krok 4: Konfigurace Nette -------------------------- - -Řekněte Nette Assets o Vite ve vašem `common.neon`: - -```neon -assets: - mapping: - default: - type: vite # říká Nette, aby použilo ViteMapper - path: assets -``` - - -Krok 5: Přidání skriptů ------------------------ - -Přidejte tyto skripty do vašeho `package.json`: - -```json -{ - "scripts": { - "dev": "vite", - "build": "vite build" - } -} -``` - -Nyní můžete: -- `npm run dev` - spustí vývojový server s hot reloadingem -- `npm run build` - vytvoří optimalizované produkční soubory - - -Vstupní body -============ - -**Vstupní bod** je hlavní soubor, kde začíná vaše aplikace. Z tohoto souboru importujete další soubory (CSS, JavaScript moduly, obrázky), čímž vytváříte strom závislostí. Vite sleduje tyto importy a vše zabalí dohromady. - -Příklad vstupního bodu `assets/app.js`: - -```js -// Importovat styly -import './style.css' - -// Importovat JavaScript moduly -import netteForms from 'nette-forms'; -import naja from 'naja'; - -// Inicializovat vaši aplikaci -netteForms.initOnLoad(); -naja.initialize(); -``` - -V šabloně můžete vložit vstupní bod následovně: - -```latte -{asset 'app.js'} -``` - -Nette Assets automaticky generuje všechny potřebné HTML tagy - JavaScript, CSS a jakékoli další závislosti. - -.[note]{data-version:1.0.8} -Pro vstupní body používejte značku `{asset}`, ne atribut `n:asset`. Atribut dokáže doplnit jen samotný element ` - *} -{* V produkci: - - *} -``` - -Není potřeba žádná konfigurace - prostě to funguje! Pokud chcete detekci vypnout nebo zadat URL dev serveru ručně, podívejte se na [volbu devServer |configuration#Vite Mapper]. - -.[note] -Signální soubor je uložen v `www/assets/.vite/nette.json` (hned vedle produkčního `manifest.json`). Přejmenovat ho můžete volbou pluginu `infoFile`, která má výchozí hodnotu `.vite/nette.json`. Pokud Nette běžící dev server nezachytí, zkontrolujte, že tento soubor existuje a odkazuje na správnou URL. - - -Automatické obnovení při změně šablon -------------------------------------- - -Vite HMR sleduje jen soubory ve svém grafu modulů – váš JavaScript, CSS a to, co importují. Latte šablony a PHP soubory jsou mimo něj, takže jejich úprava se v prohlížeči sama neprojeví, dokud stránku ručně neobnovíte. - -Volba `refresh` to řeší. Zadejte jí glob patterny; kdykoli se odpovídající soubor změní, plugin vyvolá úplné obnovení stránky: - -```js -nette({ - entry: 'app.js', - refresh: 'app/**/*.latte', // obnovení při změně jakékoli Latte šablony -}), -``` - -Můžete předat i pole patternů. Vyhodnocují se vůči celému projektu, ne jen vůči `root`. - - -Práce na různých doménách -------------------------- - -Pokud váš vývojový server běží na něčem jiném než `localhost` (například `myapp.local`), můžete narazit na problémy s CORS (Cross-Origin Resource Sharing). CORS je bezpečnostní funkce ve webových prohlížečích, která ve výchozím nastavení blokuje požadavky mezi různými doménami. Když vaše PHP aplikace běží na `myapp.local`, ale Vite běží na `localhost:5173`, prohlížeč je vnímá jako různé domény a blokuje požadavky. - -Nejčistší řešení je říct pluginu, kde vaše PHP aplikace běží, pomocí volby `appUrl`. Plugin pak tento origin (přes http i https, na libovolném portu) povolí v CORS dev serveru, takže prohlížeč požadavky nikdy neblokuje: - -```js -nette({ - entry: 'app.js', - appUrl: 'https://myapp.local', // kde běží vaše PHP aplikace -}), -``` - -Pokud raději konfigurujete Vite ručně, máte dvě možnosti: - -**Možnost 1: Konfigurace CORS** - -Nejjednodušší řešení je povolit cross-origin požadavky z vaší PHP aplikace: - -```js -export default defineConfig({ - // ... další konfigurace ... - - server: { - cors: { - origin: 'http://myapp.local', // URL vaší PHP aplikace - }, - }, -}); -``` - -**Možnost 2: Spusťte Vite na vaší doméně** - -Dalším řešením je nechat Vite běžet na stejné doméně jako vaše PHP aplikace. - -```js -export default defineConfig({ - // ... další konfigurace ... - - server: { - host: 'myapp.local', // stejné jako vaše PHP aplikace - }, -}); -``` - -Ve skutečnosti i v tomto případě musíte nakonfigurovat CORS, protože dev server běží na stejném hostiteli, ale na jiném portu. V tomto případě však CORS automaticky nakonfiguruje Nette Vite plugin. - - -Vývoj s HTTPS -------------- - -Pokud vyvíjíte na HTTPS, potřebujete certifikáty pro váš Vite vývojový server. Nejjednodušší způsob je použití pluginu, který automaticky generuje certifikáty: - -```shell -npm install -D vite-plugin-mkcert -``` - -Takto to nakonfigurujete v `vite.config.ts`: - -```js -import mkcert from 'vite-plugin-mkcert'; - -export default defineConfig({ - // ... další konfigurace ... - - plugins: [ - mkcert(), // automaticky generuje certifikáty a povolí https - nette(), - ], -}); -``` - -Všimněte si, že pokud používáte konfiguraci CORS (možnost 1 výše), musíte v ní upravit origin tak, aby používal `https://` namísto `http://`. - - -Vývoj s Dockerem ----------------- - -Když spouštíte Vite uvnitř Docker kontejneru, je potřeba ohlídat dvě věci: prohlížeč na vašem počítači musí být schopen dosáhnout na vývojový server a Vite musí rozpoznat změny souborů přes hranici kontejneru. - -Nejprve z kontejneru [zpřístupněte |https://docs.docker.com/get-started/docker-concepts/running-containers/publishing-ports/] port Vite a navažte vývojový server na všechna rozhraní, aby byl dostupný zvenčí kontejneru: - -```js -export default defineConfig({ - // ... další konfigurace ... - - plugins: [ - nette(), - ], - server: { - host: '0.0.0.0', // naslouchá na všech rozhraních (v kontejneru nutné) - port: 5173, // musí odpovídat zpřístupněnému portu - strictPort: true, // raději selže, než by zvolil jiný port - watch: { - usePolling: true, // zapněte, pokud se změny souborů na připojených svazcích nedetekují - }, - }, -}); -``` - -Plugin zapíše URL vývojového serveru do `nette.json` pro PHP stranu. Protože `host: '0.0.0.0'` není použitelný v prohlížeči (u každého assetu přesměruje na `localhost`), plugin ho v této URL automaticky přepíše na `localhost`, takže se assety načtou správně. - -Pokud aplikaci otevíráte na vlastní doméně místo `localhost`, nastavte volbu pluginu `host` na tuto doménu: - -```js - plugins: [ - nette({ host: 'myapp.local' }), // stejná doména jako vaše PHP aplikace - ], -``` - -Plugin pak tento host použije pro URL vývojového serveru, přidá ho mezi CORS origins a povolí ho ve Vite `allowedHosts` - takže to funguje bez ruční konfigurace CORS. - -U složitějších nasazení - například když Vite běží za reverzní proxy, kde se veřejný host, port i protokol liší od interní adresy - nastavte ve Vite `server.origin` na úplnou veřejnou URL. Plugin ji respektuje a zapíše do `nette.json` tak, jak je, místo aby URL odvozoval z lokálního socketu: - -```js - server: { - origin: 'https://myapp.local:8443', // veřejná URL, na které prohlížeč dosáhne na Vite - }, -``` - - -Testování na jiných zařízeních ------------------------------- - -Chcete-li otevřít běžící aplikaci na telefonu nebo jiném zařízení ve stejné síti, nastavte volbu pluginu `host` na `'network'`. Plugin naváže Vite na všechna rozhraní a do `nette.json` zapíše LAN IP vašeho počítače (místo `localhost`), takže se assety a HMR načtou i z druhého zařízení: - -```js -nette({ - entry: 'app.js', - host: 'network', // ohlásí LAN IP počítače -}), -``` - -Poté na zařízení otevřete svou PHP aplikaci přes stejnou LAN IP. Pokud se žádná externí adresa nenajde, plugin se vrátí k `localhost`. - - -Produkční buildy -================ - -Vytvořte optimalizované produkční soubory: - -```shell -npm run build -``` - -Vite bude: -- Minifikovat veškerý JavaScript a CSS -- Rozdělit kód do optimálních chunků -- Generovat hashované názvy souborů pro cache-busting -- Vytvořit soubor manifestu pro Nette Assets - -Příklad výstupu: - -``` -www/assets/ -├── app-4f3a2b1c.js # Váš hlavní JavaScript (minifikovaný) -├── app-7d8e9f2a.css # Extrahované CSS (minifikované) -├── vendor-8c4b5e6d.js # Sdílené závislosti -└── .vite/ - └── manifest.json # Mapování pro Nette Assets -``` - -Hashované názvy souborů zajišťují, že prohlížeče vždy načtou nejnovější verzi. - - -Veřejná složka -============== - -Soubory v adresáři `assets/public/` jsou kopírovány do výstupu bez zpracování: - -``` -assets/ -├── public/ -│ ├── favicon.ico -│ ├── robots.txt -│ └── images/ -│ └── og-image.jpg -├── app.js -└── style.css -``` - -Odkazujte na ně normálně: - -```latte -{* Tyto soubory jsou kopírovány tak, jak jsou *} - - -``` - -Pro veřejné soubory můžete použít funkce FilesystemMapperu. Volba `extension` se uplatní u referencí bez přípony (např. `{asset 'images/og-image'}` najde nejdřív `og-image.webp`): - -```neon -assets: - mapping: - default: - type: vite - path: assets - extension: [webp, jpg, png] # pro reference bez přípony - versioning: true # Přidat cache-busting -``` - -V konfiguraci `vite.config.ts` můžete změnit veřejnou složku pomocí možnosti `publicDir`. - - -Dynamické importy -================= - -Vite automaticky rozděluje kód pro optimální načítání. Dynamické importy vám umožňují načítat kód pouze tehdy, když je skutečně potřeba, čímž se snižuje počáteční velikost balíčku: - -```js -// Načíst náročné komponenty na vyžádání -button.addEventListener('click', async () => { - let { Chart } = await import('./components/chart.js') - new Chart(data) -}) -``` - -Dynamické importy vytvářejí samostatné chunky, které se načítají pouze v případě potřeby. Tomu se říká "code splitting" a je to jedna z nejvýkonnějších funkcí Vite. Když používáte dynamické importy, Vite automaticky vytváří samostatné JavaScriptové soubory pro každý dynamicky importovaný modul. - -Tag `{asset 'app.js'}` **automaticky nepřednačítá** tyto dynamické chunky. Toto je záměrné chování - nechceme stahovat kód, který by se nikdy nemusel použít. Chunky se stahují pouze při provedení dynamického importu. - -Pokud však víte, že určité dynamické importy jsou kritické a budou brzy potřeba, můžete je přednačíst: - -```latte -{* Hlavní vstupní bod *} -{asset 'app.js'} - -{* Přednačíst kritické dynamické importy *} -{preload 'components/chart.js'} -``` - -To říká prohlížeči, aby stáhl komponentu grafu na pozadí, takže je okamžitě připravena, když je potřeba. - - -Podpora TypeScriptu -=================== - -TypeScript funguje bez další konfigurace: - -```ts -// assets/main.ts -interface User { - name: string - email: string -} - -export function greetUser(user: User): void { - console.log(`Hello, ${user.name}!`) -} -``` - -Odkazujte na soubory TypeScriptu normálně (jako u každého souboru musí být `main.ts` vstupním bodem): - -```latte -{asset 'main.ts'} -``` - -Pro plnou podporu TypeScriptu jej nainstalujte: - -```shell -npm install -D typescript -``` - - -Další konfigurace Vite -====================== - -Zde jsou některé užitečné konfigurační možnosti Vite s podrobnými vysvětleními: - -```js -export default defineConfig({ - // Kořenový adresář obsahující zdrojové assety - root: 'assets', - - // Složka, jejíž obsah je kopírován do výstupního adresáře tak, jak je - // Výchozí: 'public' (relativně k 'root') - publicDir: 'public', - - build: { - // Kam umístit zkompilované soubory (relativně k 'root') - outDir: '../www/assets', - - // Vyprázdnit výstupní adresář před sestavením? - // Užitečné pro odstranění starých souborů z předchozích buildů - emptyOutDir: true, - - // Podadresář uvnitř outDir pro generované chunky a assety - // To pomáhá organizovat výstupní strukturu - assetsDir: 'static', - - rollupOptions: { - // Vstupní bod(y) – může být jeden soubor nebo pole souborů - // Každý vstupní bod se stane samostatným balíčkem - input: [ - 'app.js', // hlavní aplikace - 'admin.js', // administrační panel - ], - }, - }, - - server: { - // Hostitel, na který se má dev server navázat - // Použijte '0.0.0.0' pro vystavení do sítě - host: 'localhost', - - // Port pro dev server - port: 5173, - - // Konfigurace CORS pro cross-origin požadavky - cors: { - origin: 'http://myapp.local', - }, - }, - - css: { - // Povolit CSS source mapy ve vývoji - devSourcemap: true, - }, - - plugins: [ - nette(), - ], -}); -``` - -.[warning] -Pozor na cesty ke vstupním bodům v `rollupOptions.input` výše: Rollup resolvuje relativní cesty vůči aktuálnímu pracovnímu adresáři (kořeni projektu), **ne** vůči `root: 'assets'`. Holé `'app.js'` proto při buildu neexistuje. Buď použijte volbu pluginu `entry` (ta řeší cesty relativně ke `root`), nebo cesty pište relativně ke kořeni projektu, např. `'assets/app.js'`. - -To je vše! Nyní máte moderní build systém integrovaný s Nette Assets. - -{{maintitle: Vite v PHP s Nette Assets: HMR a produkční buildy}} -{{description: Prvotřídní integrace Vite pro PHP aplikace: bleskový vývoj s Hot Module Replacement, optimalizované produkční buildy, vstupní body a TypeScript.}} diff --git a/assets/de/@home.texy b/assets/de/@home.texy deleted file mode 100644 index 377c62911d..0000000000 --- a/assets/de/@home.texy +++ /dev/null @@ -1,467 +0,0 @@ -Nette Assets -************ - -
    - -Müde davon, statische Dateien in Ihren Webanwendungen von Hand zu verwalten? Vergessen Sie fest verdrahtete Pfade, das Ungültigmachen des Caches oder Sorgen um die Versionierung von Dateien. Nette Assets verändert grundlegend, wie Sie mit Bildern, Stylesheets, Skripten und anderen statischen Ressourcen arbeiten. - -- **Intelligente Versionierung** sorgt dafür, dass Browser immer die neuesten Dateien laden -- **Automatische Erkennung** von Dateitypen und Abmessungen -- **Nahtlose Latte-Integration** mit intuitiven Tags -- **Flexible Architektur** mit Unterstützung für Dateisysteme, CDNs und Vite -- **Lazy Loading** für optimale Leistung - -
    - - -Warum Nette Assets? -=================== - -Die Arbeit mit statischen Dateien bedeutet oft sich wiederholenden, fehleranfälligen Code. Sie bauen URLs von Hand zusammen, ergänzen Versionsparameter fürs Cache Busting und behandeln verschiedene Dateitypen unterschiedlich. Das führt zu Code wie diesem: - -```latte -Logo - -``` - -Mit Nette Assets verschwindet diese ganze Komplexität: - -```latte -{* Alles automatisch - URL, Versionierung, Abmessungen *} - - - -{* Oder einfach *} -{asset 'css/style.css'} -``` - -Das war's! Die Bibliothek erledigt automatisch: -- Sie ergänzt Versionsparameter anhand der Änderungszeit der Datei -- Sie erkennt die Abmessungen von Bildern und trägt sie ins HTML ein -- Sie erzeugt für jeden Dateityp das richtige HTML-Element -- Sie behandelt sowohl die Entwicklungs- als auch die Produktionsumgebung - - -Installation -============ - -Installieren Sie Nette Assets mit [Composer |best-practices:composer]: - -```shell -composer require nette/assets -``` - -Es benötigt PHP 8.1 oder höher und funktioniert perfekt mit dem Nette Framework, lässt sich aber auch eigenständig verwenden. - - -Erste Schritte -============== - -Nette Assets funktioniert sofort und ohne jede Konfiguration. Legen Sie Ihre statischen Dateien in das Verzeichnis `www/assets/` und verwenden Sie sie: - -```latte -{* Ein Bild mit automatischen Abmessungen anzeigen *} -{asset 'logo.png'} - -{* Ein Stylesheet mit Versionierung einbinden *} -{asset 'style.css'} - -{* Ein Skript laden *} -{asset 'app.js'} -``` - -Für mehr Kontrolle über das erzeugte HTML verwenden Sie das Attribut `n:asset` oder die Funktion `asset()`. - - -Wie es funktioniert -=================== - -Nette Assets baut auf drei Kernkonzepten auf, die es mächtig und zugleich einfach zu verwenden machen: - - -Assets - Ihre Dateien, intelligent gemacht ------------------------------------------- - -Ein **Asset** steht für eine beliebige statische Datei in Ihrer Anwendung. Jede Datei wird zu einem Objekt mit nützlichen Readonly-Properties: - -```php -$image = $assets->getAsset('photo.jpg'); -echo $image->url; // '/assets/photo.jpg?v=1699123456' -echo $image->file; // '/var/www/assets/photo.jpg' (lokaler Pfad oder null) -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' -``` - -Verschiedene Dateitypen bieten verschiedene Properties: -- **Bilder**: Breite, Höhe, Alternativtext, Lazy Loading -- **Skripte**: Modultyp, Integrity-Hashes, crossorigin -- **Stylesheets**: Media Queries, Integrity -- **Audio/Video**: Dauer, Abmessungen (nur Video) -- **Schriften**: korrektes Preloading mit CORS - -Die Bibliothek erkennt die Dateitypen automatisch und erzeugt die passende Asset-Klasse. - - -Mapper - woher die Dateien kommen ---------------------------------- - -Ein **Mapper** weiß, wie man Dateien findet und URLs für sie erzeugt. Sie können mehrere Mapper für verschiedene Zwecke haben - lokale Dateien, CDN, Cloud-Speicher oder Build-Werkzeuge (jeder von ihnen hat einen Namen). Der eingebaute `FilesystemMapper` kümmert sich um lokale Dateien, während `ViteMapper` moderne Build-Werkzeuge einbindet. - -Mapper werden in der [Konfiguration |configuration] definiert. - - -Registry - Ihre Hauptschnittstelle ----------------------------------- - -Die **Registry** verwaltet alle Mapper und stellt die zentrale API bereit: - -```php -// Die Registry in Ihren Service injizieren -public function __construct( - private Nette\Assets\Registry $assets -) {} -``` - -```php -// Assets aus verschiedenen Mappern holen -$logo = $this->assets->getAsset('images:logo.png'); // Mapper 'images' -$app = $this->assets->getAsset('app:main.js'); // Mapper 'app' -$style = $this->assets->getAsset('style.css'); // verwendet den Standard-Mapper -``` - -Die Registry wählt automatisch den richtigen Mapper und cacht die Ergebnisse für die Leistung. - - -Arbeiten mit Assets in PHP -========================== - -Die Registry bietet zwei Methoden, um Assets zu holen: - -```php -// Wirft Nette\Assets\AssetNotFoundException, wenn die Datei nicht existiert -$logo = $assets->getAsset('logo.png'); - -// Gibt null zurück, wenn die Datei nicht existiert -$banner = $assets->tryGetAsset('banner.jpg'); -if ($banner) { - echo $banner->url; -} -``` - - -Mapper angeben --------------- - -Sie können ausdrücklich wählen, welcher Mapper verwendet wird: - -```php -// Standard-Mapper verwenden -$file = $assets->getAsset('document.pdf'); - -// Bestimmten Mapper über ein Präfix verwenden -$image = $assets->getAsset('images:photo.jpg'); - -// Bestimmten Mapper in Array-Schreibweise verwenden -$script = $assets->getAsset(['scripts', 'app.js']); -``` - - -Asset-Eigenschaften und -Typen ------------------------------- - -Jeder Asset-Typ bietet die passenden Readonly-Properties: - -```php -// Eigenschaften eines Bildes -$image = $assets->getAsset('photo.jpg'); -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' - -// Eigenschaften eines Skripts -$script = $assets->getAsset('app.js'); -echo $script->type; // null ('module' bei Vite-Einstiegspunkten) - -// Eigenschaften von Audio -$audio = $assets->getAsset('song.mp3'); -echo $audio->duration; // Dauer in Sekunden - -// Alle Assets lassen sich zu einem String casten (gibt die URL zurück) -$url = (string) $assets->getAsset('document.pdf'); -``` - -.[note] -Eigenschaften wie Abmessungen oder Dauer werden erst beim Zugriff geladen, wodurch die Bibliothek schnell bleibt. - -.[tip] -Für eine genaue statische Analyse installieren Sie die Extension [nette/phpstan-rules |tools:phpstan-rules#Assets]. PHPStan kennt dann den konkreten Typ jedes Assets, sodass `getAsset('photo.jpg')` als `ImageAsset` verstanden wird und der Zugriff auf `->width` keinen Fehler auslöst. - - -Assets in Latte-Templates verwenden -=================================== - -Nette Assets bietet eine intuitive Integration in [Latte |latte:] mit Tags und Funktionen. - - -`{asset}` ---------- - -Der Tag `{asset}` rendert vollständige HTML-Elemente: - -```latte -{* Rendert: *} -{asset 'hero.jpg'} - -{* Rendert: *} -{asset 'app.js'} - -{* Rendert: *} -{asset 'style.css'} -``` - -Der Tag erledigt automatisch: -- Er erkennt den Asset-Typ und erzeugt das passende HTML -- Er ergänzt die Versionierung fürs Cache Busting -- Er ergänzt die Abmessungen von Bildern -- Er setzt die richtigen Attribute (type, media usw.) - -Innerhalb von HTML-Attributen und innerhalb der Elemente ` -``` - - -Eigene Mapper -============= - -Erstellen Sie eigene Mapper für besondere Anforderungen wie Cloud-Speicher oder dynamische Generierung: - -```php -use Nette\Assets\Mapper; -use Nette\Assets\Asset; -use Nette\Assets\Helpers; - -class CloudStorageMapper implements Mapper -{ - public function __construct( - private CloudClient $client, - private string $bucket, - ) {} - - public function getAsset(string $reference, array $options = []): Asset - { - if (!$this->client->exists($this->bucket, $reference)) { - throw new Nette\Assets\AssetNotFoundException("Asset '$reference' not found"); - } - - $url = $this->client->getPublicUrl($this->bucket, $reference); - return Helpers::createAssetFromUrl($url); - } -} -``` - -In der Konfiguration registrieren: - -```neon -assets: - mapping: - cloud: CloudStorageMapper(@cloudClient, 'my-bucket') -``` - -Wie jeden anderen Mapper verwenden: - -```latte -{asset 'cloud:user-uploads/photo.jpg'} -``` - -Die Methode `Helpers::createAssetFromUrl()` erzeugt automatisch den richtigen Asset-Typ anhand der Dateiendung. - -Asset-Typen, die das Interface `Nette\Assets\HtmlRenderable` implementieren (Bilder, Skripte, Styles und so weiter), kann `{asset}` als vollständiges HTML-Element rendern. Andere Dateitypen werden zu einem `GenericAsset` (zum Beispiel ein PDF), das sich nicht als HTML-Element rendern lässt, aber trotzdem eine URL (und weitere Metadaten) bereitstellt. Der Versuch, ein solches Asset als HTML-Element zu rendern, wirft eine `Nette\InvalidArgumentException`; seine URL können Sie aber weiterhin innerhalb eines Attributs verwenden. - - -Weiterführende Lektüre -====================== - -- [Nette Assets: Finally unified API for everything from images to Vite |https://blog.nette.org/en/introducing-nette-assets] diff --git a/assets/de/@left-menu.texy b/assets/de/@left-menu.texy deleted file mode 100644 index 0fa7fb7b55..0000000000 --- a/assets/de/@left-menu.texy +++ /dev/null @@ -1,14 +0,0 @@ -Nette Assets -************ -- [Erste Schritte |@home] -- [Vite |vite] -- [Konfiguration|configuration] - - -Weiterführende Lektüre -********************** -- [Nette Dokumentation |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Best Practices |best-practices:] -- [Fehlerbehebung |nette:troubleshooting] diff --git a/assets/de/@meta.texy b/assets/de/@meta.texy deleted file mode 100644 index b3b806b2ca..0000000000 --- a/assets/de/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokumentation}} diff --git a/assets/de/configuration.texy b/assets/de/configuration.texy deleted file mode 100644 index 242e89183f..0000000000 --- a/assets/de/configuration.texy +++ /dev/null @@ -1,195 +0,0 @@ -Konfiguration von Assets -************************ - -.[perex] -Übersicht der Konfigurationsoptionen für Nette Assets. - - -```neon -assets: - # Basispfad zum Auflösen relativer Pfade der Mapper - basePath: ... # (string) Standardwert ist %wwwDir% - - # Basis-URL zum Auflösen relativer URLs der Mapper - baseUrl: ... # (string) Standardwert ist %baseUrl% - - # Versionierung der Assets global aktivieren? - versioning: ... # (bool) Standardwert ist true - - # definiert die Asset-Mapper - mapping: ... # (array) Standardwert ist der Pfad 'assets' -``` - -`basePath` setzt das Standardverzeichnis im Dateisystem zum Auflösen relativer Pfade in den Mappern. Standardmäßig wird das Web-Verzeichnis verwendet (`%wwwDir%`). - -`baseUrl` setzt das Standard-URL-Präfix zum Auflösen relativer URLs in den Mappern. Standardmäßig wird die Wurzel-URL verwendet (`%baseUrl%`). - -Die Option `versioning` steuert global, ob den URLs der Assets Versionsparameter fürs Cache Busting angehängt werden. Einzelne Mapper können diese Einstellung überschreiben. - - -Mapper ------- - -Mapper lassen sich auf drei Arten konfigurieren: in einfacher String-Schreibweise, in ausführlicher Array-Schreibweise oder als Service (mit der Entity-Schreibweise `ClassName(...)` bzw. `@serviceName()`). - -Der einfachste Weg, einen Mapper zu definieren: - -```neon -assets: - mapping: - default: assets # erzeugt einen Filesystem-Mapper für %wwwDir%/assets/ - images: img # erzeugt einen Filesystem-Mapper für %wwwDir%/img/ - scripts: js # erzeugt einen Filesystem-Mapper für %wwwDir%/js/ -``` - -Jeder Mapper erzeugt einen `FilesystemMapper`, der: -- Dateien in `%wwwDir%/` sucht -- URLs der Form `%baseUrl%/` erzeugt -- die globale Einstellung zur Versionierung erbt - - -Für mehr Kontrolle verwenden Sie die ausführliche Schreibweise: - -```neon -assets: - mapping: - images: - # Verzeichnis, in dem die Dateien liegen - path: ... # (string) optional, Standardwert ist der Basispfad (basePath) - - # URL-Präfix für die erzeugten Links - url: ... # (string) optional, Standardwert ist path - - # Versionierung für diesen Mapper aktivieren? - versioning: ... # (bool) optional, erbt die globale Einstellung - - # Endung(en) beim Suchen von Dateien automatisch ergänzen - extension: ... # (string|array) optional, Standardwert ist null -``` - -So werden die Konfigurationswerte aufgelöst: - -Auflösung der Pfade: - - Relative Pfade werden von `basePath` aus aufgelöst (oder von `%wwwDir%`, wenn `basePath` nicht gesetzt ist) - - Absolute Pfade werden unverändert verwendet - -Auflösung der URLs: - - Relative URLs werden von `baseUrl` aus aufgelöst (oder von `%baseUrl%`, wenn `baseUrl` nicht gesetzt ist) - - Absolute URLs (mit Schema oder `//`) werden unverändert verwendet (eine `//`-URL erhält ihr Schema aus `baseUrl`) - - Ist `url` nicht angegeben, wird der Wert von `path` verwendet - - -```neon -assets: - basePath: /var/www/project/www - baseUrl: https://example.com/assets - - mapping: - # relativer Pfad und relative URL - images: - path: img # aufgelöst zu: /var/www/project/www/img - url: images # aufgelöst zu: https://example.com/assets/images - - # absoluter Pfad und absolute URL - uploads: - path: /var/shared/uploads # unverändert verwendet: /var/shared/uploads - url: https://cdn.example.com # unverändert verwendet: https://cdn.example.com - - # nur der Pfad angegeben - styles: - path: css # Pfad: /var/www/project/www/css - # URL: https://example.com/assets/css -``` - - -Eigene Mapper -------------- - -Bei eigenen Mappern verweisen Sie mit `@serviceName` auf einen bestehenden Service oder definieren ihn direkt über `ClassName(arguments)` bzw. einen bloßen Klassennamen: - -```neon -services: - s3mapper: App\Assets\S3Mapper(%s3.bucket%) - -assets: - mapping: - cloud: @s3mapper - database: App\Assets\DatabaseMapper(@database.connection) -``` - - -Vite-Mapper ------------ - -Beim Vite-Mapper müssen Sie nur `type: vite` ergänzen. Das ist die vollständige Liste der Konfigurationsoptionen: - -```neon -assets: - mapping: - default: - # Typ des Mappers (bei Vite erforderlich) - type: vite # (string) erforderlich, muss 'vite' sein - - # Ausgabeverzeichnis des Vite-Builds - path: ... # (string) optional, Standardwert ist der Basispfad (basePath) - - # URL-Präfix für die gebauten Assets - url: ... # (string) optional, Standardwert ist path - - # Ort der Manifest-Datei von Vite - manifest: ... # (string) optional, relativ zu path, Standardwert ist /.vite/manifest.json - - # Konfiguration des Vite-Dev-Servers - devServer: ... # (bool|string) optional, Standardwert ist true - - # Versionierung für Dateien im Public-Verzeichnis - versioning: ... # (bool) optional, erbt die globale Einstellung - - # automatische Endung für Dateien im Public-Verzeichnis - extension: ... # (string|array) optional, Standardwert ist null -``` - -Die Option `devServer` steuert, wie die Assets während der Entwicklung geladen werden: - -- `true` (Standard) - Erkennt automatisch einen laufenden Vite-Dev-Server (über die Datei `.vite/nette.json`, die das Nette-Vite-Plugin im Build-Verzeichnis anlegt). Läuft der Dev-Server **und ist Ihre Anwendung im Debug-Modus**, werden die Assets von ihm geladen, samt Unterstützung für Hot Module Replacement. Läuft der Dev-Server nicht, werden die Assets aus den gebauten Dateien im öffentlichen Verzeichnis geladen. -- `false` - Schaltet die Integration des Dev-Servers vollständig ab. Die Assets werden immer aus den gebauten Dateien geladen. -- Eigene URL (z. B. `https://localhost:5173`) - Gibt die URL des Dev-Servers samt Protokoll und Port von Hand an. Nützlich, wenn der Dev-Server auf einem anderen Host oder Port läuft. Wie die automatische Erkennung gilt sie nur im Debug-Modus; in der Produktion werden immer die gebauten Dateien verwendet. - -Die Optionen `versioning` und `extension` gelten nur für Dateien im Public-Verzeichnis von Vite, die von Vite nicht verarbeitet werden. - - -Manuelle Konfiguration ----------------------- - -Wenn Sie Nette DI nicht verwenden, konfigurieren Sie die Mapper von Hand: - -```php -use Nette\Assets\Registry; -use Nette\Assets\FilesystemMapper; -use Nette\Assets\ViteMapper; - -$registry = new Registry; - -// Filesystem-Mapper hinzufügen -$registry->addMapper('images', new FilesystemMapper( - baseUrl: 'https://example.com/img', - basePath: __DIR__ . '/www/img', - extensions: ['webp', 'jpg', 'png'], - versioning: true, -)); - -// Vite-Mapper hinzufügen -$registry->addMapper('app', new ViteMapper( - baseUrl: '/build', - basePath: __DIR__ . '/www/build', - manifestPath: __DIR__ . '/www/build/.vite/manifest.json', - devServer: 'https://localhost:5173', -)); -``` - -Jeden registrierten Mapper können Sie außerdem mit der Methode `getMapper()` über seinen Namen holen: - -```php -$mapper = $registry->getMapper('images'); // gibt den registrierten Mapper zurück -$default = $registry->getMapper(); // gibt den Mapper 'default' zurück -``` diff --git a/assets/de/vite.texy b/assets/de/vite.texy deleted file mode 100644 index a19cf40541..0000000000 --- a/assets/de/vite.texy +++ /dev/null @@ -1,567 +0,0 @@ -Vite-Integration -**************** - -
    - -Moderne JavaScript-Anwendungen brauchen ausgereifte Build-Werkzeuge. Nette Assets bietet eine erstklassige Integration von [Vite |https://vite.dev/], dem Frontend-Build-Werkzeug der nächsten Generation. Sie bekommen blitzschnelle Entwicklung mit Hot Module Replacement (HMR) und optimierte Produktions-Builds ohne Konfigurationsaufwand. - -- **Keine Konfiguration nötig** - automatische Brücke zwischen Vite und PHP-Templates -- **Vollständige Verwaltung der Abhängigkeiten** - ein Tag kümmert sich um alle Assets -- **Hot Module Replacement** - sofortige Aktualisierung von JavaScript und CSS -- **Optimierte Produktions-Builds** - Code Splitting und Tree Shaking - -
    - - -Nette Assets bindet Vite nahtlos ein, sodass Sie all diese Vorteile bekommen und Ihre Templates trotzdem wie gewohnt schreiben. - - -Vite einrichten -=============== - -Richten wir Vite Schritt für Schritt ein. Keine Sorge, wenn Build-Werkzeuge für Sie neu sind - wir erklären alles! - - -Schritt 1: Vite installieren ----------------------------- - -Installieren Sie zuerst Vite und das Nette-Plugin in Ihrem Projekt: - -```shell -npm install -D vite @nette/vite-plugin -``` - -Damit werden Vite und ein spezielles Plugin installiert, das Vite hervorragend mit Nette zusammenarbeiten lässt. - - -Schritt 2: Projektstruktur --------------------------- - -Üblich ist es, die Quelldateien der Assets in einem Ordner `assets/` im Wurzelverzeichnis des Projekts abzulegen und die kompilierten Fassungen in `www/assets/`: - -/--pre -web-project/ -├── assets/ ← Quelldateien (SCSS, TypeScript, Quellbilder) -│ ├── public/ ← statische Dateien (unverändert kopiert) -│ │ └── favicon.ico -│ ├── images/ -│ │ └── logo.png -│ ├── app.js ← Haupt-Einstiegspunkt -│ └── style.css ← Ihre Styles -└── www/ ← öffentliches Verzeichnis (Document-Root) - ├── assets/ ← hier landen die kompilierten Dateien - └── index.php -\-- - -Der Ordner `assets/` enthält Ihre Quelldateien - den Code, den Sie schreiben. Vite verarbeitet diese Dateien und legt die kompilierten Fassungen in `www/assets/` ab. - - -Schritt 3: Vite konfigurieren ------------------------------ - -Legen Sie im Wurzelverzeichnis des Projekts eine Datei `vite.config.ts` an. Diese Datei sagt Vite, wo es Ihre Quelldateien findet und wohin es die kompilierten legen soll. - -Das Nette-Vite-Plugin bringt sinnvolle Standardwerte mit, die die Konfiguration einfach machen. Es geht davon aus, dass Ihre Frontend-Quelldateien im Verzeichnis `assets/` liegen (Option `root`) und die kompilierten Dateien nach `www/assets/` gehen (Option `outDir`). Sie müssen nur den [Einstiegspunkt |#Einstiegspunkte] angeben: - -```js -import { defineConfig } from 'vite'; -import nette from '@nette/vite-plugin'; - -export default defineConfig({ - plugins: [ - nette({ - entry: 'app.js', - }), - ], -}); -``` - -Unter der Haube setzt das Plugin außer `root` und `outDir` noch einige weitere Vite-Optionen, damit alles zusammenpasst: `base` auf `''` (die Assets werden direkt vom Document-Root ausgeliefert), `build.manifest` auf `true` (damit Nette Assets die gehashten Dateinamen zuordnen kann) und `build.assetsDir` auf `''` (die kompilierten Dateien landen direkt in `outDir`, ohne einen Unterordner `static/`). Jede davon können Sie überschreiben. - -.[note] -Der Standardwert von `outDir` (`www/assets`) setzt voraus, dass das Verzeichnis `www/` bereits existiert. Tut es das nicht, bricht das Plugin mit dem Fehler *"The output directory ... does not exist"* ab. - -Wenn Sie einen anderen Verzeichnisnamen für den Build Ihrer Assets angeben wollen, müssen Sie einige Optionen ändern: - -```js -export default defineConfig({ - root: 'assets', // Wurzelverzeichnis der Quell-Assets - - build: { - outDir: '../www/assets', // wohin die kompilierten Dateien gehen - }, - - // ... weitere Konfiguration ... -}); -``` - -.[note] -Der Pfad `outDir` gilt als relativ zu `root`, deshalb steht am Anfang `../`. - - -Schritt 4: Nette konfigurieren ------------------------------- - -Teilen Sie Nette Assets in Ihrer `common.neon` von Vite mit: - -```neon -assets: - mapping: - default: - type: vite # sagt Nette, dass es den ViteMapper verwenden soll - path: assets -``` - - -Schritt 5: Skripte hinzufügen ------------------------------ - -Ergänzen Sie diese Skripte in Ihrer `package.json`: - -```json -{ - "scripts": { - "dev": "vite", - "build": "vite build" - } -} -``` - -Nun können Sie: -- `npm run dev` - den Entwicklungsserver mit Hot Reloading starten -- `npm run build` - optimierte Produktionsdateien erzeugen - - -Einstiegspunkte -=============== - -Ein **Einstiegspunkt** ist die Hauptdatei, in der Ihre Anwendung beginnt. Aus dieser Datei importieren Sie weitere Dateien (CSS, JavaScript-Module, Bilder) und erzeugen so einen Abhängigkeitsbaum. Vite folgt diesen Importen und bündelt alles zusammen. - -Beispiel für einen Einstiegspunkt `assets/app.js`: - -```js -// Styles importieren -import './style.css' - -// JavaScript-Module importieren -import netteForms from 'nette-forms'; -import naja from 'naja'; - -// Die Anwendung initialisieren -netteForms.initOnLoad(); -naja.initialize(); -``` - -Im Template können Sie einen Einstiegspunkt so einfügen: - -```latte -{asset 'app.js'} -``` - -Nette Assets erzeugt automatisch alle nötigen HTML-Tags - JavaScript, CSS und alle weiteren Abhängigkeiten. - - -Mehrere Einstiegspunkte ------------------------ - -Größere Anwendungen brauchen oft getrennte Einstiegspunkte: - -```js -export default defineConfig({ - plugins: [ - nette({ - entry: [ - 'app.js', // öffentliche Seiten - 'admin.js', // Adminbereich - ], - }), - ], -}); -``` - -Verwenden Sie sie in verschiedenen Templates: - -```latte -{* Auf den öffentlichen Seiten *} -{asset 'app.js'} - -{* Im Adminbereich *} -{asset 'admin.js'} -``` - - -Wichtig: Quell- vs. kompilierte Dateien ---------------------------------------- - -Es ist entscheidend zu verstehen, dass Sie in der Produktion nur Dateien laden können, die Vite bereitstellt - entweder über sein Manifest oder indem sie unverändert aus dem Public-Ordner kopiert werden: - -1. **Einstiegspunkte**, die in `entry` definiert sind (einschließlich der Module, die sie dynamisch importieren), und **Assets, auf die aus JavaScript oder CSS verwiesen wird** (Bilder, Schriften, ...) - all das wird im Manifest festgehalten -2. **Dateien aus dem Verzeichnis `assets/public/`** - diese stehen *nicht* im Manifest; sie werden unverändert kopiert, und `{asset}` findet sie über einen Fallback im Dateisystem - -Sie können mit `{asset}` **keine** beliebigen Dateien aus `assets/` laden - wird eine Datei nirgends referenziert, wird sie nicht kompiliert. Wenn Sie Vite weitere Assets bekannt machen wollen, können Sie sie in den [Public-Ordner |#Public-Ordner] verschieben. - -Beachten Sie, dass Vite standardmäßig alle Assets kleiner als 4 KB inline einbettet, sodass Sie diese Dateien nicht direkt referenzieren können. (Siehe die [Vite-Dokumentation |https://vite.dev/guide/assets.html]). - -```latte -{* ✓ Das funktioniert - es ist ein Einstiegspunkt *} -{asset 'app.js'} - -{* ✓ Das funktioniert - es liegt in assets/public/ *} -{asset 'favicon.ico'} - -{* ✗ Das funktioniert nicht - eine beliebige Datei in assets/ *} -{asset 'components/button.js'} -``` - - -Entwicklungsmodus -================= - -Der Entwicklungsmodus ist völlig optional, bringt aber deutliche Vorteile, wenn er aktiviert ist. Der Hauptvorteil ist **Hot Module Replacement (HMR)** - Sie sehen Änderungen sofort, ohne den Zustand der Anwendung zu verlieren, was die Entwicklung deutlich flüssiger und schneller macht. - -Vite ist ein modernes Build-Werkzeug, das die Entwicklung unglaublich schnell macht. Anders als klassische Bundler liefert Vite Ihren Code während der Entwicklung direkt an den Browser aus, was einen sofortigen Serverstart unabhängig von der Größe Ihres Projekts und blitzschnelle Aktualisierungen bedeutet. - - -Entwicklungsserver starten --------------------------- - -Starten Sie den Entwicklungsserver: - -```shell -npm run dev -``` - -Sie sehen: - -``` - ➜ Local: http://localhost:5173/ - ➜ Network: use --host to expose -``` - -Lassen Sie dieses Terminal während der Entwicklung offen. - -Solange der Dev-Server läuft, schreibt das Plugin eine kleine Signaldatei `www/assets/.vite/nette.json` mit seiner URL. Nette Assets liest diese Datei auf der PHP-Seite und schaltet auf das Laden vom Dev-Server um, wenn beides zutrifft: -1. der Vite-Dev-Server läuft (die Signaldatei existiert) und -2. Ihre Nette-Anwendung ist im Debug-Modus. - -Das Ergebnis: - -```latte -{asset 'app.js'} -{* In der Entwicklung: - *} -{* In der Produktion: *} -``` - -Keine Konfiguration nötig - es funktioniert einfach! Um die Erkennung abzuschalten oder die URL des Dev-Servers von Hand zu setzen, siehe die [Option devServer |configuration#Vite-Mapper]. - -.[note] -Die Signaldatei liegt unter `www/assets/.vite/nette.json` (direkt neben der `manifest.json` der Produktion). Sie können sie mit der Plugin-Option `infoFile` umbenennen, deren Standardwert `.vite/nette.json` ist. Erkennt Nette den laufenden Dev-Server nicht, prüfen Sie, ob diese Datei existiert und auf die richtige URL zeigt. - - -Arbeiten auf verschiedenen Domains ----------------------------------- - -Läuft Ihr Entwicklungsserver auf etwas anderem als `localhost` (etwa `myapp.local`), können Probleme mit CORS (Cross-Origin Resource Sharing) auftreten. CORS ist eine Sicherheitsfunktion in Webbrowsern, die Requests zwischen verschiedenen Domains standardmäßig blockiert. Läuft Ihre PHP-Anwendung auf `myapp.local`, Vite aber auf `localhost:5173`, sieht der Browser das als verschiedene Domains und blockiert die Requests. - -Sie haben zwei Möglichkeiten, das zu lösen: - -**Möglichkeit 1: CORS konfigurieren** - -Am einfachsten erlauben Sie Cross-Origin-Requests aus Ihrer PHP-Anwendung: - -```js -export default defineConfig({ - // ... weitere Konfiguration ... - - server: { - cors: { - origin: 'http://myapp.local', // die URL Ihrer PHP-Anwendung - }, - }, -}); -``` - -**Möglichkeit 2: Vite auf Ihrer Domain laufen lassen** - -Die andere Lösung ist, Vite auf derselben Domain laufen zu lassen wie Ihre PHP-Anwendung. - -```js -export default defineConfig({ - // ... weitere Konfiguration ... - - server: { - host: 'myapp.local', // dieselbe wie bei Ihrer PHP-Anwendung - }, -}); -``` - -Tatsächlich müssen Sie auch in diesem Fall CORS konfigurieren, weil der Dev-Server unter demselben Hostnamen, aber auf einem anderen Port läuft. In diesem Fall konfiguriert das Nette-Vite-Plugin CORS jedoch automatisch. - - -HTTPS-Entwicklung ------------------ - -Wenn Sie über HTTPS entwickeln, brauchen Sie Zertifikate für Ihren Vite-Entwicklungsserver. Am einfachsten ist ein Plugin, das die Zertifikate automatisch erzeugt: - -```shell -npm install -D vite-plugin-mkcert -``` - -So konfigurieren Sie es in `vite.config.ts`: - -```js -import mkcert from 'vite-plugin-mkcert'; - -export default defineConfig({ - // ... weitere Konfiguration ... - - plugins: [ - mkcert(), // erzeugt die Zertifikate automatisch und aktiviert https - nette(), - ], -}); -``` - -Beachten Sie, dass Sie bei Verwendung der CORS-Konfiguration (Möglichkeit 1 von oben) die Origin-URL auf `https://` statt `http://` anpassen müssen. - - -Entwicklung mit Docker ----------------------- - -Wenn Sie Vite in einem Docker-Container laufen lassen, brauchen zwei Dinge Aufmerksamkeit: Der Browser auf Ihrem Rechner muss den Dev-Server erreichen können, und Vite muss Dateiänderungen über die Containergrenze hinweg erkennen. - -[Veröffentlichen |https://docs.docker.com/get-started/docker-concepts/running-containers/publishing-ports/] Sie zuerst den Vite-Port aus dem Container und binden Sie den Dev-Server an alle Interfaces, damit er von außerhalb des Containers erreichbar ist: - -```js -export default defineConfig({ - // ... weitere Konfiguration ... - - plugins: [ - nette(), - ], - server: { - host: '0.0.0.0', // auf allen Interfaces lauschen (im Container nötig) - port: 5173, // muss zum veröffentlichten Port passen - strictPort: true, // lieber fehlschlagen als einen anderen Port wählen - watch: { - usePolling: true, // aktivieren, wenn Dateiänderungen auf gemounteten Volumes nicht erkannt werden - }, - }, -}); -``` - -Das Plugin schreibt die URL des Dev-Servers für die PHP-Seite in die `nette.json`. Weil `host: '0.0.0.0'` für einen Browser nicht nutzbar ist (er leitet bei jedem Asset auf `localhost` weiter), schreibt das Plugin sie in dieser URL automatisch auf `localhost` um, damit die Assets korrekt geladen werden. - -Wenn Sie die Anwendung statt unter `localhost` unter einer eigenen Domain öffnen, setzen Sie die Plugin-Option `host` auf diese Domain: - -```js - plugins: [ - nette({ host: 'myapp.local' }), // dieselbe Domain wie Ihre PHP-Anwendung - ], -``` - -Das Plugin verwendet diesen Host dann für die URL des Dev-Servers, ergänzt ihn bei den CORS-Origins und trägt ihn in die `allowedHosts` von Vite ein - so funktioniert es ohne jede manuelle CORS-Einrichtung. - -Für komplexere Aufbauten - etwa wenn Vite hinter einem Reverse Proxy läuft, bei dem sich öffentlicher Host, Port und Protokoll allesamt von der internen Adresse unterscheiden - setzen Sie Vites `server.origin` auf die vollständige öffentliche URL. Das Plugin berücksichtigt sie und schreibt sie unverändert in die `nette.json`, statt die URL aus dem lokalen Socket abzuleiten: - -```js - server: { - origin: 'https://myapp.local:8443', // die öffentliche URL, unter der der Browser Vite erreicht - }, -``` - - -Produktions-Builds -================== - -Erzeugen Sie optimierte Produktionsdateien: - -```shell -npm run build -``` - -Vite wird: -- das gesamte JavaScript und CSS minifizieren -- den Code in optimale Chunks aufteilen -- gehashte Dateinamen fürs Cache Busting erzeugen -- eine Manifest-Datei für Nette Assets anlegen - -Beispielausgabe: - -``` -www/assets/ -├── app-4f3a2b1c.js # Ihr Haupt-JavaScript (minifiziert) -├── app-7d8e9f2a.css # extrahiertes CSS (minifiziert) -├── vendor-8c4b5e6d.js # gemeinsame Abhängigkeiten -└── .vite/ - └── manifest.json # Zuordnung für Nette Assets -``` - -Die gehashten Dateinamen sorgen dafür, dass Browser immer die neueste Version laden. - - -Public-Ordner -============= - -Dateien im Verzeichnis `assets/public/` werden ohne Verarbeitung in die Ausgabe kopiert: - -``` -assets/ -├── public/ -│ ├── favicon.ico -│ ├── robots.txt -│ └── images/ -│ └── og-image.jpg -├── app.js -└── style.css -``` - -Referenzieren Sie sie ganz normal: - -```latte -{* Diese Dateien werden unverändert kopiert *} - - -``` - -Für öffentliche Dateien können Sie die Möglichkeiten des FilesystemMapper nutzen. Die Option `extension` gilt für Referenzen ohne Endung (z. B. `{asset 'images/og-image'}` findet zuerst `og-image.webp`): - -```neon -assets: - mapping: - default: - type: vite - path: assets - extension: [webp, jpg, png] # für Referenzen ohne Endung - versioning: true # Cache Busting ergänzen -``` - -In der Konfiguration `vite.config.ts` können Sie den Public-Ordner über die Option `publicDir` ändern. - - -Dynamische Importe -================== - -Vite teilt den Code automatisch für optimales Laden auf. Dynamische Importe erlauben es, Code erst dann zu laden, wenn er wirklich gebraucht wird, was die Größe des anfänglichen Bundles verringert: - -```js -// Schwere Komponenten bei Bedarf laden -button.addEventListener('click', async () => { - let { Chart } = await import('./components/chart.js') - new Chart(data) -}) -``` - -Dynamische Importe erzeugen eigene Chunks, die nur bei Bedarf geladen werden. Das nennt man "Code Splitting" und es ist eine der stärksten Funktionen von Vite. Wenn Sie dynamische Importe verwenden, erzeugt Vite für jedes dynamisch importierte Modul automatisch eine eigene JavaScript-Datei. - -Der Tag `{asset 'app.js'}` lädt diese dynamischen Chunks **nicht** automatisch vor. Das ist beabsichtigt - wir wollen keinen Code herunterladen, der vielleicht nie verwendet wird. Die Chunks werden erst heruntergeladen, wenn der dynamische Import ausgeführt wird. - -Wenn Sie jedoch wissen, dass bestimmte dynamische Importe entscheidend sind und bald gebraucht werden, können Sie sie vorladen: - -```latte -{* Haupt-Einstiegspunkt *} -{asset 'app.js'} - -{* Kritische dynamische Importe vorladen *} -{preload 'components/chart.js'} -``` - -Das sagt dem Browser, dass er die Chart-Komponente im Hintergrund herunterladen soll, damit sie bei Bedarf sofort bereitsteht. - - -TypeScript-Unterstützung -======================== - -TypeScript funktioniert sofort: - -```ts -// assets/main.ts -interface User { - name: string - email: string -} - -export function greetUser(user: User): void { - console.log(`Hello, ${user.name}!`) -} -``` - -Referenzieren Sie TypeScript-Dateien ganz normal (wie bei jeder Datei muss `main.ts` ein Einstiegspunkt sein): - -```latte -{asset 'main.ts'} -``` - -Für die volle TypeScript-Unterstützung installieren Sie es: - -```shell -npm install -D typescript -``` - - -Zusätzliche Vite-Konfiguration -============================== - -Hier einige nützliche Konfigurationsoptionen von Vite mit ausführlichen Erklärungen: - -```js -export default defineConfig({ - // Wurzelverzeichnis mit den Quell-Assets - root: 'assets', - - // Ordner, dessen Inhalt unverändert in das Ausgabeverzeichnis kopiert wird - // Standard: 'public' (relativ zu 'root') - publicDir: 'public', - - build: { - // Wohin die kompilierten Dateien kommen (relativ zu 'root') - outDir: '../www/assets', - - // Ausgabeverzeichnis vor dem Build leeren? - // Nützlich, um alte Dateien früherer Builds zu entfernen - emptyOutDir: true, - - // Unterverzeichnis innerhalb von outDir für erzeugte Chunks und Assets - // Das hilft, die Struktur der Ausgabe zu ordnen - assetsDir: 'static', - - rollupOptions: { - // Einstiegspunkt(e) - eine einzelne Datei oder ein Array von Dateien - // Jeder Einstiegspunkt wird ein eigenes Bundle - input: [ - 'app.js', // Hauptanwendung - 'admin.js', // Adminbereich - ], - }, - }, - - server: { - // Host, an den der Dev-Server gebunden wird - // '0.0.0.0' verwenden, um ihn im Netzwerk verfügbar zu machen - host: 'localhost', - - // Port für den Dev-Server - port: 5173, - - // CORS-Konfiguration für Cross-Origin-Requests - cors: { - origin: 'http://myapp.local', - }, - }, - - css: { - // CSS-Sourcemaps in der Entwicklung aktivieren - devSourcemap: true, - }, - - plugins: [ - nette(), - ], -}); -``` - -.[warning] -Achten Sie auf die Pfade der Einstiegspunkte in `rollupOptions.input` oben: Rollup löst relative Pfade gegen das aktuelle Arbeitsverzeichnis auf (das Wurzelverzeichnis Ihres Projekts), **nicht** gegen `root: 'assets'`. Ein bloßes `'app.js'` existiert zur Build-Zeit also nicht. Verwenden Sie entweder die Plugin-Option `entry` (die die Pfade der Einstiegspunkte relativ zu `root` auflöst) oder schreiben Sie die Pfade relativ zum Wurzelverzeichnis des Projekts, also z. B. `'assets/app.js'`. - -Das war's! Sie haben nun ein modernes Build-System, das mit Nette Assets zusammenarbeitet. diff --git a/assets/en/@home.texy b/assets/en/@home.texy deleted file mode 100644 index e7bffb6cb0..0000000000 --- a/assets/en/@home.texy +++ /dev/null @@ -1,478 +0,0 @@ -Nette Assets -************ - -
    - -Tired of manually managing static files in your web applications? Forget about hardcoding paths, dealing with cache invalidation, or worrying about file versioning. Nette Assets transforms how you work with images, stylesheets, scripts, and other static resources. - -- **Smart versioning** ensures browsers always load the latest files -- **Automatic detection** of file types and dimensions -- **Seamless Latte integration** with intuitive tags -- **Flexible architecture** supporting filesystems, CDNs, and Vite -- **Lazy loading** for optimal performance - -
    - - -Why Nette Assets? -================= - -Working with static files often means repetitive, error-prone code. You manually construct URLs, add version parameters for cache busting, and handle different file types differently. This leads to code like: - -```latte -Logo - -``` - -With Nette Assets, all this complexity disappears: - -```latte -{* Everything automated - URL, versioning, dimensions *} - - - -{* Or just *} -{asset 'css/style.css'} -``` - -That's it! The library automatically: -- Adds version parameters based on file modification time -- Detects image dimensions and includes them in the HTML -- Generates the correct HTML element for each file type -- Handles both development and production environments - - -Installation -============ - -Install Nette Assets using [Composer|best-practices:composer]: - -```shell -composer require nette/assets -``` - -It requires PHP 8.1 or higher and works perfectly with Nette Framework, but can also be used standalone. - - -First Steps -=========== - -Nette Assets works out of the box with zero configuration. Place your static files in the `www/assets/` directory and start using them: - -```latte -{* Display an image with automatic dimensions *} -{asset 'logo.png'} - -{* Include a stylesheet with versioning *} -{asset 'style.css'} - -{* Load a script *} -{asset 'app.js'} -``` - -For more control over the generated HTML, use the `n:asset` attribute or the `asset()` function. - - -How It Works -============ - -Nette Assets is built around three core concepts that make it powerful yet simple to use: - - -Assets - Your Files Made Smart ------------------------------- - -An **asset** represents any static file in your application. Each file becomes an object with useful readonly properties: - -```php -$image = $assets->getAsset('photo.jpg'); -echo $image->url; // '/assets/photo.jpg?v=1699123456' -echo $image->file; // '/var/www/assets/photo.jpg' (local path, or null) -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' -``` - -Different file types provide different properties: -- **Images**: width, height, alternative text, lazy loading -- **Scripts**: module type, integrity hashes, crossorigin -- **Stylesheets**: media queries, integrity -- **Audio/Video**: duration, dimensions (video only) -- **Fonts**: proper preloading with CORS - -The library automatically detects file types and creates the appropriate asset class. - - -Mappers - Where Files Come From -------------------------------- - -A **mapper** knows how to find files and create URLs for them. You can have multiple mappers for different purposes - local files, CDN, cloud storage, or build tools (each of them has a name). The built-in `FilesystemMapper` handles local files, while `ViteMapper` integrates with modern build tools. - -Mappers are defined in the [configuration]. - - -Registry - Your Main Interface ------------------------------- - -The **registry** manages all mappers and provides the main API: - -```php -// Inject the registry in your service -public function __construct( - private Nette\Assets\Registry $assets -) {} -``` - -```php -// Get assets from different mappers -$logo = $this->assets->getAsset('images:logo.png'); // 'images' mapper -$app = $this->assets->getAsset('app:main.js'); // 'app' mapper -$style = $this->assets->getAsset('style.css'); // uses default mapper -``` - -The registry automatically selects the right mapper and caches results for performance. - - -Working with Assets in PHP -========================== - -The Registry provides two methods for retrieving assets: - -```php -// Throws Nette\Assets\AssetNotFoundException if file doesn't exist -$logo = $assets->getAsset('logo.png'); - -// Returns null if file doesn't exist -$banner = $assets->tryGetAsset('banner.jpg'); -if ($banner) { - echo $banner->url; -} -``` - - -Specifying Mappers ------------------- - -You can explicitly choose which mapper to use: - -```php -// Use default mapper -$file = $assets->getAsset('document.pdf'); - -// Use specific mapper with prefix -$image = $assets->getAsset('images:photo.jpg'); - -// Use specific mapper with array syntax -$script = $assets->getAsset(['scripts', 'app.js']); -``` - - -Asset Properties and Types --------------------------- - -Each asset type provides relevant readonly properties: - -```php -// Image properties -$image = $assets->getAsset('photo.jpg'); -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' - -// Script properties -$script = $assets->getAsset('app.js'); -echo $script->type; // null ('module' for Vite entry points) - -// Audio properties -$audio = $assets->getAsset('song.mp3'); -echo $audio->duration; // duration in seconds - -// All assets can be cast to string (returns URL) -$url = (string) $assets->getAsset('document.pdf'); -``` - -.[note] -Properties like dimensions or duration are loaded lazily only when accessed, keeping the library fast. - -.[tip] -For precise static analysis, install the [nette/phpstan-rules |tools:phpstan-rules#Assets] extension. PHPStan then knows the concrete type of each asset, so `getAsset('photo.jpg')` is understood as `ImageAsset` and accessing `->width` raises no error. - - -Using Assets in Latte Templates -=============================== - -Nette Assets provides intuitive [Latte|latte:] integration with tags and functions. - - -`{asset}` ---------- - -The `{asset}` tag renders complete HTML elements: - -```latte -{* Renders: *} -{asset 'hero.jpg'} - -{* Renders: *} -{asset 'app.js'} - -{* Renders: *} -{asset 'style.css'} -``` - -The tag automatically: -- Detects asset type and generates appropriate HTML -- Includes versioning for cache busting -- Adds dimensions for images -- Sets correct attributes (type, media, etc.) - -When used inside HTML attributes or inside ` -``` - - -Custom Mappers -============== - -Create custom mappers for special needs like cloud storage or dynamic generation: - -```php -use Nette\Assets\Mapper; -use Nette\Assets\Asset; -use Nette\Assets\Helpers; - -class CloudStorageMapper implements Mapper -{ - public function __construct( - private CloudClient $client, - private string $bucket, - ) {} - - public function getAsset(string $reference, array $options = []): Asset - { - if (!$this->client->exists($this->bucket, $reference)) { - throw new Nette\Assets\AssetNotFoundException("Asset '$reference' not found"); - } - - $url = $this->client->getPublicUrl($this->bucket, $reference); - return Helpers::createAssetFromUrl($url); - } -} -``` - -Register in configuration: - -```neon -assets: - mapping: - cloud: CloudStorageMapper(@cloudClient, 'my-bucket') -``` - -Use like any other mapper: - -```latte -{asset 'cloud:user-uploads/photo.jpg'} -``` - -The `Helpers::createAssetFromUrl()` method automatically creates the correct asset type based on the file extension. - -Asset types that implement the `Nette\Assets\HtmlRenderable` interface (images, scripts, styles, and so on) can be rendered by `{asset}` as a complete HTML element. Other file types become a `GenericAsset` (for example a PDF), which cannot be rendered as an HTML element but still provides a URL (and other metadata). Trying to render such an asset as an HTML element throws `Nette\InvalidArgumentException`; you can still use its URL inside an attribute. - - -Further Reading -=============== - -- [Nette Assets: Finally unified API for everything from images to Vite |https://blog.nette.org/en/introducing-nette-assets] - -{{maintitle: Nette Assets: Manage Static Files in PHP Applications}} -{{description: Stop hardcoding paths and worrying about cache invalidation. Nette Assets manages images, styles and scripts in your PHP app, in code and in Latte templates.}} diff --git a/assets/en/@left-menu.texy b/assets/en/@left-menu.texy deleted file mode 100644 index e7a8686a05..0000000000 --- a/assets/en/@left-menu.texy +++ /dev/null @@ -1,14 +0,0 @@ -Nette Assets -************ -- [Getting Started |@home] -- [Vite |vite] -- [Configuration] - - -Further Reading -*************** -- [Nette Documentation |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Best practices |best-practices:] -- [Troubleshooting |nette:troubleshooting] diff --git a/assets/en/@meta.texy b/assets/en/@meta.texy deleted file mode 100644 index 42471908b0..0000000000 --- a/assets/en/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Documentation}} diff --git a/assets/en/configuration.texy b/assets/en/configuration.texy deleted file mode 100644 index b18b3586f7..0000000000 --- a/assets/en/configuration.texy +++ /dev/null @@ -1,199 +0,0 @@ -Assets Configuration -******************** - -.[perex] -Overview of configuration options for Nette Assets. - - -```neon -assets: - # base path for resolving relative mapper paths - basePath: ... # (string) defaults to %wwwDir% - - # base URL for resolving relative mapper URLs - baseUrl: ... # (string) defaults to %baseUrl% - - # enable asset versioning globally? - versioning: ... # (bool) defaults to true - - # defines asset mappers - mapping: ... # (array) defaults to path 'assets' -``` - -The `basePath` sets the default filesystem directory for resolving relative paths in mappers. By default, it uses the web directory (`%wwwDir%`). - -The `baseUrl` sets the default URL prefix for resolving relative URLs in mappers. By default, it uses the root URL (`%baseUrl%`). - -The `versioning` option globally controls whether version parameters are added to asset URLs for cache busting. Individual mappers can override this setting. - - -Mappers -------- - -Mappers can be configured in three ways: simple string notation, detailed array notation, or as a service (using entity notation `ClassName(...)` or a reference `@serviceName`). - -The simplest way to define a mapper: - -```neon -assets: - mapping: - default: assets # Creates filesystem mapper for %wwwDir%/assets/ - images: img # Creates filesystem mapper for %wwwDir%/img/ - scripts: js # Creates filesystem mapper for %wwwDir%/js/ -``` - -Each mapper creates a `FilesystemMapper` that: -- Looks for files in `%wwwDir%/` -- Generates URLs like `%baseUrl%/` -- Inherits global versioning setting -- Finds only files inside this directory: a reference like `../secret.txt` throws `Nette\Assets\AssetNotFoundException` .{data-version:1.0.8} - - -For more control, use the detailed notation: - -```neon -assets: - mapping: - images: - # directory where files are stored - path: ... # (string) optional, defaults to base path (basePath) - - # URL prefix for generated links - url: ... # (string) optional, defaults to path - - # enable versioning for this mapper? - versioning: ... # (bool) optional, inherits global setting - - # auto-add extension(s) when searching for files - extension: ... # (string|array) optional, defaults to null -``` - -Understanding how configuration values are resolved: - -Path Resolution: - - Relative paths are resolved from `basePath` (or `%wwwDir%` if `basePath` is not set) - - Absolute paths are used as-is - -URL Resolution: - - Relative URLs are resolved from `baseUrl` (or `%baseUrl%` if `baseUrl` is not set) - - Absolute URLs (with scheme or `//`) are used as-is (a `//` URL gets its scheme from `baseUrl`) - - If `url` is not specified, it uses the value of `path` - - -```neon -assets: - basePath: /var/www/project/www - baseUrl: https://example.com/assets - - mapping: - # Relative path and URL - images: - path: img # Resolved to: /var/www/project/www/img - url: images # Resolved to: https://example.com/assets/images - - # Absolute path and URL - uploads: - path: /var/shared/uploads # Used as-is: /var/shared/uploads - url: https://cdn.example.com # Used as-is: https://cdn.example.com - - # Only path specified - styles: - path: css # Path: /var/www/project/www/css - # URL: https://example.com/assets/css -``` - - -Custom Mappers --------------- - -For custom mappers, reference an existing service with `@serviceName`, or define it directly using `ClassName(arguments)` or a bare class name: - -```neon -services: - s3mapper: App\Assets\S3Mapper(%s3.bucket%) - -assets: - mapping: - cloud: @s3mapper # reference to a service (since 1.0.8) - database: App\Assets\DatabaseMapper(@database.connection) -``` - - -Vite Mapper ------------ - -The Vite mapper only requires you to add `type: vite`. This is a complete list of configuration options: - -```neon -assets: - mapping: - default: - # mapper type (required for Vite) - type: vite # (string) required, must be 'vite' - - # Vite build output directory - path: ... # (string) optional, defaults to base path (basePath) - - # URL prefix for built assets - url: ... # (string) optional, defaults to path - - # location of Vite manifest file - manifest: ... # (string) optional, relative to path, defaults to /.vite/manifest.json - - # Vite dev server configuration - devServer: ... # (bool|string) optional, defaults to true - - # versioning for public directory files - versioning: ... # (bool) optional, inherits global setting - - # auto-extension for public directory files - extension: ... # (string|array) optional, defaults to null -``` - -The `devServer` option controls how assets are loaded during development: - -- `true` (default) - Automatically detects a running Vite dev server (via the `.vite/nette.json` file which the Nette Vite plugin creates in the build directory). If the dev server is running **and your application is in debug mode**, assets are loaded from it with hot module replacement support. If the dev server is not running, assets are loaded from the built files in the public directory. -- `false` - Completely disables the dev server integration. Assets are always loaded from the built files. -- Custom URL (e.g., `https://localhost:5173`) - Manually specify the dev server URL including protocol and port. Useful when the dev server runs on a different host or port. Like auto-detection, it only applies in debug mode; in production the built files are always used. - -The `versioning` and `extension` options apply only to files in Vite's public directory that aren't processed by Vite. - - -Manual Configuration --------------------- - -When not using Nette DI, configure mappers manually: - -```php -use Nette\Assets\Registry; -use Nette\Assets\FilesystemMapper; -use Nette\Assets\ViteMapper; - -$registry = new Registry; - -// Add filesystem mapper -$registry->addMapper('images', new FilesystemMapper( - baseUrl: 'https://example.com/img', - basePath: __DIR__ . '/www/img', - extensions: ['webp', 'jpg', 'png'], - versioning: true, -)); - -// Add Vite mapper -$registry->addMapper('app', new ViteMapper( - baseUrl: '/build', - basePath: __DIR__ . '/www/build', - manifestPath: __DIR__ . '/www/build/.vite/manifest.json', - devServer: 'https://localhost:5173', -)); -``` - -You can also retrieve any registered mapper by name using the `getMapper()` method: - -```php -$mapper = $registry->getMapper('images'); // returns the registered mapper -$default = $registry->getMapper(); // returns the 'default' mapper -``` - -{{maintitle: Nette Assets Configuration: Mappers, Paths and Versioning}} -{{description: Configure Nette Assets in NEON: the base path and URL, global versioning and the mappers that say where your static files live, including Vite.}} diff --git a/assets/en/vite.texy b/assets/en/vite.texy deleted file mode 100644 index 2508b8d4a7..0000000000 --- a/assets/en/vite.texy +++ /dev/null @@ -1,624 +0,0 @@ -Vite Integration -**************** - -
    - -Modern JavaScript applications require sophisticated build tools. Nette Assets provides first-class integration with [Vite |https://vite.dev/], the next-generation frontend build tool. Get lightning-fast development with Hot Module Replacement (HMR) and optimized production builds with zero configuration hassle. - -- **Zero configuration** - automatic bridge between Vite and PHP templates -- **Complete dependency management** - one tag handles all assets -- **Hot Module Replacement** - instant JavaScript and CSS updates -- **Optimized production builds** - code splitting and tree shaking - -
    - - -Nette Assets integrates seamlessly with Vite, so you get all these benefits while writing your templates as usual. - - -Setting Up Vite -=============== - -Let's set up Vite step by step. Don't worry if you're new to build tools - we'll explain everything! - - -Step 1: Install Vite --------------------- - -First, install Vite and the Nette plugin in your project: - -```shell -npm install -D vite @nette/vite-plugin -``` - -This installs Vite and a special plugin that helps Vite work perfectly with Nette. - - -Step 2: Project Structure -------------------------- - -The standard approach is to place source asset files in an `assets/` folder in your project root, and the compiled versions in `www/assets/`: - -/--pre -web-project/ -├── assets/ ← source files (SCSS, TypeScript, source images) -│ ├── public/ ← static files (copied as-is) -│ │ └── favicon.ico -│ ├── images/ -│ │ └── logo.png -│ ├── app.js ← main entry point -│ └── style.css ← your styles -└── www/ ← public directory (document root) - ├── assets/ ← compiled files will go here - └── index.php -\-- - -The `assets/` folder contains your source files - the code you write. Vite will process these files and put the compiled versions in `www/assets/`. - - -Step 3: Configure Vite ----------------------- - -Create a `vite.config.ts` file in your project root. This file tells Vite where to find your source files and where to put the compiled ones. - -The Nette Vite plugin comes with smart defaults that make configuration simple. It assumes your front-end source files are in the `assets/` directory (option `root`) and compiled files go to `www/assets/` (option `outDir`). You only need to specify the [entry point|#Entry Points]: - -```js -import { defineConfig } from 'vite'; -import nette from '@nette/vite-plugin'; - -export default defineConfig({ - plugins: [ - nette({ - entry: 'app.js', - }), - ], -}); -``` - -Under the hood, besides `root` and `outDir` the plugin sets a few more Vite options so everything fits together: `base` to `''` (assets are served straight from the document root), `build.manifest` to `true` (so Nette Assets can map the hashed filenames), and `build.assetsDir` to `''` (compiled files land directly in `outDir`, without a `static/` subfolder). You can override any of them. - -.[note] -The default `outDir` (`www/assets`) requires the `www/` directory to already exist. If it doesn't, the plugin stops with the error *"The output directory ... does not exist"*. - -If you want to specify another directory name to build your assets, you will need to change a few options: - -```js -export default defineConfig({ - root: 'assets', // root directory of source assets - - build: { - outDir: '../www/assets', // where compiled files go - }, - - // ... other config ... -}); -``` - -.[note] -The `outDir` path is considered relative to `root`, which is why there's `../` at the beginning. - - -Step 4: Configure Nette ------------------------ - -Tell Nette Assets about Vite in your `common.neon`: - -```neon -assets: - mapping: - default: - type: vite # tells Nette to use the ViteMapper - path: assets -``` - - -Step 5: Add scripts -------------------- - -Add these scripts to your `package.json`: - -```json -{ - "scripts": { - "dev": "vite", - "build": "vite build" - } -} -``` - -Now you can: -- `npm run dev` - start development server with hot reloading -- `npm run build` - create optimized production files - - -Entry Points -============ - -An **entry point** is the main file where your application starts. From this file, you import other files (CSS, JavaScript modules, images), creating a dependency tree. Vite follows these imports and bundles everything together. - -Example entry point `assets/app.js`: - -```js -// Import styles -import './style.css' - -// Import JavaScript modules -import netteForms from 'nette-forms'; -import naja from 'naja'; - -// Initialize your application -netteForms.initOnLoad(); -naja.initialize(); -``` - -In the template you can insert an entry point as follows: - -```latte -{asset 'app.js'} -``` - -Nette Assets automatically generates all necessary HTML tags - JavaScript, CSS, and any other dependencies. - -.[note]{data-version:1.0.8} -For entry points, use the `{asset}` tag, not the `n:asset` attribute. An attribute can fill in only the ` - *} -{* In production: - - *} -``` - -No configuration needed - it just works! To disable the detection or set the dev server URL manually, see the [devServer option |configuration#Vite Mapper]. - -.[note] -The signal file lives at `www/assets/.vite/nette.json` (right next to the production `manifest.json`). You can rename it with the plugin's `infoFile` option, which defaults to `.vite/nette.json`. If Nette doesn't pick up the running dev server, check that this file exists and points to the right URL. - - -Auto-Reloading on Template Changes ----------------------------------- - -Vite's HMR only watches files in its own module graph - your JavaScript, CSS, and what they import. Latte templates and PHP files are outside it, so editing them normally does nothing in the browser until you reload by hand. - -The `refresh` option fixes that. Give it glob patterns; whenever a matching file changes, the plugin triggers a full page reload: - -```js -nette({ - entry: 'app.js', - refresh: 'app/**/*.latte', // reload on any Latte template change -}), -``` - -You can also pass an array of patterns. They are matched against the whole project, not just `root`. - - -Working on Different Domains ----------------------------- - -If your development server runs on something other than `localhost` (like `myapp.local`), you might encounter CORS (Cross-Origin Resource Sharing) issues. CORS is a security feature in web browsers that blocks requests between different domains by default. When your PHP application runs on `myapp.local` but Vite runs on `localhost:5173`, the browser sees these as different domains and blocks the requests. - -The cleanest fix is to tell the plugin where your PHP application lives via the `appUrl` option. The plugin then allows that origin (over http or https, on any port) in the dev server's CORS, so the browser never blocks the requests: - -```js -nette({ - entry: 'app.js', - appUrl: 'https://myapp.local', // where your PHP app is served -}), -``` - -If you'd rather configure Vite by hand, you have two options: - -**Option 1: Configure CORS** - -The simplest solution is to allow cross-origin requests from your PHP application: - -```js -export default defineConfig({ - // ... other config ... - - server: { - cors: { - origin: 'http://myapp.local', // your PHP app URL - }, - }, -}); -``` - -**Option 2: Run Vite on your domain** - -The other solution is to make Vite run on the same domain as your PHP application. - -```js -export default defineConfig({ - // ... other config ... - - server: { - host: 'myapp.local', // same as your PHP app - }, -}); -``` - -Actually, even in this case, you need to configure CORS because the dev server runs on the same hostname but on a different port. However, in this case, CORS is automatically configured by the Nette Vite plugin. - - -HTTPS Development ------------------ - -If you develop on HTTPS, you need certificates for your Vite development server. The easiest way is using a plugin that generates certificates automatically: - -```shell -npm install -D vite-plugin-mkcert -``` - -Here's how to configure it in `vite.config.ts`: - -```js -import mkcert from 'vite-plugin-mkcert'; - -export default defineConfig({ - // ... other config ... - - plugins: [ - mkcert(), // generates certificates automatically and enables https - nette(), - ], -}); -``` - -Note that if you're using the CORS configuration (Option 1 from above), you need to update the origin URL to use `https://` instead of `http://`. - - -Docker Development ------------------- - -When you run Vite inside a Docker container, two things need attention: the browser on your machine must be able to reach the dev server, and Vite must detect file changes across the container boundary. - -First, [publish |https://docs.docker.com/get-started/docker-concepts/running-containers/publishing-ports/] the Vite port from the container and bind the dev server to all interfaces, so it is reachable from outside the container: - -```js -export default defineConfig({ - // ... other config ... - - plugins: [ - nette(), - ], - server: { - host: '0.0.0.0', // listen on all interfaces (required in a container) - port: 5173, // must match the published port - strictPort: true, // fail rather than pick another port - watch: { - usePolling: true, // enable if file changes aren't detected on mounted volumes - }, - }, -}); -``` - -The plugin writes the dev server URL into `nette.json` for the PHP side. Because `host: '0.0.0.0'` is not usable by a browser (it redirects to `localhost` for every asset), the plugin automatically rewrites it to `localhost` in that URL, so assets load correctly. - -If you open the application on a custom domain instead of `localhost`, set the plugin's `host` option to that domain: - -```js - plugins: [ - nette({ host: 'myapp.local' }), // the same domain as your PHP app - ], -``` - -The plugin then uses this host for the dev server URL, adds it to the CORS origins, and whitelists it in Vite's `allowedHosts` - so it works without any manual CORS setup. - -For more complex setups - for example when Vite runs behind a reverse proxy where the public host, port, and protocol all differ from the internal address - set Vite's `server.origin` to the full public URL. The plugin respects it and writes it into `nette.json` as-is, instead of deriving the URL from the local socket: - -```js - server: { - origin: 'https://myapp.local:8443', // the public URL where the browser reaches Vite - }, -``` - - -Testing on Other Devices ------------------------- - -To open the running app on your phone or another device on the same network, set the plugin's `host` option to `'network'`. The plugin binds Vite to all interfaces and writes your machine's LAN IP (instead of `localhost`) into `nette.json`, so assets and HMR load from the other device: - -```js -nette({ - entry: 'app.js', - host: 'network', // advertise the machine's LAN IP -}), -``` - -Then browse to your PHP app from the device using that same LAN IP. If no external address is found, the plugin falls back to `localhost`. - - -Production Builds -================= - -Create optimized production files: - -```shell -npm run build -``` - -Vite will: -- Minify all JavaScript and CSS -- Split code into optimal chunks -- Generate hashed filenames for cache-busting -- Create a manifest file for Nette Assets - -Example output: - -``` -www/assets/ -├── app-4f3a2b1c.js # Your main JavaScript (minified) -├── app-7d8e9f2a.css # Extracted CSS (minified) -├── vendor-8c4b5e6d.js # Shared dependencies -└── .vite/ - └── manifest.json # Mapping for Nette Assets -``` - -The hashed filenames ensure browsers always load the latest version. - - -Public Folder -============= - -Files in the `assets/public/` directory are copied to the output without processing: - -``` -assets/ -├── public/ -│ ├── favicon.ico -│ ├── robots.txt -│ └── images/ -│ └── og-image.jpg -├── app.js -└── style.css -``` - -Reference them normally: - -```latte -{* These files are copied as-is *} - - -``` - -For public files, you can use FilesystemMapper features. The `extension` option applies to references without an extension (e.g. `{asset 'images/og-image'}` finds `og-image.webp` first): - -```neon -assets: - mapping: - default: - type: vite - path: assets - extension: [webp, jpg, png] # for extension-less references - versioning: true # Add cache-busting -``` - -In the `vite.config.ts` configuration you can change the public folder using the `publicDir` option. - - -Dynamic Imports -=============== - -Vite automatically splits code for optimal loading. Dynamic imports allow you to load code only when it's actually needed, reducing the initial bundle size: - -```js -// Load heavy components on demand -button.addEventListener('click', async () => { - let { Chart } = await import('./components/chart.js') - new Chart(data) -}) -``` - -Dynamic imports create separate chunks that are loaded only when needed. This is called "code splitting" and it's one of Vite's most powerful features. When you use dynamic imports, Vite automatically creates separate JavaScript files for each dynamically imported module. - -The `{asset 'app.js'}` tag does **not** automatically preload these dynamic chunks. This is intentional behavior - we don't want to download code that might never be used. The chunks are downloaded only when the dynamic import is executed. - -However, if you know that certain dynamic imports are critical and will be needed soon, you can preload them: - -```latte -{* Main entry point *} -{asset 'app.js'} - -{* Preload critical dynamic imports *} -{preload 'components/chart.js'} -``` - -This tells the browser to download the chart component in the background, so it's ready immediately when needed. - - -TypeScript Support -================== - -TypeScript works out of the box: - -```ts -// assets/main.ts -interface User { - name: string - email: string -} - -export function greetUser(user: User): void { - console.log(`Hello, ${user.name}!`) -} -``` - -Reference TypeScript files normally (as with any file, `main.ts` must be an entry point): - -```latte -{asset 'main.ts'} -``` - -For full TypeScript support, install it: - -```shell -npm install -D typescript -``` - - -Additional Vite Configuration -============================= - -Here are some useful Vite configuration options with detailed explanations: - -```js -export default defineConfig({ - // Root directory containing source assets - root: 'assets', - - // Folder whose contents are copied to output directory as-is - // Default: 'public' (relative to 'root') - publicDir: 'public', - - build: { - // Where to put compiled files (relative to 'root') - outDir: '../www/assets', - - // Empty output directory before building? - // Useful to remove old files from previous builds - emptyOutDir: true, - - // Subdirectory within outDir for generated chunks and assets - // This helps organize the output structure - assetsDir: 'static', - - rollupOptions: { - // Entry point(s) - can be a single file or array of files - // Each entry point becomes a separate bundle - input: [ - 'app.js', // main application - 'admin.js', // admin panel - ], - }, - }, - - server: { - // Host to bind the dev server to - // Use '0.0.0.0' to expose to network - host: 'localhost', - - // Port for the dev server - port: 5173, - - // CORS configuration for cross-origin requests - cors: { - origin: 'http://myapp.local', - }, - }, - - css: { - // Enable CSS source maps in development - devSourcemap: true, - }, - - plugins: [ - nette(), - ], -}); -``` - -.[warning] -Watch out for the entry paths in `rollupOptions.input` above: Rollup resolves relative paths against the current working directory (your project root), **not** against `root: 'assets'`. So a bare `'app.js'` won't exist at build time. Either use the plugin's `entry` option (which resolves entry paths relative to `root`), or write the paths relative to the project root, e.g. `'assets/app.js'`. - -That's it! You now have a modern build system integrated with Nette Assets. - -{{maintitle: Vite in PHP with Nette Assets: HMR and Production Builds}} -{{description: First-class Vite integration for PHP apps: lightning-fast development with Hot Module Replacement, optimized production builds, entry points and TypeScript.}} diff --git a/assets/es/@home.texy b/assets/es/@home.texy deleted file mode 100644 index 513e930eb2..0000000000 --- a/assets/es/@home.texy +++ /dev/null @@ -1,467 +0,0 @@ -Nette Assets -************ - -
    - -¿Cansado de gestionar a mano los archivos estáticos de sus aplicaciones web? Olvídese de escribir rutas a mano, de lidiar con la invalidación de la caché o de preocuparse por el versionado de los archivos. Nette Assets transforma la forma de trabajar con imágenes, hojas de estilo, scripts y otros recursos estáticos. - -- El **versionado inteligente** asegura que los navegadores carguen siempre los archivos más recientes -- **Detección automática** de los tipos de archivo y de sus dimensiones -- **Integración perfecta con Latte** mediante etiquetas intuitivas -- **Arquitectura flexible** que soporta sistemas de archivos, CDN y Vite -- **Carga diferida** para un rendimiento óptimo - -
    - - -¿Por qué Nette Assets? -====================== - -Trabajar con archivos estáticos suele significar código repetitivo y propenso a errores. Construye las URL a mano, añade parámetros de versión para invalidar la caché y trata cada tipo de archivo de forma distinta. Eso lleva a un código como este: - -```latte -Logo - -``` - -Con Nette Assets toda esa complejidad desaparece: - -```latte -{* Todo automatizado: URL, versionado, dimensiones *} - - - -{* O simplemente *} -{asset 'css/style.css'} -``` - -¡Eso es todo! La biblioteca automáticamente: -- Añade los parámetros de versión según la fecha de modificación del archivo -- Detecta las dimensiones de las imágenes y las incluye en el HTML -- Genera el elemento HTML correcto para cada tipo de archivo -- Se ocupa tanto del entorno de desarrollo como del de producción - - -Instalación -=========== - -Instale Nette Assets con [Composer|best-practices:composer]: - -```shell -composer require nette/assets -``` - -Requiere PHP 8.1 o superior y funciona perfectamente con Nette Framework, pero también se puede usar por separado. - - -Primeros pasos -============== - -Nette Assets funciona nada más instalarlo, sin ninguna configuración. Coloque sus archivos estáticos en el directorio `www/assets/` y empiece a usarlos: - -```latte -{* Muestra una imagen con las dimensiones automáticas *} -{asset 'logo.png'} - -{* Incluye una hoja de estilo con versionado *} -{asset 'style.css'} - -{* Carga un script *} -{asset 'app.js'} -``` - -Para tener más control sobre el HTML generado, use el atributo `n:asset` o la función `asset()`. - - -Cómo funciona -============= - -Nette Assets se construye en torno a tres conceptos básicos que lo hacen potente y a la vez sencillo de usar: - - -Assets: sus archivos, con inteligencia --------------------------------------- - -Un **asset** representa cualquier archivo estático de su aplicación. Cada archivo se convierte en un objeto con propiedades de solo lectura útiles: - -```php -$image = $assets->getAsset('photo.jpg'); -echo $image->url; // '/assets/photo.jpg?v=1699123456' -echo $image->file; // '/var/www/assets/photo.jpg' (ruta local, o null) -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' -``` - -Los distintos tipos de archivo proporcionan propiedades distintas: -- **Imágenes**: ancho, alto, texto alternativo, carga diferida -- **Scripts**: tipo de módulo, hashes de integridad, crossorigin -- **Hojas de estilo**: media queries, integridad -- **Audio y vídeo**: duración, dimensiones (solo el vídeo) -- **Fuentes**: precarga correcta con CORS - -La biblioteca detecta automáticamente los tipos de archivo y crea la clase de asset adecuada. - - -Mappers: de dónde vienen los archivos -------------------------------------- - -Un **mapper** sabe cómo encontrar los archivos y crear las URL para ellos. Puede tener varios mappers para distintos fines: archivos locales, CDN, almacenamiento en la nube o herramientas de compilación (cada uno de ellos tiene un nombre). El `FilesystemMapper` integrado se ocupa de los archivos locales, mientras que `ViteMapper` se integra con las herramientas de compilación modernas. - -Los mappers se definen en la [configuración |configuration]. - - -Registry: su interfaz principal -------------------------------- - -El **registry** gestiona todos los mappers y proporciona la API principal: - -```php -// Inyecte el registry en su servicio -public function __construct( - private Nette\Assets\Registry $assets -) {} -``` - -```php -// Obtiene assets de distintos mappers -$logo = $this->assets->getAsset('images:logo.png'); // mapper 'images' -$app = $this->assets->getAsset('app:main.js'); // mapper 'app' -$style = $this->assets->getAsset('style.css'); // usa el mapper predeterminado -``` - -El registry elige automáticamente el mapper correcto y cachea los resultados por rendimiento. - - -Trabajar con assets en PHP -========================== - -El Registry proporciona dos métodos para obtener los assets: - -```php -// Lanza Nette\Assets\AssetNotFoundException si el archivo no existe -$logo = $assets->getAsset('logo.png'); - -// Devuelve null si el archivo no existe -$banner = $assets->tryGetAsset('banner.jpg'); -if ($banner) { - echo $banner->url; -} -``` - - -Indicar el mapper ------------------ - -Puede elegir explícitamente qué mapper usar: - -```php -// Usa el mapper predeterminado -$file = $assets->getAsset('document.pdf'); - -// Usa un mapper concreto con prefijo -$image = $assets->getAsset('images:photo.jpg'); - -// Usa un mapper concreto con la sintaxis de array -$script = $assets->getAsset(['scripts', 'app.js']); -``` - - -Propiedades y tipos de los assets ---------------------------------- - -Cada tipo de asset proporciona las propiedades de solo lectura pertinentes: - -```php -// Propiedades de una imagen -$image = $assets->getAsset('photo.jpg'); -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' - -// Propiedades de un script -$script = $assets->getAsset('app.js'); -echo $script->type; // null ('module' en los puntos de entrada de Vite) - -// Propiedades de un audio -$audio = $assets->getAsset('song.mp3'); -echo $audio->duration; // duración en segundos - -// Todos los assets se pueden convertir a cadena (devuelve la URL) -$url = (string) $assets->getAsset('document.pdf'); -``` - -.[note] -Las propiedades como las dimensiones o la duración se cargan de forma diferida, solo al acceder a ellas, lo que mantiene la biblioteca rápida. - -.[tip] -Para un análisis estático preciso, instale la extensión [nette/phpstan-rules |tools:phpstan-rules#Assets]. PHPStan conoce entonces el tipo concreto de cada asset, así que `getAsset('photo.jpg')` se entiende como `ImageAsset` y acceder a `->width` no provoca ningún error. - - -Usar los assets en las plantillas de Latte -========================================== - -Nette Assets proporciona una integración intuitiva con [Latte|latte:] mediante etiquetas y funciones. - - -`{asset}` ---------- - -La etiqueta `{asset}` renderiza elementos HTML completos: - -```latte -{* Renderiza: *} -{asset 'hero.jpg'} - -{* Renderiza: *} -{asset 'app.js'} - -{* Renderiza: *} -{asset 'style.css'} -``` - -La etiqueta automáticamente: -- Detecta el tipo de asset y genera el HTML adecuado -- Incluye el versionado para invalidar la caché -- Añade las dimensiones de las imágenes -- Establece los atributos correctos (type, media, etc.) - -Cuando se usa dentro de atributos HTML o dentro de los elementos ` -``` - - -Mappers propios -=============== - -Cree mappers propios para necesidades especiales, como el almacenamiento en la nube o la generación dinámica: - -```php -use Nette\Assets\Mapper; -use Nette\Assets\Asset; -use Nette\Assets\Helpers; - -class CloudStorageMapper implements Mapper -{ - public function __construct( - private CloudClient $client, - private string $bucket, - ) {} - - public function getAsset(string $reference, array $options = []): Asset - { - if (!$this->client->exists($this->bucket, $reference)) { - throw new Nette\Assets\AssetNotFoundException("Asset '$reference' not found"); - } - - $url = $this->client->getPublicUrl($this->bucket, $reference); - return Helpers::createAssetFromUrl($url); - } -} -``` - -Regístrelo en la configuración: - -```neon -assets: - mapping: - cloud: CloudStorageMapper(@cloudClient, 'my-bucket') -``` - -Úselo como cualquier otro mapper: - -```latte -{asset 'cloud:user-uploads/photo.jpg'} -``` - -El método `Helpers::createAssetFromUrl()` crea automáticamente el tipo de asset correcto según la extensión del archivo. - -Los tipos de asset que implementan la interfaz `Nette\Assets\HtmlRenderable` (imágenes, scripts, estilos, etc.) los puede renderizar `{asset}` como un elemento HTML completo. Los demás tipos de archivo se convierten en un `GenericAsset` (por ejemplo un PDF), que no se puede renderizar como elemento HTML pero sigue proporcionando una URL (y otros metadatos). Intentar renderizar un asset así como elemento HTML lanza `Nette\InvalidArgumentException`; su URL sí la puede usar dentro de un atributo. - - -Lecturas adicionales -==================== - -- [Nette Assets: por fin una API unificada para todo, de las imágenes a Vite |https://blog.nette.org/en/introducing-nette-assets] diff --git a/assets/es/@left-menu.texy b/assets/es/@left-menu.texy deleted file mode 100644 index f0218f3720..0000000000 --- a/assets/es/@left-menu.texy +++ /dev/null @@ -1,14 +0,0 @@ -Nette Assets -************ -- [Primeros pasos |@home] -- [Vite |vite] -- [Configuración|configuration] - - -Lecturas adicionales -******************** -- [Documentación de Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Buenas prácticas |best-practices:] -- [Solución de problemas |nette:troubleshooting] diff --git a/assets/es/@meta.texy b/assets/es/@meta.texy deleted file mode 100644 index 3798d9cda4..0000000000 --- a/assets/es/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentación de Nette}} diff --git a/assets/es/configuration.texy b/assets/es/configuration.texy deleted file mode 100644 index 670276faa4..0000000000 --- a/assets/es/configuration.texy +++ /dev/null @@ -1,195 +0,0 @@ -Configuración de Assets -*********************** - -.[perex] -Resumen de las opciones de configuración de Nette Assets. - - -```neon -assets: - # ruta base para resolver las rutas relativas de los mappers - basePath: ... # (string) el valor predeterminado es %wwwDir% - - # URL base para resolver las URL relativas de los mappers - baseUrl: ... # (string) el valor predeterminado es %baseUrl% - - # ¿activar globalmente el versionado de los assets? - versioning: ... # (bool) el valor predeterminado es true - - # define los mappers de assets - mapping: ... # (array) el valor predeterminado es la ruta 'assets' -``` - -`basePath` establece el directorio predeterminado del sistema de archivos para resolver las rutas relativas de los mappers. De forma predeterminada usa el directorio web (`%wwwDir%`). - -`baseUrl` establece el prefijo de URL predeterminado para resolver las URL relativas de los mappers. De forma predeterminada usa la URL raíz (`%baseUrl%`). - -La opción `versioning` controla globalmente si a las URL de los assets se les añaden parámetros de versión para invalidar la caché. Cada mapper puede sobrescribir este ajuste. - - -Mappers -------- - -Los mappers se pueden configurar de tres formas: con la notación sencilla de cadena, con la notación detallada de array, o como servicio (usando la notación de entidad `NombreDeClase(...)` o `@nombreDeServicio()`). - -La forma más sencilla de definir un mapper: - -```neon -assets: - mapping: - default: assets # Crea un mapper de sistema de archivos para %wwwDir%/assets/ - images: img # Crea un mapper de sistema de archivos para %wwwDir%/img/ - scripts: js # Crea un mapper de sistema de archivos para %wwwDir%/js/ -``` - -Cada mapper crea un `FilesystemMapper` que: -- Busca los archivos en `%wwwDir%/` -- Genera URL como `%baseUrl%/` -- Hereda el ajuste global de versionado - - -Para tener más control, use la notación detallada: - -```neon -assets: - mapping: - images: - # directorio donde se guardan los archivos - path: ... # (string) opcional, el valor predeterminado es la ruta base (basePath) - - # prefijo de URL de los enlaces generados - url: ... # (string) opcional, el valor predeterminado es path - - # ¿activar el versionado para este mapper? - versioning: ... # (bool) opcional, hereda el ajuste global - - # añadir automáticamente la extensión (o extensiones) al buscar los archivos - extension: ... # (string|array) opcional, el valor predeterminado es null -``` - -Así se resuelven los valores de la configuración: - -Resolución de la ruta: - - Las rutas relativas se resuelven a partir de `basePath` (o de `%wwwDir%` si `basePath` no está establecido) - - Las rutas absolutas se usan tal cual - -Resolución de la URL: - - Las URL relativas se resuelven a partir de `baseUrl` (o de `%baseUrl%` si `baseUrl` no está establecido) - - Las URL absolutas (con esquema o `//`) se usan tal cual (una URL `//` toma el esquema de `baseUrl`) - - Si no se indica `url`, se usa el valor de `path` - - -```neon -assets: - basePath: /var/www/project/www - baseUrl: https://example.com/assets - - mapping: - # Ruta y URL relativas - images: - path: img # Se resuelve como: /var/www/project/www/img - url: images # Se resuelve como: https://example.com/assets/images - - # Ruta y URL absolutas - uploads: - path: /var/shared/uploads # Se usa tal cual: /var/shared/uploads - url: https://cdn.example.com # Se usa tal cual: https://cdn.example.com - - # Solo se indica la ruta - styles: - path: css # Ruta: /var/www/project/www/css - # URL: https://example.com/assets/css -``` - - -Mappers propios ---------------- - -Para los mappers propios, referencie un servicio existente con `@nombreDeServicio`, o defínalo directamente con `NombreDeClase(argumentos)` o con el nombre de la clase a secas: - -```neon -services: - s3mapper: App\Assets\S3Mapper(%s3.bucket%) - -assets: - mapping: - cloud: @s3mapper - database: App\Assets\DatabaseMapper(@database.connection) -``` - - -Mapper de Vite --------------- - -El mapper de Vite solo requiere que añada `type: vite`. Esta es la lista completa de opciones de configuración: - -```neon -assets: - mapping: - default: - # tipo de mapper (obligatorio para Vite) - type: vite # (string) obligatorio, tiene que ser 'vite' - - # directorio de salida de la compilación de Vite - path: ... # (string) opcional, el valor predeterminado es la ruta base (basePath) - - # prefijo de URL de los assets compilados - url: ... # (string) opcional, el valor predeterminado es path - - # ubicación del archivo de manifiesto de Vite - manifest: ... # (string) opcional, relativo a path, el valor predeterminado es /.vite/manifest.json - - # configuración del servidor de desarrollo de Vite - devServer: ... # (bool|string) opcional, el valor predeterminado es true - - # versionado de los archivos del directorio público - versioning: ... # (bool) opcional, hereda el ajuste global - - # extensión automática de los archivos del directorio público - extension: ... # (string|array) opcional, el valor predeterminado es null -``` - -La opción `devServer` controla cómo se cargan los assets durante el desarrollo: - -- `true` (predeterminado): detecta automáticamente si hay un servidor de desarrollo de Vite en marcha (mediante el archivo `.vite/nette.json` que el plugin de Vite de Nette crea en el directorio de compilación). Si el servidor de desarrollo está en marcha **y su aplicación está en modo de depuración**, los assets se cargan de él con soporte de hot module replacement. Si el servidor de desarrollo no está en marcha, los assets se cargan de los archivos compilados del directorio público. -- `false`: desactiva por completo la integración con el servidor de desarrollo. Los assets se cargan siempre de los archivos compilados. -- Una URL propia (p. ej. `https://localhost:5173`): indica manualmente la URL del servidor de desarrollo, incluidos el protocolo y el puerto. Es útil cuando el servidor de desarrollo corre en otro host o puerto. Igual que la detección automática, se aplica solo en modo de depuración; en producción se usan siempre los archivos compilados. - -Las opciones `versioning` y `extension` se aplican solo a los archivos del directorio público de Vite que Vite no procesa. - - -Configuración manual --------------------- - -Cuando no use Nette DI, configure los mappers a mano: - -```php -use Nette\Assets\Registry; -use Nette\Assets\FilesystemMapper; -use Nette\Assets\ViteMapper; - -$registry = new Registry; - -// Añade un mapper de sistema de archivos -$registry->addMapper('images', new FilesystemMapper( - baseUrl: 'https://example.com/img', - basePath: __DIR__ . '/www/img', - extensions: ['webp', 'jpg', 'png'], - versioning: true, -)); - -// Añade un mapper de Vite -$registry->addMapper('app', new ViteMapper( - baseUrl: '/build', - basePath: __DIR__ . '/www/build', - manifestPath: __DIR__ . '/www/build/.vite/manifest.json', - devServer: 'https://localhost:5173', -)); -``` - -También puede obtener por su nombre cualquier mapper registrado con el método `getMapper()`: - -```php -$mapper = $registry->getMapper('images'); // devuelve el mapper registrado -$default = $registry->getMapper(); // devuelve el mapper 'default' -``` diff --git a/assets/es/vite.texy b/assets/es/vite.texy deleted file mode 100644 index 41c74549e4..0000000000 --- a/assets/es/vite.texy +++ /dev/null @@ -1,567 +0,0 @@ -Integración con Vite -******************** - -
    - -Las aplicaciones JavaScript modernas requieren herramientas de compilación sofisticadas. Nette Assets proporciona una integración de primera clase con [Vite |https://vite.dev/], la herramienta de compilación frontend de nueva generación. Consiga un desarrollo ultrarrápido con Hot Module Replacement (HMR) y compilaciones de producción optimizadas sin quebraderos de cabeza de configuración. - -- **Cero configuración**: puente automático entre Vite y las plantillas PHP -- **Gestión completa de las dependencias**: una sola etiqueta se ocupa de todos los assets -- **Hot Module Replacement**: actualizaciones instantáneas de JavaScript y CSS -- **Compilaciones de producción optimizadas**: división del código y tree shaking - -
    - - -Nette Assets se integra perfectamente con Vite, así que obtiene todas estas ventajas mientras escribe sus plantillas como siempre. - - -Configurar Vite -=============== - -Configuremos Vite paso a paso. No se preocupe si las herramientas de compilación son nuevas para usted, ¡lo explicaremos todo! - - -Paso 1: instalar Vite ---------------------- - -Primero, instale Vite y el plugin de Nette en su proyecto: - -```shell -npm install -D vite @nette/vite-plugin -``` - -Eso instala Vite y un plugin especial que ayuda a que Vite funcione perfectamente con Nette. - - -Paso 2: estructura del proyecto -------------------------------- - -El enfoque estándar es colocar los archivos fuente de los assets en una carpeta `assets/` en la raíz del proyecto, y las versiones compiladas en `www/assets/`: - -/--pre -web-project/ -├── assets/ ← archivos fuente (SCSS, TypeScript, imágenes fuente) -│ ├── public/ ← archivos estáticos (se copian tal cual) -│ │ └── favicon.ico -│ ├── images/ -│ │ └── logo.png -│ ├── app.js ← punto de entrada principal -│ └── style.css ← sus estilos -└── www/ ← directorio público (document root) - ├── assets/ ← aquí irán los archivos compilados - └── index.php -\-- - -La carpeta `assets/` contiene sus archivos fuente, el código que escribe. Vite procesará esos archivos y pondrá las versiones compiladas en `www/assets/`. - - -Paso 3: configurar Vite ------------------------ - -Cree un archivo `vite.config.ts` en la raíz de su proyecto. Ese archivo le dice a Vite dónde encontrar los archivos fuente y dónde poner los compilados. - -El plugin de Vite de Nette trae valores predeterminados inteligentes que simplifican la configuración. Da por hecho que los archivos fuente del frontend están en el directorio `assets/` (opción `root`) y que los compilados van a `www/assets/` (opción `outDir`). Solo tiene que indicar el [punto de entrada|#Puntos de entrada]: - -```js -import { defineConfig } from 'vite'; -import nette from '@nette/vite-plugin'; - -export default defineConfig({ - plugins: [ - nette({ - entry: 'app.js', - }), - ], -}); -``` - -Por debajo, además de `root` y `outDir`, el plugin establece algunas opciones más de Vite para que todo encaje: `base` a `''` (los assets se sirven directamente desde el document root), `build.manifest` a `true` (para que Nette Assets pueda mapear los nombres de archivo con hash) y `build.assetsDir` a `''` (los archivos compilados van directamente a `outDir`, sin una subcarpeta `static/`). Puede sobrescribir cualquiera de ellas. - -.[note] -El `outDir` predeterminado (`www/assets`) requiere que el directorio `www/` ya exista. Si no existe, el plugin se detiene con el error *"The output directory ... does not exist"*. - -Si quiere indicar otro nombre de directorio para compilar sus assets, tendrá que cambiar algunas opciones: - -```js -export default defineConfig({ - root: 'assets', // directorio raíz de los assets fuente - - build: { - outDir: '../www/assets', // adónde van los archivos compilados - }, - - // ... resto de la configuración ... -}); -``` - -.[note] -La ruta `outDir` se considera relativa a `root`, por eso lleva `../` al principio. - - -Paso 4: configurar Nette ------------------------- - -Hable a Nette Assets de Vite en su `common.neon`: - -```neon -assets: - mapping: - default: - type: vite # le dice a Nette que use el ViteMapper - path: assets -``` - - -Paso 5: añadir los scripts --------------------------- - -Añada estos scripts a su `package.json`: - -```json -{ - "scripts": { - "dev": "vite", - "build": "vite build" - } -} -``` - -Ahora puede: -- `npm run dev`: arranca el servidor de desarrollo con recarga en caliente -- `npm run build`: crea los archivos de producción optimizados - - -Puntos de entrada -================= - -Un **punto de entrada** es el archivo principal donde arranca su aplicación. Desde ese archivo importa otros archivos (CSS, módulos de JavaScript, imágenes) y crea un árbol de dependencias. Vite sigue esas importaciones y lo empaqueta todo junto. - -Ejemplo de punto de entrada `assets/app.js`: - -```js -// Importa los estilos -import './style.css' - -// Importa los módulos de JavaScript -import netteForms from 'nette-forms'; -import naja from 'naja'; - -// Inicializa la aplicación -netteForms.initOnLoad(); -naja.initialize(); -``` - -En la plantilla puede insertar un punto de entrada así: - -```latte -{asset 'app.js'} -``` - -Nette Assets genera automáticamente todas las etiquetas HTML necesarias: JavaScript, CSS y cualquier otra dependencia. - - -Varios puntos de entrada ------------------------- - -Las aplicaciones más grandes suelen necesitar puntos de entrada separados: - -```js -export default defineConfig({ - plugins: [ - nette({ - entry: [ - 'app.js', // páginas públicas - 'admin.js', // panel de administración - ], - }), - ], -}); -``` - -Úselos en plantillas distintas: - -```latte -{* En las páginas públicas *} -{asset 'app.js'} - -{* En el panel de administración *} -{asset 'admin.js'} -``` - - -Importante: archivos fuente frente a archivos compilados --------------------------------------------------------- - -Es fundamental entender que en producción solo puede cargar los archivos que Vite pone a disposición, ya sea a través de su manifiesto o copiándolos sin cambios desde la carpeta pública: - -1. **Los puntos de entrada** definidos en `entry` (incluidos los módulos que importan dinámicamente) y los **assets referenciados desde JavaScript o CSS** (imágenes, fuentes, ...): todos ellos quedan registrados en el manifiesto -2. **Los archivos del directorio `assets/public/`**: estos *no* están en el manifiesto; se copian tal cual y `{asset}` los localiza mediante una búsqueda alternativa en el sistema de archivos - -**No** puede cargar con `{asset}` archivos arbitrarios de `assets/`: si un archivo no se referencia en ningún sitio, no se compilará. Si quiere que Vite conozca otros assets, puede moverlos a la [carpeta pública |#Carpeta pública]. - -Tenga en cuenta que, de forma predeterminada, Vite incrusta todos los assets menores de 4 KB, así que no podrá referenciar esos archivos directamente. (Vea la [documentación de Vite |https://vite.dev/guide/assets.html]). - -```latte -{* ✓ Esto funciona: es un punto de entrada *} -{asset 'app.js'} - -{* ✓ Esto funciona: está en assets/public/ *} -{asset 'favicon.ico'} - -{* ✗ Esto no funcionará: un archivo cualquiera de assets/ *} -{asset 'components/button.js'} -``` - - -Modo de desarrollo -================== - -El modo de desarrollo es totalmente opcional, pero aporta ventajas notables cuando se activa. La principal es el **Hot Module Replacement (HMR)**: ve los cambios al instante sin perder el estado de la aplicación, lo que hace la experiencia de desarrollo mucho más fluida y rápida. - -Vite es una herramienta de compilación moderna que hace el desarrollo increíblemente rápido. A diferencia de los empaquetadores tradicionales, durante el desarrollo Vite sirve su código directamente al navegador, lo que significa un arranque instantáneo del servidor por grande que sea su proyecto y actualizaciones ultrarrápidas. - - -Arrancar el servidor de desarrollo ----------------------------------- - -Ejecute el servidor de desarrollo: - -```shell -npm run dev -``` - -Verá: - -``` - ➜ Local: http://localhost:5173/ - ➜ Network: use --host to expose -``` - -Mantenga esta terminal abierta mientras desarrolla. - -Mientras el servidor de desarrollo está en marcha, el plugin escribe un pequeño archivo de señal `www/assets/.vite/nette.json` que contiene su URL. Nette Assets, del lado de PHP, lee ese archivo y pasa a cargar desde el servidor de desarrollo cuando se cumplen ambas cosas: -1. el servidor de desarrollo de Vite está en marcha (el archivo de señal existe), y -2. su aplicación de Nette está en modo de depuración. - -El resultado: - -```latte -{asset 'app.js'} -{* En desarrollo: - *} -{* En producción: *} -``` - -No hace falta configurar nada, ¡simplemente funciona! Para desactivar la detección o establecer la URL del servidor de desarrollo a mano, vea la [opción devServer |configuration#Mapper de Vite]. - -.[note] -El archivo de señal está en `www/assets/.vite/nette.json` (justo al lado del `manifest.json` de producción). Puede renombrarlo con la opción `infoFile` del plugin, cuyo valor predeterminado es `.vite/nette.json`. Si Nette no detecta el servidor de desarrollo en marcha, compruebe que ese archivo existe y apunta a la URL correcta. - - -Trabajar en dominios distintos ------------------------------- - -Si su servidor de desarrollo corre en algo que no sea `localhost` (como `myapp.local`), puede toparse con problemas de CORS (Cross-Origin Resource Sharing). CORS es una función de seguridad de los navegadores web que bloquea de forma predeterminada las peticiones entre dominios distintos. Cuando su aplicación PHP corre en `myapp.local` pero Vite corre en `localhost:5173`, el navegador los ve como dominios distintos y bloquea las peticiones. - -Tiene dos opciones para resolverlo: - -**Opción 1: configurar CORS** - -La solución más sencilla es permitir las peticiones entre orígenes desde su aplicación PHP: - -```js -export default defineConfig({ - // ... resto de la configuración ... - - server: { - cors: { - origin: 'http://myapp.local', // la URL de su aplicación PHP - }, - }, -}); -``` - -**Opción 2: ejecutar Vite en su dominio** - -La otra solución es hacer que Vite corra en el mismo dominio que su aplicación PHP. - -```js -export default defineConfig({ - // ... resto de la configuración ... - - server: { - host: 'myapp.local', // el mismo que su aplicación PHP - }, -}); -``` - -En realidad, incluso en este caso hay que configurar CORS, porque el servidor de desarrollo corre en el mismo hostname pero en otro puerto. Pero en este caso el CORS lo configura automáticamente el plugin de Vite de Nette. - - -Desarrollo con HTTPS --------------------- - -Si desarrolla con HTTPS, necesita certificados para su servidor de desarrollo de Vite. La forma más fácil es usar un plugin que genere los certificados automáticamente: - -```shell -npm install -D vite-plugin-mkcert -``` - -Así se configura en `vite.config.ts`: - -```js -import mkcert from 'vite-plugin-mkcert'; - -export default defineConfig({ - // ... resto de la configuración ... - - plugins: [ - mkcert(), // genera los certificados automáticamente y activa https - nette(), - ], -}); -``` - -Tenga en cuenta que, si usa la configuración de CORS (la opción 1 de arriba), tiene que actualizar la URL de origin para que use `https://` en lugar de `http://`. - - -Desarrollo con Docker ---------------------- - -Cuando ejecuta Vite dentro de un contenedor de Docker hay que prestar atención a dos cosas: el navegador de su máquina tiene que poder llegar al servidor de desarrollo, y Vite tiene que detectar los cambios en los archivos a través de la frontera del contenedor. - -Primero, [publique |https://docs.docker.com/get-started/docker-concepts/running-containers/publishing-ports/] el puerto de Vite del contenedor y enlace el servidor de desarrollo a todas las interfaces, para que sea accesible desde fuera del contenedor: - -```js -export default defineConfig({ - // ... resto de la configuración ... - - plugins: [ - nette(), - ], - server: { - host: '0.0.0.0', // escucha en todas las interfaces (obligatorio en un contenedor) - port: 5173, // tiene que coincidir con el puerto publicado - strictPort: true, // falla en lugar de elegir otro puerto - watch: { - usePolling: true, // actívelo si no se detectan los cambios en los volúmenes montados - }, - }, -}); -``` - -El plugin escribe la URL del servidor de desarrollo en `nette.json` para el lado de PHP. Como `host: '0.0.0.0'` no le sirve a un navegador (redirige a `localhost` en cada asset), el plugin la reescribe automáticamente a `localhost` en esa URL, para que los assets se carguen correctamente. - -Si abre la aplicación en un dominio propio en lugar de `localhost`, establezca la opción `host` del plugin a ese dominio: - -```js - plugins: [ - nette({ host: 'myapp.local' }), // el mismo dominio que su aplicación PHP - ], -``` - -El plugin usa entonces ese host para la URL del servidor de desarrollo, lo añade a los orígenes de CORS y lo mete en la lista blanca `allowedHosts` de Vite, así que funciona sin configurar CORS a mano. - -Para configuraciones más complejas, por ejemplo cuando Vite corre detrás de un proxy inverso donde el host, el puerto y el protocolo públicos difieren de la dirección interna, establezca `server.origin` de Vite a la URL pública completa. El plugin la respeta y la escribe tal cual en `nette.json`, en lugar de deducir la URL del socket local: - -```js - server: { - origin: 'https://myapp.local:8443', // la URL pública donde el navegador llega a Vite - }, -``` - - -Compilaciones de producción -=========================== - -Cree los archivos de producción optimizados: - -```shell -npm run build -``` - -Vite hará lo siguiente: -- Minificará todo el JavaScript y el CSS -- Dividirá el código en fragmentos óptimos -- Generará nombres de archivo con hash para invalidar la caché -- Creará un archivo de manifiesto para Nette Assets - -Ejemplo de salida: - -``` -www/assets/ -├── app-4f3a2b1c.js # Su JavaScript principal (minificado) -├── app-7d8e9f2a.css # CSS extraído (minificado) -├── vendor-8c4b5e6d.js # Dependencias compartidas -└── .vite/ - └── manifest.json # Mapeo para Nette Assets -``` - -Los nombres de archivo con hash aseguran que los navegadores carguen siempre la última versión. - - -Carpeta pública -=============== - -Los archivos del directorio `assets/public/` se copian a la salida sin procesarlos: - -``` -assets/ -├── public/ -│ ├── favicon.ico -│ ├── robots.txt -│ └── images/ -│ └── og-image.jpg -├── app.js -└── style.css -``` - -Referéncielos con normalidad: - -```latte -{* Estos archivos se copian tal cual *} - - -``` - -En los archivos públicos puede usar las funciones de FilesystemMapper. La opción `extension` se aplica a las referencias sin extensión (p. ej. `{asset 'images/og-image'}` encuentra primero `og-image.webp`): - -```neon -assets: - mapping: - default: - type: vite - path: assets - extension: [webp, jpg, png] # para las referencias sin extensión - versioning: true # Añade la invalidación de caché -``` - -En la configuración de `vite.config.ts` puede cambiar la carpeta pública con la opción `publicDir`. - - -Importaciones dinámicas -======================= - -Vite divide automáticamente el código para cargarlo de forma óptima. Las importaciones dinámicas le permiten cargar código solo cuando se necesita de verdad, lo que reduce el tamaño del paquete inicial: - -```js -// Carga los componentes pesados bajo demanda -button.addEventListener('click', async () => { - let { Chart } = await import('./components/chart.js') - new Chart(data) -}) -``` - -Las importaciones dinámicas crean fragmentos separados que se cargan solo cuando hacen falta. Esto se llama "code splitting" y es una de las funciones más potentes de Vite. Cuando usa importaciones dinámicas, Vite crea automáticamente archivos JavaScript separados para cada módulo importado dinámicamente. - -La etiqueta `{asset 'app.js'}` **no** precarga automáticamente esos fragmentos dinámicos. Es un comportamiento intencionado: no queremos descargar código que quizá no se use nunca. Los fragmentos se descargan solo cuando se ejecuta la importación dinámica. - -Pero, si sabe que ciertas importaciones dinámicas son críticas y harán falta pronto, puede precargarlas: - -```latte -{* Punto de entrada principal *} -{asset 'app.js'} - -{* Precarga las importaciones dinámicas críticas *} -{preload 'components/chart.js'} -``` - -Eso le dice al navegador que descargue el componente del gráfico en segundo plano, para que esté listo de inmediato cuando haga falta. - - -Soporte de TypeScript -===================== - -TypeScript funciona nada más instalarlo: - -```ts -// assets/main.ts -interface User { - name: string - email: string -} - -export function greetUser(user: User): void { - console.log(`Hello, ${user.name}!`) -} -``` - -Referencie los archivos TypeScript con normalidad (como con cualquier archivo, `main.ts` tiene que ser un punto de entrada): - -```latte -{asset 'main.ts'} -``` - -Para tener soporte completo de TypeScript, instálelo: - -```shell -npm install -D typescript -``` - - -Configuración adicional de Vite -=============================== - -Aquí tiene algunas opciones útiles de la configuración de Vite con explicaciones detalladas: - -```js -export default defineConfig({ - // Directorio raíz que contiene los assets fuente - root: 'assets', - - // Carpeta cuyo contenido se copia tal cual al directorio de salida - // Predeterminado: 'public' (relativo a 'root') - publicDir: 'public', - - build: { - // Adónde poner los archivos compilados (relativo a 'root') - outDir: '../www/assets', - - // ¿Vaciar el directorio de salida antes de compilar? - // Útil para eliminar los archivos antiguos de compilaciones anteriores - emptyOutDir: true, - - // Subdirectorio dentro de outDir para los fragmentos y assets generados - // Ayuda a organizar la estructura de la salida - assetsDir: 'static', - - rollupOptions: { - // Punto (o puntos) de entrada: puede ser un solo archivo o un array de archivos - // Cada punto de entrada se convierte en un paquete separado - input: [ - 'app.js', // aplicación principal - 'admin.js', // panel de administración - ], - }, - }, - - server: { - // Host al que enlazar el servidor de desarrollo - // Use '0.0.0.0' para exponerlo a la red - host: 'localhost', - - // Puerto del servidor de desarrollo - port: 5173, - - // Configuración de CORS para las peticiones entre orígenes - cors: { - origin: 'http://myapp.local', - }, - }, - - css: { - // Activa los source maps de CSS en desarrollo - devSourcemap: true, - }, - - plugins: [ - nette(), - ], -}); -``` - -.[warning] -Ojo con las rutas de los puntos de entrada de `rollupOptions.input` de arriba: Rollup resuelve las rutas relativas respecto al directorio de trabajo actual (la raíz de su proyecto), **no** respecto a `root: 'assets'`. Así que un `'app.js'` a secas no existirá en tiempo de compilación. Use la opción `entry` del plugin (que resuelve las rutas de los puntos de entrada relativas a `root`) o escriba las rutas relativas a la raíz del proyecto, p. ej. `'assets/app.js'`. - -¡Eso es todo! Ya tiene un sistema de compilación moderno integrado con Nette Assets. diff --git a/assets/fr/@home.texy b/assets/fr/@home.texy deleted file mode 100644 index 723b4c88bc..0000000000 --- a/assets/fr/@home.texy +++ /dev/null @@ -1,467 +0,0 @@ -Nette Assets -************ - -
    - -Fatigué de gérer manuellement les fichiers statiques de vos applications web ? Oubliez les chemins écrits en dur, l'invalidation du cache et les soucis de versionnage des fichiers. Nette Assets transforme votre façon de travailler avec les images, les feuilles de style, les scripts et les autres ressources statiques. - -- **Le versionnage intelligent** garantit que les navigateurs chargent toujours les fichiers les plus récents -- **La détection automatique** des types de fichiers et de leurs dimensions -- **L'intégration transparente à Latte** avec des balises intuitives -- **Une architecture souple** prenant en charge les systèmes de fichiers, les CDN et Vite -- **Le chargement paresseux** pour des performances optimales - -
    - - -Pourquoi Nette Assets ? -======================= - -Travailler avec des fichiers statiques signifie souvent écrire du code répétitif et propice aux erreurs. Vous composez les URL à la main, ajoutez des paramètres de version pour invalider le cache et traitez différemment chaque type de fichier. Cela donne du code comme celui-ci : - -```latte -Logo - -``` - -Avec Nette Assets, toute cette complexité disparaît : - -```latte -{* Tout est automatisé - URL, versionnage, dimensions *} - - - -{* Ou simplement *} -{asset 'css/style.css'} -``` - -C'est tout ! La bibliothèque, automatiquement : -- ajoute des paramètres de version d'après la date de modification du fichier -- détecte les dimensions des images et les inclut dans le HTML -- génère l'élément HTML correct pour chaque type de fichier -- gère aussi bien l'environnement de développement que celui de production - - -Installation -============ - -Installez Nette Assets à l'aide de [Composer|best-practices:composer] : - -```shell -composer require nette/assets -``` - -Elle nécessite PHP 8.1 ou plus récent et fonctionne parfaitement avec Nette Framework, mais peut aussi être utilisée seule. - - -Premiers pas -============ - -Nette Assets fonctionne immédiatement, sans aucune configuration. Placez vos fichiers statiques dans le répertoire `www/assets/` et commencez à les utiliser : - -```latte -{* Affiche une image avec ses dimensions automatiques *} -{asset 'logo.png'} - -{* Inclut une feuille de style avec versionnage *} -{asset 'style.css'} - -{* Charge un script *} -{asset 'app.js'} -``` - -Pour plus de contrôle sur le HTML généré, utilisez l'attribut `n:asset` ou la fonction `asset()`. - - -Comment ça marche -================= - -Nette Assets s'articule autour de trois concepts fondamentaux qui la rendent puissante tout en restant simple à utiliser : - - -Assets - vos fichiers devenus intelligents ------------------------------------------- - -Un **asset** représente n'importe quel fichier statique de votre application. Chaque fichier devient un objet doté de propriétés en lecture seule utiles : - -```php -$image = $assets->getAsset('photo.jpg'); -echo $image->url; // '/assets/photo.jpg?v=1699123456' -echo $image->file; // '/var/www/assets/photo.jpg' (chemin local, ou null) -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' -``` - -Les différents types de fichiers offrent des propriétés différentes : -- **Images** : largeur, hauteur, texte alternatif, chargement paresseux -- **Scripts** : type de module, hachages d'intégrité, crossorigin -- **Feuilles de style** : media queries, intégrité -- **Audio/vidéo** : durée, dimensions (vidéo seulement) -- **Polices** : préchargement correct avec CORS - -La bibliothèque détecte automatiquement les types de fichiers et crée la classe d'asset appropriée. - - -Mappers - d'où viennent les fichiers ------------------------------------- - -Un **mapper** sait comment trouver les fichiers et créer leurs URL. Vous pouvez avoir plusieurs mappers à des fins différentes - fichiers locaux, CDN, stockage cloud ou outils de build (chacun porte un nom). Le `FilesystemMapper` intégré s'occupe des fichiers locaux, tandis que `ViteMapper` s'intègre aux outils de build modernes. - -Les mappers sont définis dans la [configuration]. - - -Registry - votre interface principale -------------------------------------- - -Le **registre** gère tous les mappers et fournit l'API principale : - -```php -// Injectez le registre dans votre service -public function __construct( - private Nette\Assets\Registry $assets -) {} -``` - -```php -// Récupère des assets depuis différents mappers -$logo = $this->assets->getAsset('images:logo.png'); // mapper 'images' -$app = $this->assets->getAsset('app:main.js'); // mapper 'app' -$style = $this->assets->getAsset('style.css'); // utilise le mapper par défaut -``` - -Le registre choisit automatiquement le bon mapper et met les résultats en cache pour les performances. - - -Travailler avec les assets en PHP -================================= - -Le registre fournit deux méthodes pour récupérer les assets : - -```php -// Lève Nette\Assets\AssetNotFoundException si le fichier n'existe pas -$logo = $assets->getAsset('logo.png'); - -// Renvoie null si le fichier n'existe pas -$banner = $assets->tryGetAsset('banner.jpg'); -if ($banner) { - echo $banner->url; -} -``` - - -Indiquer les mappers --------------------- - -Vous pouvez choisir explicitement le mapper à utiliser : - -```php -// Utilise le mapper par défaut -$file = $assets->getAsset('document.pdf'); - -// Utilise un mapper précis avec un préfixe -$image = $assets->getAsset('images:photo.jpg'); - -// Utilise un mapper précis avec la syntaxe tableau -$script = $assets->getAsset(['scripts', 'app.js']); -``` - - -Propriétés et types des assets ------------------------------- - -Chaque type d'asset fournit les propriétés en lecture seule qui le concernent : - -```php -// Propriétés d'une image -$image = $assets->getAsset('photo.jpg'); -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' - -// Propriétés d'un script -$script = $assets->getAsset('app.js'); -echo $script->type; // null ('module' pour les points d'entrée Vite) - -// Propriétés d'un fichier audio -$audio = $assets->getAsset('song.mp3'); -echo $audio->duration; // durée en secondes - -// Tous les assets peuvent être convertis en chaîne (ce qui renvoie l'URL) -$url = (string) $assets->getAsset('document.pdf'); -``` - -.[note] -Les propriétés comme les dimensions ou la durée ne sont chargées paresseusement qu'au moment de leur accès, ce qui garde la bibliothèque rapide. - -.[tip] -Pour une analyse statique précise, installez l'extension [nette/phpstan-rules |tools:phpstan-rules#Assets]. PHPStan connaît alors le type concret de chaque asset : `getAsset('photo.jpg')` est compris comme un `ImageAsset` et l'accès à `->width` ne provoque aucune erreur. - - -Utiliser les assets dans les templates Latte -============================================ - -Nette Assets offre une intégration intuitive à [Latte|latte:] avec des balises et des fonctions. - - -`{asset}` ---------- - -La balise `{asset}` rend des éléments HTML complets : - -```latte -{* Rend : *} -{asset 'hero.jpg'} - -{* Rend : *} -{asset 'app.js'} - -{* Rend : *} -{asset 'style.css'} -``` - -La balise, automatiquement : -- détecte le type d'asset et génère le HTML approprié -- inclut le versionnage pour invalider le cache -- ajoute les dimensions des images -- pose les bons attributs (type, media, etc.) - -Utilisée à l'intérieur d'attributs HTML ou dans les éléments ` -``` - - -Mappers personnalisés -===================== - -Créez des mappers personnalisés pour des besoins particuliers comme le stockage cloud ou la génération dynamique : - -```php -use Nette\Assets\Mapper; -use Nette\Assets\Asset; -use Nette\Assets\Helpers; - -class CloudStorageMapper implements Mapper -{ - public function __construct( - private CloudClient $client, - private string $bucket, - ) {} - - public function getAsset(string $reference, array $options = []): Asset - { - if (!$this->client->exists($this->bucket, $reference)) { - throw new Nette\Assets\AssetNotFoundException("Asset '$reference' not found"); - } - - $url = $this->client->getPublicUrl($this->bucket, $reference); - return Helpers::createAssetFromUrl($url); - } -} -``` - -Enregistrez-le dans la configuration : - -```neon -assets: - mapping: - cloud: CloudStorageMapper(@cloudClient, 'my-bucket') -``` - -Utilisez-le comme n'importe quel autre mapper : - -```latte -{asset 'cloud:user-uploads/photo.jpg'} -``` - -La méthode `Helpers::createAssetFromUrl()` crée automatiquement le bon type d'asset d'après l'extension du fichier. - -Les types d'assets qui implémentent l'interface `Nette\Assets\HtmlRenderable` (images, scripts, styles, etc.) peuvent être rendus par `{asset}` comme un élément HTML complet. Les autres types de fichiers deviennent un `GenericAsset` (par exemple un PDF), qui ne peut pas être rendu comme élément HTML mais fournit tout de même une URL (et d'autres métadonnées). Tenter de rendre un tel asset comme élément HTML lève une `Nette\InvalidArgumentException` ; vous pouvez malgré tout utiliser son URL à l'intérieur d'un attribut. - - -Pour aller plus loin -==================== - -- [Nette Assets : enfin une API unifiée pour tout, des images à Vite |https://blog.nette.org/en/introducing-nette-assets] diff --git a/assets/fr/@left-menu.texy b/assets/fr/@left-menu.texy deleted file mode 100644 index 422281932a..0000000000 --- a/assets/fr/@left-menu.texy +++ /dev/null @@ -1,14 +0,0 @@ -Nette Assets -************ -- [Premiers pas |@home] -- [Vite |vite] -- [Configuration|configuration] - - -Pour aller plus loin -******************** -- [Documentation Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Bonnes pratiques |best-practices:] -- [Résolution de problèmes |nette:troubleshooting] diff --git a/assets/fr/@meta.texy b/assets/fr/@meta.texy deleted file mode 100644 index 72ae4b8db8..0000000000 --- a/assets/fr/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentation Nette}} diff --git a/assets/fr/configuration.texy b/assets/fr/configuration.texy deleted file mode 100644 index 78dbb74245..0000000000 --- a/assets/fr/configuration.texy +++ /dev/null @@ -1,195 +0,0 @@ -Configuration des assets -************************ - -.[perex] -Aperçu des options de configuration de Nette Assets. - - -```neon -assets: - # chemin de base pour résoudre les chemins relatifs des mappers - basePath: ... # (string) %wwwDir% par défaut - - # URL de base pour résoudre les URL relatives des mappers - baseUrl: ... # (string) %baseUrl% par défaut - - # activer le versionnage des assets globalement ? - versioning: ... # (bool) true par défaut - - # définit les mappers d'assets - mapping: ... # (array) chemin 'assets' par défaut -``` - -`basePath` définit le répertoire du système de fichiers par défaut pour résoudre les chemins relatifs des mappers. Par défaut, il utilise le répertoire web (`%wwwDir%`). - -`baseUrl` définit le préfixe d'URL par défaut pour résoudre les URL relatives des mappers. Par défaut, il utilise l'URL racine (`%baseUrl%`). - -L'option `versioning` détermine globalement si des paramètres de version sont ajoutés aux URL des assets pour invalider le cache. Chaque mapper peut redéfinir ce réglage. - - -Mappers -------- - -Les mappers peuvent être configurés de trois façons : notation simple sous forme de chaîne, notation détaillée sous forme de tableau, ou comme service (à l'aide de la notation entité `ClassName(...)` ou `@serviceName()`). - -La façon la plus simple de définir un mapper : - -```neon -assets: - mapping: - default: assets # Crée un mapper de système de fichiers pour %wwwDir%/assets/ - images: img # Crée un mapper de système de fichiers pour %wwwDir%/img/ - scripts: js # Crée un mapper de système de fichiers pour %wwwDir%/js/ -``` - -Chaque mapper crée un `FilesystemMapper` qui : -- cherche les fichiers dans `%wwwDir%/` -- génère des URL de la forme `%baseUrl%/` -- hérite du réglage global de versionnage - - -Pour plus de contrôle, utilisez la notation détaillée : - -```neon -assets: - mapping: - images: - # répertoire où les fichiers sont stockés - path: ... # (string) facultatif, chemin de base (basePath) par défaut - - # préfixe d'URL des liens générés - url: ... # (string) facultatif, path par défaut - - # activer le versionnage pour ce mapper ? - versioning: ... # (bool) facultatif, hérite du réglage global - - # ajouter automatiquement la ou les extensions lors de la recherche de fichiers - extension: ... # (string|array) facultatif, null par défaut -``` - -Comprendre comment les valeurs de configuration sont résolues : - -Résolution des chemins : - - les chemins relatifs sont résolus à partir de `basePath` (ou de `%wwwDir%` si `basePath` n'est pas défini) - - les chemins absolus sont utilisés tels quels - -Résolution des URL : - - les URL relatives sont résolues à partir de `baseUrl` (ou de `%baseUrl%` si `baseUrl` n'est pas défini) - - les URL absolues (avec un schéma ou `//`) sont utilisées telles quelles (une URL `//` reçoit son schéma de `baseUrl`) - - si `url` n'est pas indiquée, elle prend la valeur de `path` - - -```neon -assets: - basePath: /var/www/project/www - baseUrl: https://example.com/assets - - mapping: - # Chemin et URL relatifs - images: - path: img # Résolu en : /var/www/project/www/img - url: images # Résolu en : https://example.com/assets/images - - # Chemin et URL absolus - uploads: - path: /var/shared/uploads # Utilisé tel quel : /var/shared/uploads - url: https://cdn.example.com # Utilisé tel quel : https://cdn.example.com - - # Seul le chemin est indiqué - styles: - path: css # Chemin : /var/www/project/www/css - # URL : https://example.com/assets/css -``` - - -Mappers personnalisés ---------------------- - -Pour des mappers personnalisés, référencez un service existant avec `@serviceName`, ou définissez-le directement avec `ClassName(arguments)` ou un simple nom de classe : - -```neon -services: - s3mapper: App\Assets\S3Mapper(%s3.bucket%) - -assets: - mapping: - cloud: @s3mapper - database: App\Assets\DatabaseMapper(@database.connection) -``` - - -Mapper Vite ------------ - -Le mapper Vite exige seulement que vous ajoutiez `type: vite`. Voici la liste complète des options de configuration : - -```neon -assets: - mapping: - default: - # type de mapper (obligatoire pour Vite) - type: vite # (string) obligatoire, doit valoir 'vite' - - # répertoire de sortie du build Vite - path: ... # (string) facultatif, chemin de base (basePath) par défaut - - # préfixe d'URL des assets construits - url: ... # (string) facultatif, path par défaut - - # emplacement du fichier manifest de Vite - manifest: ... # (string) facultatif, relatif à path, /.vite/manifest.json par défaut - - # configuration du serveur de développement Vite - devServer: ... # (bool|string) facultatif, true par défaut - - # versionnage des fichiers du répertoire public - versioning: ... # (bool) facultatif, hérite du réglage global - - # extension automatique pour les fichiers du répertoire public - extension: ... # (string|array) facultatif, null par défaut -``` - -L'option `devServer` détermine la façon dont les assets sont chargés pendant le développement : - -- `true` (par défaut) - détecte automatiquement un serveur de développement Vite en cours d'exécution (via le fichier `.vite/nette.json` que le plugin Vite de Nette crée dans le répertoire de build). Si le serveur de développement tourne **et que votre application est en mode débogage**, les assets sont chargés depuis lui avec prise en charge du hot module replacement. Si le serveur de développement ne tourne pas, les assets sont chargés depuis les fichiers construits du répertoire public. -- `false` - désactive complètement l'intégration du serveur de développement. Les assets sont toujours chargés depuis les fichiers construits. -- URL personnalisée (par exemple `https://localhost:5173`) - indique manuellement l'URL du serveur de développement, protocole et port compris. Utile lorsque le serveur de développement tourne sur un autre hôte ou un autre port. Comme la détection automatique, cela ne s'applique qu'en mode débogage ; en production, ce sont toujours les fichiers construits qui sont utilisés. - -Les options `versioning` et `extension` ne s'appliquent qu'aux fichiers du répertoire public de Vite qui ne sont pas traités par Vite. - - -Configuration manuelle ----------------------- - -Quand vous n'utilisez pas Nette DI, configurez les mappers manuellement : - -```php -use Nette\Assets\Registry; -use Nette\Assets\FilesystemMapper; -use Nette\Assets\ViteMapper; - -$registry = new Registry; - -// Ajoute un mapper de système de fichiers -$registry->addMapper('images', new FilesystemMapper( - baseUrl: 'https://example.com/img', - basePath: __DIR__ . '/www/img', - extensions: ['webp', 'jpg', 'png'], - versioning: true, -)); - -// Ajoute un mapper Vite -$registry->addMapper('app', new ViteMapper( - baseUrl: '/build', - basePath: __DIR__ . '/www/build', - manifestPath: __DIR__ . '/www/build/.vite/manifest.json', - devServer: 'https://localhost:5173', -)); -``` - -Vous pouvez aussi récupérer n'importe quel mapper enregistré par son nom à l'aide de la méthode `getMapper()` : - -```php -$mapper = $registry->getMapper('images'); // renvoie le mapper enregistré -$default = $registry->getMapper(); // renvoie le mapper 'default' -``` diff --git a/assets/fr/vite.texy b/assets/fr/vite.texy deleted file mode 100644 index 9d6d874234..0000000000 --- a/assets/fr/vite.texy +++ /dev/null @@ -1,567 +0,0 @@ -Intégration de Vite -******************* - -
    - -Les applications JavaScript modernes exigent des outils de build élaborés. Nette Assets offre une intégration de premier ordre avec [Vite |https://vite.dev/], l'outil de build frontend de nouvelle génération. Profitez d'un développement ultra-rapide avec le Hot Module Replacement (HMR) et de builds de production optimisés, sans le moindre casse-tête de configuration. - -- **Zéro configuration** - pont automatique entre Vite et les templates PHP -- **Gestion complète des dépendances** - une seule balise s'occupe de tous les assets -- **Hot Module Replacement** - mises à jour instantanées du JavaScript et du CSS -- **Builds de production optimisés** - découpage du code et tree shaking - -
    - - -Nette Assets s'intègre parfaitement à Vite, si bien que vous profitez de tous ces avantages tout en écrivant vos templates comme d'habitude. - - -Mettre Vite en place -==================== - -Mettons Vite en place pas à pas. Ne vous inquiétez pas si les outils de build sont nouveaux pour vous : nous expliquerons tout ! - - -Étape 1 : installer Vite ------------------------- - -Installez d'abord Vite et le plugin Nette dans votre projet : - -```shell -npm install -D vite @nette/vite-plugin -``` - -Cela installe Vite et un plugin particulier qui l'aide à fonctionner parfaitement avec Nette. - - -Étape 2 : structure du projet ------------------------------ - -L'approche standard consiste à placer les fichiers sources des assets dans un dossier `assets/` à la racine du projet, et les versions compilées dans `www/assets/` : - -/--pre -web-project/ -├── assets/ ← fichiers sources (SCSS, TypeScript, images sources) -│ ├── public/ ← fichiers statiques (copiés tels quels) -│ │ └── favicon.ico -│ ├── images/ -│ │ └── logo.png -│ ├── app.js ← point d'entrée principal -│ └── style.css ← vos styles -└── www/ ← répertoire public (document root) - ├── assets/ ← les fichiers compilés arriveront ici - └── index.php -\-- - -Le dossier `assets/` contient vos fichiers sources, le code que vous écrivez. Vite traitera ces fichiers et placera les versions compilées dans `www/assets/`. - - -Étape 3 : configurer Vite -------------------------- - -Créez un fichier `vite.config.ts` à la racine de votre projet. Ce fichier indique à Vite où trouver vos fichiers sources et où placer les fichiers compilés. - -Le plugin Vite de Nette est livré avec des valeurs par défaut intelligentes qui simplifient la configuration. Il suppose que vos fichiers sources front-end sont dans le répertoire `assets/` (option `root`) et que les fichiers compilés vont dans `www/assets/` (option `outDir`). Vous n'avez qu'à indiquer le [point d'entrée|#Points d'entrée] : - -```js -import { defineConfig } from 'vite'; -import nette from '@nette/vite-plugin'; - -export default defineConfig({ - plugins: [ - nette({ - entry: 'app.js', - }), - ], -}); -``` - -Sous le capot, outre `root` et `outDir`, le plugin définit quelques autres options de Vite pour que tout s'emboîte : `base` à `''` (les assets sont servis directement depuis le document root), `build.manifest` à `true` (pour que Nette Assets puisse associer les noms de fichiers hachés) et `build.assetsDir` à `''` (les fichiers compilés atterrissent directement dans `outDir`, sans sous-dossier `static/`). Vous pouvez redéfinir chacune d'elles. - -.[note] -L'`outDir` par défaut (`www/assets`) exige que le répertoire `www/` existe déjà. Sinon, le plugin s'arrête avec l'erreur *"The output directory ... does not exist"*. - -Si vous voulez indiquer un autre nom de répertoire pour construire vos assets, vous devrez changer quelques options : - -```js -export default defineConfig({ - root: 'assets', // répertoire racine des assets sources - - build: { - outDir: '../www/assets', // où vont les fichiers compilés - }, - - // ... autre configuration ... -}); -``` - -.[note] -Le chemin `outDir` est considéré comme relatif à `root`, d'où le `../` au début. - - -Étape 4 : configurer Nette --------------------------- - -Parlez de Vite à Nette Assets dans votre `common.neon` : - -```neon -assets: - mapping: - default: - type: vite # indique à Nette d'utiliser le ViteMapper - path: assets -``` - - -Étape 5 : ajouter les scripts ------------------------------ - -Ajoutez ces scripts à votre `package.json` : - -```json -{ - "scripts": { - "dev": "vite", - "build": "vite build" - } -} -``` - -Vous pouvez désormais : -- `npm run dev` - démarrer le serveur de développement avec rechargement à chaud -- `npm run build` - créer les fichiers de production optimisés - - -Points d'entrée -=============== - -Un **point d'entrée** est le fichier principal par lequel votre application commence. Depuis ce fichier, vous importez d'autres fichiers (CSS, modules JavaScript, images), ce qui crée un arbre de dépendances. Vite suit ces imports et regroupe le tout. - -Exemple de point d'entrée `assets/app.js` : - -```js -// Import des styles -import './style.css' - -// Import des modules JavaScript -import netteForms from 'nette-forms'; -import naja from 'naja'; - -// Initialisation de votre application -netteForms.initOnLoad(); -naja.initialize(); -``` - -Dans le template, vous pouvez insérer un point d'entrée ainsi : - -```latte -{asset 'app.js'} -``` - -Nette Assets génère automatiquement toutes les balises HTML nécessaires - JavaScript, CSS et toutes les autres dépendances. - - -Plusieurs points d'entrée -------------------------- - -Les applications plus grandes ont souvent besoin de points d'entrée distincts : - -```js -export default defineConfig({ - plugins: [ - nette({ - entry: [ - 'app.js', // pages publiques - 'admin.js', // panneau d'administration - ], - }), - ], -}); -``` - -Utilisez-les dans différents templates : - -```latte -{* Dans les pages publiques *} -{asset 'app.js'} - -{* Dans le panneau d'administration *} -{asset 'admin.js'} -``` - - -Important : fichiers sources et fichiers compilés -------------------------------------------------- - -Il est essentiel de comprendre qu'en production vous ne pouvez charger que les fichiers que Vite met à disposition, soit par son manifest, soit en les copiant tels quels depuis le dossier public : - -1. **Les points d'entrée** définis dans `entry` (y compris les modules qu'ils importent dynamiquement) et **les assets référencés depuis le JavaScript ou le CSS** (images, polices, ...) - tous sont consignés dans le manifest -2. **Les fichiers du répertoire `assets/public/`** - ceux-là ne sont *pas* dans le manifest ; ils sont copiés tels quels et `{asset}` les localise par un repli sur le système de fichiers - -Vous **ne pouvez pas** charger avec `{asset}` n'importe quel fichier d'`assets/` : si un fichier n'est référencé nulle part, il ne sera pas compilé. Si vous voulez faire connaître d'autres assets à Vite, vous pouvez les déplacer dans le [dossier public|#Dossier public]. - -Notez que, par défaut, Vite intègre en ligne tous les assets de moins de 4 Ko, vous ne pourrez donc pas référencer ces fichiers directement. (Voir la [documentation de Vite |https://vite.dev/guide/assets.html]). - -```latte -{* ✓ Cela fonctionne - c'est un point d'entrée *} -{asset 'app.js'} - -{* ✓ Cela fonctionne - c'est dans assets/public/ *} -{asset 'favicon.ico'} - -{* ✗ Cela ne fonctionnera pas - fichier quelconque dans assets/ *} -{asset 'components/button.js'} -``` - - -Mode développement -================== - -Le mode développement est totalement facultatif, mais il apporte de gros avantages une fois activé. Le principal est le **Hot Module Replacement (HMR)** : vous voyez les changements instantanément sans perdre l'état de l'application, ce qui rend le développement bien plus fluide et rapide. - -Vite est un outil de build moderne qui rend le développement incroyablement rapide. Contrairement aux bundlers traditionnels, Vite sert votre code directement au navigateur pendant le développement, ce qui signifie un démarrage instantané du serveur quelle que soit la taille du projet et des mises à jour éclair. - - -Démarrer le serveur de développement ------------------------------------- - -Lancez le serveur de développement : - -```shell -npm run dev -``` - -Vous verrez : - -``` - ➜ Local: http://localhost:5173/ - ➜ Network: use --host to expose -``` - -Gardez ce terminal ouvert pendant le développement. - -Tant que le serveur de développement tourne, le plugin écrit un petit fichier signal `www/assets/.vite/nette.json` contenant son URL. Côté PHP, Nette Assets lit ce fichier et bascule sur le chargement depuis le serveur de développement lorsque les deux conditions suivantes sont remplies : -1. le serveur de développement Vite tourne (le fichier signal existe), et -2. votre application Nette est en mode débogage. - -Le résultat : - -```latte -{asset 'app.js'} -{* En développement : - *} -{* En production : *} -``` - -Aucune configuration n'est nécessaire, cela fonctionne tout seul ! Pour désactiver la détection ou indiquer manuellement l'URL du serveur de développement, voir l'[option devServer |configuration#Mapper Vite]. - -.[note] -Le fichier signal se trouve dans `www/assets/.vite/nette.json` (juste à côté du `manifest.json` de production). Vous pouvez le renommer avec l'option `infoFile` du plugin, qui vaut `.vite/nette.json` par défaut. Si Nette ne détecte pas le serveur de développement en cours, vérifiez que ce fichier existe et pointe vers la bonne URL. - - -Travailler sur des domaines différents --------------------------------------- - -Si votre serveur de développement tourne ailleurs que sur `localhost` (par exemple sur `myapp.local`), vous pouvez rencontrer des problèmes de CORS (Cross-Origin Resource Sharing). Le CORS est une fonction de sécurité des navigateurs web qui bloque par défaut les requêtes entre domaines différents. Quand votre application PHP tourne sur `myapp.local` mais que Vite tourne sur `localhost:5173`, le navigateur les voit comme des domaines différents et bloque les requêtes. - -Vous avez deux possibilités pour régler cela : - -**Option 1 : configurer le CORS** - -La solution la plus simple est d'autoriser les requêtes cross-origin venant de votre application PHP : - -```js -export default defineConfig({ - // ... autre configuration ... - - server: { - cors: { - origin: 'http://myapp.local', // l'URL de votre application PHP - }, - }, -}); -``` - -**Option 2 : faire tourner Vite sur votre domaine** - -L'autre solution est de faire tourner Vite sur le même domaine que votre application PHP. - -```js -export default defineConfig({ - // ... autre configuration ... - - server: { - host: 'myapp.local', // le même que votre application PHP - }, -}); -``` - -En réalité, même dans ce cas, vous devez configurer le CORS, car le serveur de développement tourne sur le même nom d'hôte mais sur un port différent. Ici, cependant, le CORS est configuré automatiquement par le plugin Vite de Nette. - - -Développement en HTTPS ----------------------- - -Si vous développez en HTTPS, vous avez besoin de certificats pour votre serveur de développement Vite. Le plus simple est d'utiliser un plugin qui génère les certificats automatiquement : - -```shell -npm install -D vite-plugin-mkcert -``` - -Voici comment le configurer dans `vite.config.ts` : - -```js -import mkcert from 'vite-plugin-mkcert'; - -export default defineConfig({ - // ... autre configuration ... - - plugins: [ - mkcert(), // génère les certificats automatiquement et active https - nette(), - ], -}); -``` - -Notez que si vous utilisez la configuration CORS (option 1 ci-dessus), vous devez mettre à jour l'URL d'origine pour utiliser `https://` au lieu de `http://`. - - -Développement avec Docker -------------------------- - -Quand vous faites tourner Vite dans un conteneur Docker, deux points demandent votre attention : le navigateur de votre machine doit pouvoir joindre le serveur de développement, et Vite doit détecter les changements de fichiers au travers de la frontière du conteneur. - -Commencez par [publier |https://docs.docker.com/get-started/docker-concepts/running-containers/publishing-ports/] le port de Vite depuis le conteneur et liez le serveur de développement à toutes les interfaces, afin qu'il soit joignable depuis l'extérieur du conteneur : - -```js -export default defineConfig({ - // ... autre configuration ... - - plugins: [ - nette(), - ], - server: { - host: '0.0.0.0', // écoute sur toutes les interfaces (nécessaire dans un conteneur) - port: 5173, // doit correspondre au port publié - strictPort: true, // échouer plutôt que choisir un autre port - watch: { - usePolling: true, // à activer si les changements de fichiers ne sont pas détectés sur les volumes montés - }, - }, -}); -``` - -Le plugin écrit l'URL du serveur de développement dans `nette.json` pour le côté PHP. Comme `host: '0.0.0.0'` n'est pas utilisable par un navigateur (il redirige vers `localhost` pour chaque asset), le plugin la réécrit automatiquement en `localhost` dans cette URL, afin que les assets se chargent correctement. - -Si vous ouvrez l'application sur un domaine personnalisé plutôt que sur `localhost`, définissez l'option `host` du plugin sur ce domaine : - -```js - plugins: [ - nette({ host: 'myapp.local' }), // le même domaine que votre application PHP - ], -``` - -Le plugin utilise alors cet hôte pour l'URL du serveur de développement, l'ajoute aux origines CORS et le met dans la liste blanche `allowedHosts` de Vite - cela fonctionne donc sans aucun réglage CORS manuel. - -Pour des installations plus complexes - par exemple quand Vite tourne derrière un reverse proxy où l'hôte public, le port et le protocole diffèrent tous de l'adresse interne - définissez `server.origin` de Vite sur l'URL publique complète. Le plugin la respecte et l'écrit telle quelle dans `nette.json`, au lieu de déduire l'URL du socket local : - -```js - server: { - origin: 'https://myapp.local:8443', // l'URL publique par laquelle le navigateur atteint Vite - }, -``` - - -Builds de production -==================== - -Créez les fichiers de production optimisés : - -```shell -npm run build -``` - -Vite va : -- minifier tout le JavaScript et le CSS -- découper le code en morceaux optimaux -- générer des noms de fichiers hachés pour invalider le cache -- créer un fichier manifest pour Nette Assets - -Exemple de sortie : - -``` -www/assets/ -├── app-4f3a2b1c.js # Votre JavaScript principal (minifié) -├── app-7d8e9f2a.css # CSS extrait (minifié) -├── vendor-8c4b5e6d.js # Dépendances partagées -└── .vite/ - └── manifest.json # Correspondances pour Nette Assets -``` - -Les noms de fichiers hachés garantissent que les navigateurs chargent toujours la dernière version. - - -Dossier public -============== - -Les fichiers du répertoire `assets/public/` sont copiés vers la sortie sans traitement : - -``` -assets/ -├── public/ -│ ├── favicon.ico -│ ├── robots.txt -│ └── images/ -│ └── og-image.jpg -├── app.js -└── style.css -``` - -Référencez-les normalement : - -```latte -{* Ces fichiers sont copiés tels quels *} - - -``` - -Pour les fichiers publics, vous pouvez utiliser les fonctionnalités de FilesystemMapper. L'option `extension` s'applique aux références sans extension (par exemple `{asset 'images/og-image'}` trouve d'abord `og-image.webp`) : - -```neon -assets: - mapping: - default: - type: vite - path: assets - extension: [webp, jpg, png] # pour les références sans extension - versioning: true # Ajoute l'invalidation du cache -``` - -Dans la configuration `vite.config.ts`, vous pouvez changer le dossier public à l'aide de l'option `publicDir`. - - -Imports dynamiques -================== - -Vite découpe automatiquement le code pour un chargement optimal. Les imports dynamiques vous permettent de ne charger du code que lorsqu'il est réellement nécessaire, ce qui réduit la taille du bundle initial : - -```js -// Charge les composants lourds à la demande -button.addEventListener('click', async () => { - let { Chart } = await import('./components/chart.js') - new Chart(data) -}) -``` - -Les imports dynamiques créent des chunks distincts qui ne sont chargés qu'au besoin. C'est ce qu'on appelle le "code splitting", l'une des fonctionnalités les plus puissantes de Vite. Quand vous utilisez des imports dynamiques, Vite crée automatiquement des fichiers JavaScript distincts pour chaque module importé dynamiquement. - -La balise `{asset 'app.js'}` ne précharge **pas** automatiquement ces chunks dynamiques. C'est un comportement voulu : nous ne voulons pas télécharger du code qui pourrait ne jamais servir. Les chunks ne sont téléchargés qu'au moment où l'import dynamique s'exécute. - -Si vous savez cependant que certains imports dynamiques sont critiques et seront bientôt nécessaires, vous pouvez les précharger : - -```latte -{* Point d'entrée principal *} -{asset 'app.js'} - -{* Précharge les imports dynamiques critiques *} -{preload 'components/chart.js'} -``` - -Cela dit au navigateur de télécharger le composant chart en arrière-plan, afin qu'il soit prêt dès qu'on en aura besoin. - - -Prise en charge de TypeScript -============================= - -TypeScript fonctionne immédiatement : - -```ts -// assets/main.ts -interface User { - name: string - email: string -} - -export function greetUser(user: User): void { - console.log(`Hello, ${user.name}!`) -} -``` - -Référencez les fichiers TypeScript normalement (comme pour tout fichier, `main.ts` doit être un point d'entrée) : - -```latte -{asset 'main.ts'} -``` - -Pour une prise en charge complète de TypeScript, installez-le : - -```shell -npm install -D typescript -``` - - -Configuration supplémentaire de Vite -==================================== - -Voici quelques options de configuration utiles de Vite, avec des explications détaillées : - -```js -export default defineConfig({ - // Répertoire racine contenant les assets sources - root: 'assets', - - // Dossier dont le contenu est copié tel quel vers le répertoire de sortie - // Par défaut : 'public' (relatif à 'root') - publicDir: 'public', - - build: { - // Où placer les fichiers compilés (relatif à 'root') - outDir: '../www/assets', - - // Vider le répertoire de sortie avant le build ? - // Utile pour supprimer les anciens fichiers des builds précédents - emptyOutDir: true, - - // Sous-répertoire d'outDir pour les chunks et assets générés - // Cela aide à organiser la structure de sortie - assetsDir: 'static', - - rollupOptions: { - // Point(s) d'entrée - un seul fichier ou un tableau de fichiers - // Chaque point d'entrée devient un bundle distinct - input: [ - 'app.js', // application principale - 'admin.js', // panneau d'administration - ], - }, - }, - - server: { - // Hôte auquel lier le serveur de développement - // Utilisez '0.0.0.0' pour l'exposer au réseau - host: 'localhost', - - // Port du serveur de développement - port: 5173, - - // Configuration CORS pour les requêtes cross-origin - cors: { - origin: 'http://myapp.local', - }, - }, - - css: { - // Activer les source maps CSS en développement - devSourcemap: true, - }, - - plugins: [ - nette(), - ], -}); -``` - -.[warning] -Attention aux chemins des points d'entrée dans `rollupOptions.input` ci-dessus : Rollup résout les chemins relatifs par rapport au répertoire de travail courant (la racine de votre projet), et **non** par rapport à `root: 'assets'`. Un simple `'app.js'` n'existera donc pas au moment du build. Utilisez soit l'option `entry` du plugin (qui résout les chemins des points d'entrée relativement à `root`), soit écrivez les chemins relativement à la racine du projet, par exemple `'assets/app.js'`. - -C'est tout ! Vous disposez maintenant d'un système de build moderne intégré à Nette Assets. diff --git a/assets/it/@home.texy b/assets/it/@home.texy deleted file mode 100644 index 539dd5e9f8..0000000000 --- a/assets/it/@home.texy +++ /dev/null @@ -1,467 +0,0 @@ -Nette Assets -************ - -
    - -Stanchi di gestire a mano i file statici nelle vostre applicazioni web? Dimenticate i percorsi scritti a mano, l'invalidazione della cache e le preoccupazioni sul versionamento dei file. Nette Assets trasforma il modo in cui lavorate con immagini, fogli di stile, script e altre risorse statiche. - -- Il **versionamento intelligente** garantisce che i browser carichino sempre i file più recenti -- **Rilevamento automatico** dei tipi di file e delle dimensioni -- **Integrazione fluida con Latte** grazie a tag intuitivi -- **Architettura flessibile** che supporta filesystem, CDN e Vite -- **Caricamento pigro** per prestazioni ottimali - -
    - - -Perché Nette Assets? -==================== - -Lavorare con i file statici significa spesso codice ripetitivo e soggetto a errori. Costruite gli URL a mano, aggiungete i parametri di versione per invalidare la cache e trattate in modo diverso i vari tipi di file. Il che porta a codice come questo: - -```latte -Logo - -``` - -Con Nette Assets tutta questa complessità sparisce: - -```latte -{* tutto automatizzato: URL, versionamento, dimensioni *} - - - -{* oppure solo *} -{asset 'css/style.css'} -``` - -Ecco fatto! La libreria automaticamente: -- aggiunge i parametri di versione in base all'ora di modifica del file -- rileva le dimensioni delle immagini e le inserisce nell'HTML -- genera l'elemento HTML corretto per ogni tipo di file -- gestisce sia l'ambiente di sviluppo sia quello di produzione - - -Installazione -============= - -Installate Nette Assets con [Composer|best-practices:composer]: - -```shell -composer require nette/assets -``` - -Richiede PHP 8.1 o superiore e funziona perfettamente con il Nette Framework, ma si può usare anche da solo. - - -Primi passi -=========== - -Nette Assets funziona subito senza alcuna configurazione. Mettete i vostri file statici nella directory `www/assets/` e cominciate a usarli: - -```latte -{* mostra un'immagine con le dimensioni automatiche *} -{asset 'logo.png'} - -{* include un foglio di stile con il versionamento *} -{asset 'style.css'} - -{* carica uno script *} -{asset 'app.js'} -``` - -Per un maggiore controllo sull'HTML generato usate l'attributo `n:asset` oppure la funzione `asset()`. - - -Come funziona -============= - -Nette Assets è costruito attorno a tre concetti fondamentali che lo rendono potente e allo stesso tempo semplice da usare: - - -Asset: i vostri file resi intelligenti --------------------------------------- - -Un **asset** rappresenta qualsiasi file statico della vostra applicazione. Ogni file diventa un oggetto con utili proprietà readonly: - -```php -$image = $assets->getAsset('photo.jpg'); -echo $image->url; // '/assets/photo.jpg?v=1699123456' -echo $image->file; // '/var/www/assets/photo.jpg' (percorso locale, oppure null) -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' -``` - -Tipi di file diversi offrono proprietà diverse: -- **Immagini**: larghezza, altezza, testo alternativo, caricamento pigro -- **Script**: tipo di modulo, hash di integrità, crossorigin -- **Fogli di stile**: media query, integrità -- **Audio/video**: durata, dimensioni (solo video) -- **Font**: preloading corretto con CORS - -La libreria rileva automaticamente i tipi di file e crea la classe di asset appropriata. - - -Mapper: da dove vengono i file ------------------------------- - -Un **mapper** sa come trovare i file e creare gli URL per essi. Potete avere più mapper per scopi diversi: file locali, CDN, storage cloud o strumenti di build (ognuno ha un nome). Il `FilesystemMapper` integrato si occupa dei file locali, mentre `ViteMapper` si integra con i moderni strumenti di build. - -I mapper si definiscono nella [configurazione |configuration]. - - -Registry: la vostra interfaccia principale ------------------------------------------- - -Il **registry** gestisce tutti i mapper e offre l'API principale: - -```php -// fatevi iniettare il registry nel vostro servizio -public function __construct( - private Nette\Assets\Registry $assets -) {} -``` - -```php -// ottenete gli asset dai vari mapper -$logo = $this->assets->getAsset('images:logo.png'); // mapper 'images' -$app = $this->assets->getAsset('app:main.js'); // mapper 'app' -$style = $this->assets->getAsset('style.css'); // usa il mapper predefinito -``` - -Il registry sceglie automaticamente il mapper giusto e mette in cache i risultati per le prestazioni. - - -Lavorare con gli asset in PHP -============================= - -Il Registry offre due metodi per ottenere gli asset: - -```php -// lancia Nette\Assets\AssetNotFoundException se il file non esiste -$logo = $assets->getAsset('logo.png'); - -// restituisce null se il file non esiste -$banner = $assets->tryGetAsset('banner.jpg'); -if ($banner) { - echo $banner->url; -} -``` - - -Indicare i mapper ------------------ - -Potete scegliere esplicitamente quale mapper usare: - -```php -// usa il mapper predefinito -$file = $assets->getAsset('document.pdf'); - -// usa un mapper specifico con il prefisso -$image = $assets->getAsset('images:photo.jpg'); - -// usa un mapper specifico con la sintassi ad array -$script = $assets->getAsset(['scripts', 'app.js']); -``` - - -Proprietà e tipi degli asset ----------------------------- - -Ogni tipo di asset offre le proprietà readonly pertinenti: - -```php -// proprietà di un'immagine -$image = $assets->getAsset('photo.jpg'); -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' - -// proprietà di uno script -$script = $assets->getAsset('app.js'); -echo $script->type; // null ('module' per i punti di ingresso di Vite) - -// proprietà di un audio -$audio = $assets->getAsset('song.mp3'); -echo $audio->duration; // durata in secondi - -// tutti gli asset si possono convertire in stringa (restituisce l'URL) -$url = (string) $assets->getAsset('document.pdf'); -``` - -.[note] -Proprietà come le dimensioni o la durata vengono caricate pigramente solo quando vi si accede, il che mantiene la libreria veloce. - -.[tip] -Per un'analisi statica precisa installate l'estensione [nette/phpstan-rules |tools:phpstan-rules#Assets]. PHPStan conosce allora il tipo concreto di ogni asset, quindi `getAsset('photo.jpg')` viene inteso come `ImageAsset` e l'accesso a `->width` non provoca alcun errore. - - -Usare gli asset nei template Latte -================================== - -Nette Assets offre un'integrazione intuitiva con [Latte|latte:] tramite tag e funzioni. - - -`{asset}` ---------- - -Il tag `{asset}` renderizza elementi HTML completi: - -```latte -{* renderizza: *} -{asset 'hero.jpg'} - -{* renderizza: *} -{asset 'app.js'} - -{* renderizza: *} -{asset 'style.css'} -``` - -Il tag automaticamente: -- rileva il tipo di asset e genera l'HTML appropriato -- include il versionamento per invalidare la cache -- aggiunge le dimensioni per le immagini -- imposta gli attributi corretti (type, media ecc.) - -Quando si usa dentro un attributo HTML oppure dentro gli elementi ` -``` - - -Mapper personalizzati -===================== - -Create mapper personalizzati per esigenze particolari, come lo storage cloud o la generazione dinamica: - -```php -use Nette\Assets\Mapper; -use Nette\Assets\Asset; -use Nette\Assets\Helpers; - -class CloudStorageMapper implements Mapper -{ - public function __construct( - private CloudClient $client, - private string $bucket, - ) {} - - public function getAsset(string $reference, array $options = []): Asset - { - if (!$this->client->exists($this->bucket, $reference)) { - throw new Nette\Assets\AssetNotFoundException("Asset '$reference' not found"); - } - - $url = $this->client->getPublicUrl($this->bucket, $reference); - return Helpers::createAssetFromUrl($url); - } -} -``` - -Registratelo nella configurazione: - -```neon -assets: - mapping: - cloud: CloudStorageMapper(@cloudClient, 'my-bucket') -``` - -Usatelo come qualsiasi altro mapper: - -```latte -{asset 'cloud:user-uploads/photo.jpg'} -``` - -Il metodo `Helpers::createAssetFromUrl()` crea automaticamente il tipo di asset corretto in base all'estensione del file. - -I tipi di asset che implementano l'interfaccia `Nette\Assets\HtmlRenderable` (immagini, script, stili e così via) possono essere renderizzati da `{asset}` come elemento HTML completo. Gli altri tipi di file diventano un `GenericAsset` (per esempio un PDF), che non si può renderizzare come elemento HTML ma offre comunque un URL (e altri metadati). Provare a renderizzare un asset del genere come elemento HTML lancia `Nette\InvalidArgumentException`; potete comunque usarne l'URL dentro un attributo. - - -Letture consigliate -=================== - -- [Nette Assets: Finally unified API for everything from images to Vite |https://blog.nette.org/en/introducing-nette-assets] diff --git a/assets/it/@left-menu.texy b/assets/it/@left-menu.texy deleted file mode 100644 index c2bf6ba226..0000000000 --- a/assets/it/@left-menu.texy +++ /dev/null @@ -1,14 +0,0 @@ -Nette Assets -************ -- [Primi passi |@home] -- [Vite |vite] -- [Configurazione|configuration] - - -Letture consigliate -******************* -- [Documentazione di Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Best practice |best-practices:] -- [Risoluzione dei problemi |nette:troubleshooting] diff --git a/assets/it/@meta.texy b/assets/it/@meta.texy deleted file mode 100644 index 4647d0c8a2..0000000000 --- a/assets/it/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentazione Nette}} diff --git a/assets/it/configuration.texy b/assets/it/configuration.texy deleted file mode 100644 index 8baf8e9747..0000000000 --- a/assets/it/configuration.texy +++ /dev/null @@ -1,195 +0,0 @@ -Configurazione di Assets -************************ - -.[perex] -Panoramica delle opzioni di configurazione di Nette Assets. - - -```neon -assets: - # percorso base per risolvere i percorsi relativi dei mapper - basePath: ... # (string) predefinito %wwwDir% - - # URL base per risolvere gli URL relativi dei mapper - baseUrl: ... # (string) predefinito %baseUrl% - - # attivare globalmente il versionamento degli asset? - versioning: ... # (bool) predefinito true - - # definisce i mapper degli asset - mapping: ... # (array) predefinito il percorso 'assets' -``` - -`basePath` imposta la directory predefinita del filesystem per risolvere i percorsi relativi nei mapper. Per impostazione predefinita usa la directory web (`%wwwDir%`). - -`baseUrl` imposta il prefisso URL predefinito per risolvere gli URL relativi nei mapper. Per impostazione predefinita usa l'URL radice (`%baseUrl%`). - -L'opzione `versioning` governa globalmente se agli URL degli asset vengano aggiunti i parametri di versione per invalidare la cache. I singoli mapper possono sovrascrivere questa impostazione. - - -Mapper ------- - -I mapper si possono configurare in tre modi: con la semplice notazione a stringa, con la notazione dettagliata ad array, oppure come servizio (usando la notazione a entità `ClassName(...)` oppure `@serviceName()`). - -Il modo più semplice di definire un mapper: - -```neon -assets: - mapping: - default: assets # crea un mapper del filesystem per %wwwDir%/assets/ - images: img # crea un mapper del filesystem per %wwwDir%/img/ - scripts: js # crea un mapper del filesystem per %wwwDir%/js/ -``` - -Ogni mapper crea un `FilesystemMapper` che: -- cerca i file in `%wwwDir%/` -- genera URL del tipo `%baseUrl%/` -- eredita l'impostazione globale del versionamento - - -Per un controllo maggiore usate la notazione dettagliata: - -```neon -assets: - mapping: - images: - # directory in cui sono salvati i file - path: ... # (string) facoltativo, predefinito il percorso base (basePath) - - # prefisso URL per i link generati - url: ... # (string) facoltativo, predefinito il valore di path - - # attivare il versionamento per questo mapper? - versioning: ... # (bool) facoltativo, eredita l'impostazione globale - - # aggiunge automaticamente l'estensione (o le estensioni) quando cerca i file - extension: ... # (string|array) facoltativo, predefinito null -``` - -Come vengono risolti i valori di configurazione: - -Risoluzione del percorso: - - I percorsi relativi vengono risolti a partire da `basePath` (oppure da `%wwwDir%` se `basePath` non è impostato) - - I percorsi assoluti vengono usati così come sono - -Risoluzione dell'URL: - - Gli URL relativi vengono risolti a partire da `baseUrl` (oppure da `%baseUrl%` se `baseUrl` non è impostato) - - Gli URL assoluti (con schema oppure `//`) vengono usati così come sono (un URL `//` prende lo schema da `baseUrl`) - - Se `url` non è indicato, viene usato il valore di `path` - - -```neon -assets: - basePath: /var/www/project/www - baseUrl: https://example.com/assets - - mapping: - # percorso e URL relativi - images: - path: img # risolto in: /var/www/project/www/img - url: images # risolto in: https://example.com/assets/images - - # percorso e URL assoluti - uploads: - path: /var/shared/uploads # usato così: /var/shared/uploads - url: https://cdn.example.com # usato così: https://cdn.example.com - - # indicato solo il percorso - styles: - path: css # percorso: /var/www/project/www/css - # URL: https://example.com/assets/css -``` - - -Mapper personalizzati ---------------------- - -Per i mapper personalizzati fate riferimento a un servizio esistente con `@serviceName`, oppure definiteli direttamente con `ClassName(argomenti)` o con il semplice nome della classe: - -```neon -services: - s3mapper: App\Assets\S3Mapper(%s3.bucket%) - -assets: - mapping: - cloud: @s3mapper - database: App\Assets\DatabaseMapper(@database.connection) -``` - - -Mapper Vite ------------ - -Il mapper Vite richiede solo che aggiungiate `type: vite`. Questo è l'elenco completo delle opzioni di configurazione: - -```neon -assets: - mapping: - default: - # tipo di mapper (obbligatorio per Vite) - type: vite # (string) obbligatorio, deve essere 'vite' - - # directory di output della build di Vite - path: ... # (string) facoltativo, predefinito il percorso base (basePath) - - # prefisso URL per gli asset compilati - url: ... # (string) facoltativo, predefinito il valore di path - - # posizione del file manifest di Vite - manifest: ... # (string) facoltativo, relativo a path, predefinito /.vite/manifest.json - - # configurazione del dev server di Vite - devServer: ... # (bool|string) facoltativo, predefinito true - - # versionamento per i file della directory public - versioning: ... # (bool) facoltativo, eredita l'impostazione globale - - # estensione automatica per i file della directory public - extension: ... # (string|array) facoltativo, predefinito null -``` - -L'opzione `devServer` governa come vengono caricati gli asset durante lo sviluppo: - -- `true` (predefinito) - rileva automaticamente un dev server di Vite in esecuzione (tramite il file `.vite/nette.json` che il plugin Vite di Nette crea nella directory di build). Se il dev server è in esecuzione **e la vostra applicazione è in modalità debug**, gli asset vengono caricati da esso con il supporto all'hot module replacement. Se il dev server non è in esecuzione, gli asset vengono caricati dai file compilati nella directory pubblica. -- `false` - disattiva completamente l'integrazione con il dev server. Gli asset vengono sempre caricati dai file compilati. -- URL personalizzato (per esempio `https://localhost:5173`) - indicate a mano l'URL del dev server, protocollo e porta compresi. Torna utile quando il dev server gira su un host o una porta diversi. Come il rilevamento automatico, vale solo in modalità debug; in produzione si usano sempre i file compilati. - -Le opzioni `versioning` ed `extension` valgono solo per i file della directory public di Vite che non vengono elaborati da Vite. - - -Configurazione manuale ----------------------- - -Quando non usate Nette DI, configurate i mapper a mano: - -```php -use Nette\Assets\Registry; -use Nette\Assets\FilesystemMapper; -use Nette\Assets\ViteMapper; - -$registry = new Registry; - -// aggiunta del mapper del filesystem -$registry->addMapper('images', new FilesystemMapper( - baseUrl: 'https://example.com/img', - basePath: __DIR__ . '/www/img', - extensions: ['webp', 'jpg', 'png'], - versioning: true, -)); - -// aggiunta del mapper Vite -$registry->addMapper('app', new ViteMapper( - baseUrl: '/build', - basePath: __DIR__ . '/www/build', - manifestPath: __DIR__ . '/www/build/.vite/manifest.json', - devServer: 'https://localhost:5173', -)); -``` - -Potete anche ottenere qualsiasi mapper registrato in base al nome con il metodo `getMapper()`: - -```php -$mapper = $registry->getMapper('images'); // restituisce il mapper registrato -$default = $registry->getMapper(); // restituisce il mapper 'default' -``` diff --git a/assets/it/vite.texy b/assets/it/vite.texy deleted file mode 100644 index 4922e397df..0000000000 --- a/assets/it/vite.texy +++ /dev/null @@ -1,567 +0,0 @@ -Integrazione con Vite -********************* - -
    - -Le applicazioni JavaScript moderne richiedono strumenti di build sofisticati. Nette Assets offre un'integrazione di prima classe con [Vite |https://vite.dev/], lo strumento di build frontend di nuova generazione. Otterrete uno sviluppo fulmineo con l'Hot Module Replacement (HMR) e build di produzione ottimizzate senza grattacapi di configurazione. - -- **Zero configurazione** - ponte automatico tra Vite e i template PHP -- **Gestione completa delle dipendenze** - un solo tag si occupa di tutti gli asset -- **Hot Module Replacement** - aggiornamenti istantanei di JavaScript e CSS -- **Build di produzione ottimizzate** - code splitting e tree shaking - -
    - - -Nette Assets si integra in modo fluido con Vite, così ottenete tutti questi vantaggi scrivendo i vostri template come al solito. - - -Impostare Vite -============== - -Impostiamo Vite passo passo. Non preoccupatevi se gli strumenti di build sono una novità per voi, spiegheremo tutto! - - -Passo 1: installare Vite ------------------------- - -Per prima cosa installate nel vostro progetto Vite e il plugin di Nette: - -```shell -npm install -D vite @nette/vite-plugin -``` - -Questo installa Vite e uno speciale plugin che aiuta Vite a funzionare perfettamente con Nette. - - -Passo 2: struttura del progetto -------------------------------- - -L'approccio standard è mettere i file sorgente degli asset in una cartella `assets/` nella radice del progetto e le versioni compilate in `www/assets/`: - -/--pre -web-project/ -├── assets/ ← file sorgente (SCSS, TypeScript, immagini sorgente) -│ ├── public/ ← file statici (copiati così come sono) -│ │ └── favicon.ico -│ ├── images/ -│ │ └── logo.png -│ ├── app.js ← punto di ingresso principale -│ └── style.css ← i vostri stili -└── www/ ← directory pubblica (document root) - ├── assets/ ← qui finiranno i file compilati - └── index.php -\-- - -La cartella `assets/` contiene i vostri file sorgente, il codice che scrivete. Vite elaborerà questi file e metterà le versioni compilate in `www/assets/`. - - -Passo 3: configurare Vite -------------------------- - -Create nella radice del progetto un file `vite.config.ts`. Questo file dice a Vite dove trovare i vostri file sorgente e dove mettere quelli compilati. - -Il plugin Vite di Nette arriva con valori predefiniti intelligenti che rendono semplice la configurazione. Presuppone che i vostri file sorgente frontend siano nella directory `assets/` (opzione `root`) e che i file compilati vadano in `www/assets/` (opzione `outDir`). Dovete solo indicare il [punto di ingresso|#Punti di ingresso]: - -```js -import { defineConfig } from 'vite'; -import nette from '@nette/vite-plugin'; - -export default defineConfig({ - plugins: [ - nette({ - entry: 'app.js', - }), - ], -}); -``` - -Sotto il cofano, oltre a `root` e `outDir`, il plugin imposta qualche altra opzione di Vite perché tutto combaci: `base` a `''` (gli asset vengono serviti direttamente dal document root), `build.manifest` a `true` (così Nette Assets può mappare i nomi di file con hash) e `build.assetsDir` a `''` (i file compilati finiscono direttamente in `outDir`, senza una sottocartella `static/`). Potete sovrascrivere qualsiasi di esse. - -.[note] -L'`outDir` predefinito (`www/assets`) richiede che la directory `www/` esista già. In caso contrario il plugin si ferma con l'errore *"The output directory ... does not exist"*. - -Se volete indicare un altro nome di directory in cui compilare i vostri asset, dovrete cambiare qualche opzione: - -```js -export default defineConfig({ - root: 'assets', // directory radice degli asset sorgente - - build: { - outDir: '../www/assets', // dove finiscono i file compilati - }, - - // ... altra configurazione ... -}); -``` - -.[note] -Il percorso `outDir` è considerato relativo a `root`, ed è per questo che all'inizio c'è `../`. - - -Passo 4: configurare Nette --------------------------- - -Parlate di Vite a Nette Assets nel vostro `common.neon`: - -```neon -assets: - mapping: - default: - type: vite # dice a Nette di usare il ViteMapper - path: assets -``` - - -Passo 5: aggiungere gli script ------------------------------- - -Aggiungete questi script al vostro `package.json`: - -```json -{ - "scripts": { - "dev": "vite", - "build": "vite build" - } -} -``` - -Ora potete: -- `npm run dev` - avviare il server di sviluppo con il ricaricamento a caldo -- `npm run build` - creare i file di produzione ottimizzati - - -Punti di ingresso -================= - -Un **punto di ingresso** è il file principale da cui parte la vostra applicazione. Da questo file importate altri file (CSS, moduli JavaScript, immagini), creando un albero di dipendenze. Vite segue questi import e mette insieme tutto. - -Esempio di punto di ingresso `assets/app.js`: - -```js -// import degli stili -import './style.css' - -// import dei moduli JavaScript -import netteForms from 'nette-forms'; -import naja from 'naja'; - -// inizializzazione della vostra applicazione -netteForms.initOnLoad(); -naja.initialize(); -``` - -Nel template potete inserire un punto di ingresso così: - -```latte -{asset 'app.js'} -``` - -Nette Assets genera automaticamente tutti i tag HTML necessari: JavaScript, CSS e le eventuali altre dipendenze. - - -Più punti di ingresso ---------------------- - -Le applicazioni più grandi hanno spesso bisogno di punti di ingresso separati: - -```js -export default defineConfig({ - plugins: [ - nette({ - entry: [ - 'app.js', // pagine pubbliche - 'admin.js', // pannello di amministrazione - ], - }), - ], -}); -``` - -Usateli in template diversi: - -```latte -{* nelle pagine pubbliche *} -{asset 'app.js'} - -{* nel pannello di amministrazione *} -{asset 'admin.js'} -``` - - -Importante: file sorgente e file compilati ------------------------------------------- - -È fondamentale capire che in produzione potete caricare solo i file che Vite rende disponibili, o tramite il suo manifest, oppure copiandoli invariati dalla cartella public: - -1. **I punti di ingresso** definiti in `entry` (compresi i moduli che importano dinamicamente) e gli **asset referenziati da JavaScript o CSS** (immagini, font, ...): tutti questi vengono registrati nel manifest -2. **I file della directory `assets/public/`**: questi *non* sono nel manifest; vengono copiati così come sono e `{asset}` li trova tramite un ripiego sul filesystem - -**Non** potete caricare con `{asset}` file arbitrari da `assets/`: se un file non è referenziato da nessuna parte, non verrà compilato. Se volete far conoscere a Vite altri asset, potete spostarli nella [cartella public |#Cartella public]. - -Tenete presente che per impostazione predefinita Vite inserisce inline tutti gli asset più piccoli di 4 KB, quindi non potrete referenziare direttamente questi file. (Vedi la [documentazione di Vite |https://vite.dev/guide/assets.html]). - -```latte -{* ✓ questo funziona, è un punto di ingresso *} -{asset 'app.js'} - -{* ✓ questo funziona, è in assets/public/ *} -{asset 'favicon.ico'} - -{* ✗ questo non funziona, è un file qualsiasi in assets/ *} -{asset 'components/button.js'} -``` - - -Modalità di sviluppo -==================== - -La modalità di sviluppo è del tutto facoltativa, ma quando è attiva offre vantaggi notevoli. Il principale è l'**Hot Module Replacement (HMR)**: vedete le modifiche all'istante senza perdere lo stato dell'applicazione, il che rende lo sviluppo molto più fluido e veloce. - -Vite è uno strumento di build moderno che rende lo sviluppo incredibilmente veloce. A differenza dei bundler tradizionali, durante lo sviluppo Vite serve il vostro codice direttamente al browser, il che significa avvio istantaneo del server per quanto grande sia il progetto e aggiornamenti fulminei. - - -Avviare il server di sviluppo ------------------------------ - -Lanciate il server di sviluppo: - -```shell -npm run dev -``` - -Vedrete: - -``` - ➜ Local: http://localhost:5173/ - ➜ Network: use --host to expose -``` - -Tenete aperto questo terminale mentre sviluppate. - -Mentre il dev server è in esecuzione, il plugin scrive un piccolo file di segnalazione `www/assets/.vite/nette.json` che contiene il suo URL. Dal lato PHP Nette Assets legge questo file e passa a caricare dal dev server quando entrambe le condizioni sono vere: -1. il dev server di Vite è in esecuzione (il file di segnalazione esiste) e -2. la vostra applicazione Nette è in modalità debug. - -Il risultato: - -```latte -{asset 'app.js'} -{* in sviluppo: - *} -{* in produzione: *} -``` - -Non serve alcuna configurazione, funziona e basta! Per disattivare il rilevamento o impostare a mano l'URL del dev server, vedi l'[opzione devServer |configuration#Mapper Vite]. - -.[note] -Il file di segnalazione sta in `www/assets/.vite/nette.json` (proprio accanto al `manifest.json` di produzione). Lo potete rinominare con l'opzione `infoFile` del plugin, che vale `.vite/nette.json` per impostazione predefinita. Se Nette non rileva il dev server in esecuzione, controllate che questo file esista e punti all'URL giusto. - - -Lavorare su domini diversi --------------------------- - -Se il vostro server di sviluppo gira su qualcosa di diverso da `localhost` (per esempio `myapp.local`), potreste incontrare problemi di CORS (Cross-Origin Resource Sharing). Il CORS è una funzione di sicurezza dei browser web che per impostazione predefinita blocca le richieste tra domini diversi. Quando la vostra applicazione PHP gira su `myapp.local` ma Vite gira su `localhost:5173`, il browser li vede come domini diversi e blocca le richieste. - -Avete due possibilità per risolverlo: - -**Possibilità 1: configurare il CORS** - -La soluzione più semplice è consentire le richieste cross-origin dalla vostra applicazione PHP: - -```js -export default defineConfig({ - // ... altra configurazione ... - - server: { - cors: { - origin: 'http://myapp.local', // l'URL della vostra app PHP - }, - }, -}); -``` - -**Possibilità 2: far girare Vite sul vostro dominio** - -L'altra soluzione è far girare Vite sullo stesso dominio della vostra applicazione PHP. - -```js -export default defineConfig({ - // ... altra configurazione ... - - server: { - host: 'myapp.local', // uguale alla vostra app PHP - }, -}); -``` - -In realtà, anche in questo caso dovete configurare il CORS, perché il dev server gira sullo stesso hostname ma su una porta diversa. In questo caso però il CORS viene configurato automaticamente dal plugin Vite di Nette. - - -Sviluppo in HTTPS ------------------ - -Se sviluppate in HTTPS, avete bisogno di certificati per il vostro server di sviluppo Vite. Il modo più semplice è usare un plugin che genera i certificati automaticamente: - -```shell -npm install -D vite-plugin-mkcert -``` - -Ecco come configurarlo in `vite.config.ts`: - -```js -import mkcert from 'vite-plugin-mkcert'; - -export default defineConfig({ - // ... altra configurazione ... - - plugins: [ - mkcert(), // genera automaticamente i certificati e attiva https - nette(), - ], -}); -``` - -Notate che, se usate la configurazione CORS (Possibilità 1 di sopra), dovete aggiornare l'URL di origine perché usi `https://` invece di `http://`. - - -Sviluppo in Docker ------------------- - -Quando fate girare Vite dentro un container Docker, due cose richiedono attenzione: il browser sulla vostra macchina deve poter raggiungere il dev server e Vite deve rilevare le modifiche ai file attraverso il confine del container. - -Per prima cosa [pubblicate |https://docs.docker.com/get-started/docker-concepts/running-containers/publishing-ports/] la porta di Vite dal container e legate il dev server a tutte le interfacce, così è raggiungibile da fuori: - -```js -export default defineConfig({ - // ... altra configurazione ... - - plugins: [ - nette(), - ], - server: { - host: '0.0.0.0', // ascolta su tutte le interfacce (necessario in un container) - port: 5173, // deve corrispondere alla porta pubblicata - strictPort: true, // meglio fallire che scegliere un'altra porta - watch: { - usePolling: true, // attivate se le modifiche ai file non vengono rilevate sui volumi montati - }, - }, -}); -``` - -Il plugin scrive l'URL del dev server in `nette.json` per il lato PHP. Poiché `host: '0.0.0.0'` non è utilizzabile da un browser (per ogni asset reindirizza a `localhost`), il plugin lo riscrive automaticamente in `localhost` dentro quell'URL, così gli asset si caricano correttamente. - -Se aprite l'applicazione su un dominio personalizzato invece che su `localhost`, impostate l'opzione `host` del plugin su quel dominio: - -```js - plugins: [ - nette({ host: 'myapp.local' }), // lo stesso dominio della vostra app PHP - ], -``` - -Il plugin usa allora questo host per l'URL del dev server, lo aggiunge alle origini CORS e lo inserisce nella whitelist `allowedHosts` di Vite, così funziona senza alcuna configurazione CORS manuale. - -Per configurazioni più complesse, per esempio quando Vite gira dietro a un reverse proxy dove host pubblico, porta e protocollo differiscono tutti dall'indirizzo interno, impostate `server.origin` di Vite sull'URL pubblico completo. Il plugin lo rispetta e lo scrive in `nette.json` così com'è, invece di ricavare l'URL dal socket locale: - -```js - server: { - origin: 'https://myapp.local:8443', // l'URL pubblico da cui il browser raggiunge Vite - }, -``` - - -Build di produzione -=================== - -Create i file di produzione ottimizzati: - -```shell -npm run build -``` - -Vite: -- minimizzerà tutto il JavaScript e il CSS -- dividerà il codice in chunk ottimali -- genererà nomi di file con hash per invalidare la cache -- creerà un file manifest per Nette Assets - -Esempio di output: - -``` -www/assets/ -├── app-4f3a2b1c.js # il vostro JavaScript principale (minimizzato) -├── app-7d8e9f2a.css # CSS estratto (minimizzato) -├── vendor-8c4b5e6d.js # dipendenze condivise -└── .vite/ - └── manifest.json # mappatura per Nette Assets -``` - -I nomi di file con hash garantiscono che i browser carichino sempre la versione più recente. - - -Cartella public -=============== - -I file della directory `assets/public/` vengono copiati nell'output senza essere elaborati: - -``` -assets/ -├── public/ -│ ├── favicon.ico -│ ├── robots.txt -│ └── images/ -│ └── og-image.jpg -├── app.js -└── style.css -``` - -Referenziateli normalmente: - -```latte -{* questi file vengono copiati così come sono *} - - -``` - -Per i file public potete usare le funzionalità di FilesystemMapper. L'opzione `extension` si applica ai riferimenti senza estensione (per esempio `{asset 'images/og-image'}` trova prima `og-image.webp`): - -```neon -assets: - mapping: - default: - type: vite - path: assets - extension: [webp, jpg, png] # per i riferimenti senza estensione - versioning: true # aggiunge l'invalidazione della cache -``` - -Nella configurazione di `vite.config.ts` potete cambiare la cartella public con l'opzione `publicDir`. - - -Import dinamici -=============== - -Vite divide automaticamente il codice per un caricamento ottimale. Gli import dinamici vi permettono di caricare il codice solo quando serve davvero, riducendo la dimensione del bundle iniziale: - -```js -// carica i componenti pesanti su richiesta -button.addEventListener('click', async () => { - let { Chart } = await import('./components/chart.js') - new Chart(data) -}) -``` - -Gli import dinamici creano chunk separati che vengono caricati solo quando servono. Questo si chiama "code splitting" ed è una delle funzionalità più potenti di Vite. Quando usate gli import dinamici, Vite crea automaticamente file JavaScript separati per ogni modulo importato dinamicamente. - -Il tag `{asset 'app.js'}` **non** esegue automaticamente il preload di questi chunk dinamici. È un comportamento voluto: non vogliamo scaricare codice che potrebbe non essere mai usato. I chunk vengono scaricati solo quando l'import dinamico viene eseguito. - -Se però sapete che certi import dinamici sono critici e serviranno presto, potete farne il preload: - -```latte -{* punto di ingresso principale *} -{asset 'app.js'} - -{* preload degli import dinamici critici *} -{preload 'components/chart.js'} -``` - -Questo dice al browser di scaricare il componente chart in background, così è pronto subito quando serve. - - -Supporto per TypeScript -======================= - -TypeScript funziona subito: - -```ts -// assets/main.ts -interface User { - name: string - email: string -} - -export function greetUser(user: User): void { - console.log(`Hello, ${user.name}!`) -} -``` - -Referenziate i file TypeScript normalmente (come per qualsiasi file, `main.ts` deve essere un punto di ingresso): - -```latte -{asset 'main.ts'} -``` - -Per il pieno supporto a TypeScript, installatelo: - -```shell -npm install -D typescript -``` - - -Altra configurazione di Vite -============================ - -Ecco alcune utili opzioni di configurazione di Vite con spiegazioni dettagliate: - -```js -export default defineConfig({ - // directory radice che contiene gli asset sorgente - root: 'assets', - - // cartella il cui contenuto viene copiato nella directory di output così com'è - // predefinito: 'public' (relativo a 'root') - publicDir: 'public', - - build: { - // dove mettere i file compilati (relativo a 'root') - outDir: '../www/assets', - - // svuotare la directory di output prima della build? - // utile per rimuovere i vecchi file delle build precedenti - emptyOutDir: true, - - // sottodirectory dentro outDir per i chunk e gli asset generati - // aiuta a organizzare la struttura dell'output - assetsDir: 'static', - - rollupOptions: { - // punto o punti di ingresso: può essere un singolo file o un array di file - // ogni punto di ingresso diventa un bundle separato - input: [ - 'app.js', // applicazione principale - 'admin.js', // pannello di amministrazione - ], - }, - }, - - server: { - // host a cui legare il dev server - // usate '0.0.0.0' per esporlo in rete - host: 'localhost', - - // porta del dev server - port: 5173, - - // configurazione CORS per le richieste cross-origin - cors: { - origin: 'http://myapp.local', - }, - }, - - css: { - // attiva le source map CSS in sviluppo - devSourcemap: true, - }, - - plugins: [ - nette(), - ], -}); -``` - -.[warning] -Attenzione ai percorsi dei punti di ingresso in `rollupOptions.input` qui sopra: Rollup risolve i percorsi relativi rispetto alla directory di lavoro corrente (la radice del progetto), **non** rispetto a `root: 'assets'`. Un semplice `'app.js'` non esisterà quindi al momento della build. Usate l'opzione `entry` del plugin (che risolve i percorsi dei punti di ingresso relativamente a `root`), oppure scrivete i percorsi relativi alla radice del progetto, per esempio `'assets/app.js'`. - -Ecco fatto! Ora avete un sistema di build moderno integrato con Nette Assets. diff --git a/assets/ja/@home.texy b/assets/ja/@home.texy deleted file mode 100644 index 00af238147..0000000000 --- a/assets/ja/@home.texy +++ /dev/null @@ -1,467 +0,0 @@ -Nette Assets -************ - -
    - -ウェブアプリケーションの静的ファイルを手で管理するのに疲れましたか。パスを直に書いたり、キャッシュを無効にする手立てを考えたり、ファイルのバージョン付けを気にしたりするのはもう終わりです。Nette Assets は、画像、スタイルシート、スクリプト、そのほかの静的な資源との付き合い方を一変させます。 - -- **賢いバージョン付け** でブラウザはいつも最新のファイルを読み込みます -- ファイルの種類と寸法の **自動の判別** -- 直感的なタグによる **なめらかな Latte との統合** -- ファイルシステム、CDN、Vite に対応する **しなやかな設計** -- 性能を最大にする **遅延の読み込み** - -
    - - -なぜ Nette Assets なのか -=================== - -静的ファイルを扱う仕事は、しばしば繰り返しが多く間違いやすいコードになります。URL を手で組み立て、キャッシュを無効にするバージョンのパラメータを足し、ファイルの種類ごとに違う扱いをします。それはこんなコードにつながります。 - -```latte -Logo - -``` - -Nette Assets なら、この込み入った作業はすべて消えます。 - -```latte -{* すべて自動 - URL、バージョン付け、寸法 *} - - - -{* あるいはこれだけ *} -{asset 'css/style.css'} -``` - -これだけです。このライブラリは自動的に次のことをします。 -- ファイルが変わった時刻をもとにバージョンのパラメータを足します -- 画像の寸法を見分けて HTML に入れます -- ファイルの種類ごとに正しい HTML の要素を作ります -- 開発の環境でも本番の環境でも面倒を見ます - - -インストール -====== - -Nette Assets は [Composer|best-practices:composer]で入れます。 - -```shell -composer require nette/assets -``` - -PHP 8.1 以上が要り、Nette Framework と見事に組み合わさりますが、単独でも使えます。 - - -はじめの一歩 -====== - -Nette Assets は設定なしでそのまま動きます。静的ファイルを `www/assets/` のディレクトリに置いて、使いはじめてください。 - -```latte -{* 寸法を自動で付けて画像を表示します *} -{asset 'logo.png'} - -{* バージョン付きでスタイルシートを読み込みます *} -{asset 'style.css'} - -{* スクリプトを読み込みます *} -{asset 'app.js'} -``` - -作られる HTML をもっと思いどおりにしたいなら、`n:asset` の属性か `asset()` の関数を使います。 - - -どう働くか -===== - -Nette Assets は 3 つの中心の考え方の上に建っていて、それが強さと使いやすさを両立させています。 - - -アセット - 賢くなったあなたのファイル --------------------- - -**アセット** はアプリケーションのどんな静的ファイルも表します。それぞれのファイルは、役に立つ読み取り専用のプロパティを持つオブジェクトになります。 - -```php -$image = $assets->getAsset('photo.jpg'); -echo $image->url; // '/assets/photo.jpg?v=1699123456' -echo $image->file; // '/var/www/assets/photo.jpg'(手元のパス、または null) -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' -``` - -ファイルの種類ごとに違うプロパティが得られます。 -- **画像**: 幅、高さ、代わりの文、遅延の読み込み -- **スクリプト**: module の種類、整合性のハッシュ、crossorigin -- **スタイルシート**: メディアクエリ、整合性 -- **音声/動画**: 長さ、寸法(動画のみ) -- **フォント**: CORS を伴う正しい先読み - -このライブラリはファイルの種類を自動的に見分け、ふさわしいアセットのクラスを作ります。 - - -マッパー - ファイルはどこから来るのか --------------------- - -**マッパー** は、ファイルの見つけ方とその URL の作り方を知っています。目的ごとに複数のマッパーを持てます。手元のファイル、CDN、クラウドの保管場所、ビルドの道具などです(それぞれに名前があります)。組み込みの `FilesystemMapper` は手元のファイルを、`ViteMapper` は今どきのビルドの道具を受け持ちます。 - -マッパーは[設定 |configuration]で定めます。 - - -レジストリ - あなたの主な窓口 ----------------- - -**レジストリ** はすべてのマッパーを管理し、主な API を差し出します。 - -```php -// サービスにレジストリを注入します -public function __construct( - private Nette\Assets\Registry $assets -) {} -``` - -```php -// いろいろなマッパーからアセットを得ます -$logo = $this->assets->getAsset('images:logo.png'); // 'images' のマッパー -$app = $this->assets->getAsset('app:main.js'); // 'app' のマッパー -$style = $this->assets->getAsset('style.css'); // 既定のマッパーを使います -``` - -レジストリは正しいマッパーを自動的に選び、性能のために結果を蓄えます。 - - -PHP でアセットを扱う -============ - -Registry にはアセットを取り出すメソッドが 2 つあります。 - -```php -// ファイルが存在しなければ Nette\Assets\AssetNotFoundException を投げます -$logo = $assets->getAsset('logo.png'); - -// ファイルが存在しなければ null を返します -$banner = $assets->tryGetAsset('banner.jpg'); -if ($banner) { - echo $banner->url; -} -``` - - -マッパーを指定する ---------- - -どのマッパーを使うかをはっきり選べます。 - -```php -// 既定のマッパーを使います -$file = $assets->getAsset('document.pdf'); - -// 接頭辞で特定のマッパーを使います -$image = $assets->getAsset('images:photo.jpg'); - -// 配列の書き方で特定のマッパーを使います -$script = $assets->getAsset(['scripts', 'app.js']); -``` - - -アセットのプロパティと種類 -------------- - -アセットの種類ごとに、それに合う読み取り専用のプロパティが得られます。 - -```php -// 画像のプロパティ -$image = $assets->getAsset('photo.jpg'); -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' - -// スクリプトのプロパティ -$script = $assets->getAsset('app.js'); -echo $script->type; // null(Vite の入口なら 'module') - -// 音声のプロパティ -$audio = $assets->getAsset('song.mp3'); -echo $audio->duration; // 長さ(秒) - -// どのアセットも文字列にできます(URL を返します) -$url = (string) $assets->getAsset('document.pdf'); -``` - -.[note] -寸法や長さのようなプロパティは、触れられたときにだけ遅れて読み込まれるので、ライブラリは速いままです。 - -.[tip] -静的な解析を正確にするには、[nette/phpstan-rules |tools:phpstan-rules#Assets]の拡張を入れてください。そうすると PHPStan はそれぞれのアセットの具体的な型を知るので、`getAsset('photo.jpg')` は `ImageAsset` と理解され、`->width` に触れてもエラーになりません。 - - -Latte のテンプレートでアセットを使う -===================== - -Nette Assets はタグと関数による直感的な [Latte|latte:]との組み合わせを備えています。 - - -`{asset}` ---------- - -`{asset}` のタグは完全な HTML の要素を描きます。 - -```latte -{* 描かれるもの: *} -{asset 'hero.jpg'} - -{* 描かれるもの: *} -{asset 'app.js'} - -{* 描かれるもの: *} -{asset 'style.css'} -``` - -このタグは自動的に次のことをします。 -- アセットの種類を見分けてふさわしい HTML を作ります -- キャッシュを無効にするバージョン付けを入れます -- 画像には寸法を足します -- 正しい属性(type、media など)を設定します - -HTML の属性の中や ` -``` - - -独自のマッパー -======= - -クラウドの保管場所や動的な生成のような特別な必要には、独自のマッパーを作ります。 - -```php -use Nette\Assets\Mapper; -use Nette\Assets\Asset; -use Nette\Assets\Helpers; - -class CloudStorageMapper implements Mapper -{ - public function __construct( - private CloudClient $client, - private string $bucket, - ) {} - - public function getAsset(string $reference, array $options = []): Asset - { - if (!$this->client->exists($this->bucket, $reference)) { - throw new Nette\Assets\AssetNotFoundException("Asset '$reference' not found"); - } - - $url = $this->client->getPublicUrl($this->bucket, $reference); - return Helpers::createAssetFromUrl($url); - } -} -``` - -設定に登録します。 - -```neon -assets: - mapping: - cloud: CloudStorageMapper(@cloudClient, 'my-bucket') -``` - -ほかのマッパーと同じように使います。 - -```latte -{asset 'cloud:user-uploads/photo.jpg'} -``` - -`Helpers::createAssetFromUrl()` メソッドは、ファイルの拡張子をもとに正しいアセットの種類を自動的に作ります。 - -`Nette\Assets\HtmlRenderable` インターフェースを実装するアセットの種類(画像、スクリプト、スタイルなど)は、`{asset}` で完全な HTML の要素として描けます。そのほかのファイルの種類は `GenericAsset`(たとえば PDF)になり、HTML の要素としては描けませんが、URL(とそのほかのメタデータ)は得られます。そうしたアセットを HTML の要素として描こうとすると `Nette\InvalidArgumentException` が投げられます。属性の中でその URL を使うことはできます。 - - -さらに読む -===== - -- [Nette Assets: 画像から Vite まで、ついにすべてを統一する API |https://blog.nette.org/en/introducing-nette-assets] diff --git a/assets/ja/@left-menu.texy b/assets/ja/@left-menu.texy deleted file mode 100644 index 4e9bdacb11..0000000000 --- a/assets/ja/@left-menu.texy +++ /dev/null @@ -1,14 +0,0 @@ -Nette Assets -************ -- [はじめに |@home] -- [Vite |vite] -- [設定|configuration] - - -関連情報 -**** -- [Nette ドキュメント |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [ベストプラクティス |best-practices:] -- [トラブルシューティング |nette:troubleshooting] diff --git a/assets/ja/@meta.texy b/assets/ja/@meta.texy deleted file mode 100644 index 43b85f3cac..0000000000 --- a/assets/ja/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette ドキュメント}} diff --git a/assets/ja/configuration.texy b/assets/ja/configuration.texy deleted file mode 100644 index b6f7b93913..0000000000 --- a/assets/ja/configuration.texy +++ /dev/null @@ -1,195 +0,0 @@ -Assets の設定 -********** - -.[perex] -Nette Assets の設定オプションの一覧です。 - - -```neon -assets: - # マッパーの相対パスを解決するときの基本のパス - basePath: ... # (string) 既定は %wwwDir% - - # マッパーの相対 URL を解決するときの基本の URL - baseUrl: ... # (string) 既定は %baseUrl% - - # アセットのバージョン付けを全体で有効にしますか - versioning: ... # (bool) 既定は true - - # アセットのマッパーを定めます - mapping: ... # (array) 既定はパス 'assets' -``` - -`basePath` は、マッパーの相対パスを解決するときの既定のファイルシステムのディレクトリを決めます。既定ではウェブのディレクトリ(`%wwwDir%`)を使います。 - -`baseUrl` は、マッパーの相対 URL を解決するときの既定の URL の接頭辞を決めます。既定では根の URL(`%baseUrl%`)を使います。 - -`versioning` のオプションは、キャッシュを無効にするためのバージョンのパラメータをアセットの URL に足すかどうかを全体で決めます。マッパーごとにこの設定を上書きできます。 - - -マッパー ----- - -マッパーは 3 とおりに設定できます。単純な文字列の書き方、詳しい配列の書き方、そしてサービスとして(エンティティの書き方 `ClassName(...)` か `@serviceName()` を使います)です。 - -マッパーを定めるいちばん単純な方法です。 - -```neon -assets: - mapping: - default: assets # %wwwDir%/assets/ のファイルシステムのマッパーを作ります - images: img # %wwwDir%/img/ のファイルシステムのマッパーを作ります - scripts: js # %wwwDir%/js/ のファイルシステムのマッパーを作ります -``` - -それぞれのマッパーは次のような `FilesystemMapper` を作ります。 -- ファイルを `%wwwDir%/` で探します -- `%baseUrl%/` のような URL を作ります -- 全体のバージョン付けの設定を受け継ぎます - - -もっと細かく決めたいなら、詳しい書き方を使います。 - -```neon -assets: - mapping: - images: - # ファイルが置かれているディレクトリ - path: ... # (string) 省略でき、既定は基本のパス(basePath) - - # 作られるリンクの URL の接頭辞 - url: ... # (string) 省略でき、既定は path - - # このマッパーでバージョン付けを有効にしますか - versioning: ... # (bool) 省略でき、全体の設定を受け継ぎます - - # ファイルを探すときに拡張子を自動的に足します - extension: ... # (string|array) 省略でき、既定は null -``` - -設定の値がどう解決されるかを見ていきます。 - -パスの解決: - - 相対パスは `basePath` から解決されます(`basePath` が設定されていなければ `%wwwDir%` から) - - 絶対パスはそのまま使われます - -URL の解決: - - 相対 URL は `baseUrl` から解決されます(`baseUrl` が設定されていなければ `%baseUrl%` から) - - 絶対 URL(スキーム付き、または `//` で始まるもの)はそのまま使われます(`//` の URL はスキームを `baseUrl` から得ます) - - `url` が指定されていなければ、`path` の値が使われます - - -```neon -assets: - basePath: /var/www/project/www - baseUrl: https://example.com/assets - - mapping: - # 相対のパスと URL - images: - path: img # 解決結果: /var/www/project/www/img - url: images # 解決結果: https://example.com/assets/images - - # 絶対のパスと URL - uploads: - path: /var/shared/uploads # そのまま使われます: /var/shared/uploads - url: https://cdn.example.com # そのまま使われます: https://cdn.example.com - - # path だけを指定した場合 - styles: - path: css # パス: /var/www/project/www/css - # URL: https://example.com/assets/css -``` - - -独自のマッパー -------- - -独自のマッパーには、`@serviceName` で既存のサービスを指すか、`ClassName(arguments)` か裸のクラス名で直接定めます。 - -```neon -services: - s3mapper: App\Assets\S3Mapper(%s3.bucket%) - -assets: - mapping: - cloud: @s3mapper - database: App\Assets\DatabaseMapper(@database.connection) -``` - - -Vite のマッパー ----------- - -Vite のマッパーには `type: vite` を足すだけです。これが設定オプションの一覧です。 - -```neon -assets: - mapping: - default: - # マッパーの種類(Vite には必須) - type: vite # (string) 必須。'vite' でなければなりません - - # Vite のビルドの出力先のディレクトリ - path: ... # (string) 省略でき、既定は基本のパス(basePath) - - # ビルドされたアセットの URL の接頭辞 - url: ... # (string) 省略でき、既定は path - - # Vite のマニフェストのファイルの場所 - manifest: ... # (string) 省略でき、path からの相対。既定は /.vite/manifest.json - - # Vite の開発サーバーの設定 - devServer: ... # (bool|string) 省略でき、既定は true - - # public のディレクトリのファイルのバージョン付け - versioning: ... # (bool) 省略でき、全体の設定を受け継ぎます - - # public のディレクトリのファイルの自動の拡張子 - extension: ... # (string|array) 省略でき、既定は null -``` - -`devServer` のオプションは、開発中にアセットをどう読み込むかを決めます。 - -- `true`(既定) - 動いている Vite の開発サーバーを自動的に見つけます(Nette の Vite のプラグインがビルドのディレクトリに作る `.vite/nette.json` ファイルを通してです)。開発サーバーが動いていて**アプリケーションがデバッグモードなら**、アセットはそこから、ホットモジュールリプレースメントに対応した形で読み込まれます。開発サーバーが動いていなければ、アセットは public のディレクトリのビルドされたファイルから読み込まれます。 -- `false` - 開発サーバーとの組み合わせを完全に切ります。アセットはいつもビルドされたファイルから読み込まれます。 -- 独自の URL(たとえば `https://localhost:5173`) - 開発サーバーの URL を、プロトコルとポートも含めて手で指定します。開発サーバーが別のホストやポートで動いているときに便利です。自動の判別と同じく、デバッグモードでだけ効き、本番ではいつもビルドされたファイルが使われます。 - -`versioning` と `extension` のオプションは、Vite が処理しない public のディレクトリのファイルにだけ当たります。 - - -手での設定 ------ - -Nette DI を使っていないなら、マッパーを手で設定します。 - -```php -use Nette\Assets\Registry; -use Nette\Assets\FilesystemMapper; -use Nette\Assets\ViteMapper; - -$registry = new Registry; - -// ファイルシステムのマッパーを足します -$registry->addMapper('images', new FilesystemMapper( - baseUrl: 'https://example.com/img', - basePath: __DIR__ . '/www/img', - extensions: ['webp', 'jpg', 'png'], - versioning: true, -)); - -// Vite のマッパーを足します -$registry->addMapper('app', new ViteMapper( - baseUrl: '/build', - basePath: __DIR__ . '/www/build', - manifestPath: __DIR__ . '/www/build/.vite/manifest.json', - devServer: 'https://localhost:5173', -)); -``` - -登録されたどのマッパーも、`getMapper()` メソッドで名前から取り出せます。 - -```php -$mapper = $registry->getMapper('images'); // 登録されたマッパーを返します -$default = $registry->getMapper(); // 'default' のマッパーを返します -``` diff --git a/assets/ja/vite.texy b/assets/ja/vite.texy deleted file mode 100644 index 133999773d..0000000000 --- a/assets/ja/vite.texy +++ /dev/null @@ -1,567 +0,0 @@ -Vite との統合 -********* - -
    - -今どきの JavaScript のアプリケーションには洗練されたビルドの道具が要ります。Nette Assets は、次の世代のフロントエンドのビルドの道具 [Vite |https://vite.dev/]との一流の組み合わせを備えています。Hot Module Replacement(HMR)による稲妻のように速い開発と、設定の面倒がまったくない最適化された本番のビルドが手に入ります。 - -- **設定なし** - Vite と PHP のテンプレートを自動的につなぎます -- **依存関係をまるごと管理** - ひとつのタグがすべてのアセットを受け持ちます -- **Hot Module Replacement** - JavaScript と CSS がすぐに更新されます -- **最適化された本番のビルド** - コードの分割と使わないコードの除去 - -
    - - -Nette Assets は Vite となめらかに組み合わさるので、テンプレートをいつもどおり書きながら、これらの恩恵をすべて受けられます。 - - -Vite を用意する -========== - -Vite を一歩ずつ用意しましょう。ビルドの道具がはじめてでも心配は要りません。すべて説明します。 - - -第 1 歩: Vite を入れる ----------------- - -まず Vite と Nette のプラグインをプロジェクトに入れます。 - -```shell -npm install -D vite @nette/vite-plugin -``` - -これで Vite と、Vite が Nette と見事に働くのを助ける特別なプラグインが入ります。 - - -第 2 歩: プロジェクトの構造 ----------------- - -標準のやり方は、もとのアセットのファイルをプロジェクトの根の `assets/` のフォルダに置き、ビルドされたものを `www/assets/` に置くことです。 - -/--pre -web-project/ -├── assets/ ← もとのファイル(SCSS、TypeScript、もとの画像) -│ ├── public/ ← 静的ファイル(そのままコピーされます) -│ │ └── favicon.ico -│ ├── images/ -│ │ └── logo.png -│ ├── app.js ← 主な入口 -│ └── style.css ← あなたのスタイル -└── www/ ← 公開のディレクトリ(document root) - ├── assets/ ← ビルドされたファイルがここに置かれます - └── index.php -\-- - -`assets/` のフォルダには、あなたが書くもとのファイルが入ります。Vite はそれらを処理し、ビルドされたものを `www/assets/` に置きます。 - - -第 3 歩: Vite を設定する ------------------ - -プロジェクトの根に `vite.config.ts` のファイルを作ります。このファイルは、もとのファイルがどこにあり、ビルドされたものをどこへ置くかを Vite に伝えます。 - -Nette の Vite のプラグインには賢い既定値が備わっていて、設定が簡単になります。フロントエンドのもとのファイルは `assets/` のディレクトリにあり(`root` のオプション)、ビルドされたファイルは `www/assets/` へ行く(`outDir` のオプション)ものと考えます。あなたが指定する必要があるのは[入口|#入口]だけです。 - -```js -import { defineConfig } from 'vite'; -import nette from '@nette/vite-plugin'; - -export default defineConfig({ - plugins: [ - nette({ - entry: 'app.js', - }), - ], -}); -``` - -内側では、`root` と `outDir` のほかにもいくつかの Vite のオプションをプラグインが設定して、すべてがかみ合うようにします。`base` を `''` に(アセットは document root から直接配られます)、`build.manifest` を `true` に(Nette Assets がハッシュの付いたファイル名を対応づけられるように)、`build.assetsDir` を `''` に(ビルドされたファイルは `static/` の下位のフォルダなしに `outDir` へ直接置かれます)します。どれも上書きできます。 - -.[note] -既定の `outDir`(`www/assets`)には、`www/` のディレクトリがすでにあることが要ります。なければプラグインは *"The output directory ... does not exist"* のエラーで止まります。 - -アセットをビルドするディレクトリに別の名前を使いたいなら、いくつかのオプションを変える必要があります。 - -```js -export default defineConfig({ - root: 'assets', // もとのアセットの根のディレクトリ - - build: { - outDir: '../www/assets', // ビルドされたファイルの置き場所 - }, - - // ... そのほかの設定 ... -}); -``` - -.[note] -`outDir` のパスは `root` からの相対と見なされます。だから先頭に `../` があるのです。 - - -第 4 歩: Nette を設定する ------------------- - -`common.neon` で Nette Assets に Vite のことを伝えます。 - -```neon -assets: - mapping: - default: - type: vite # ViteMapper を使うよう Nette に伝えます - path: assets -``` - - -第 5 歩: スクリプトを足す ---------------- - -`package.json` に次のスクリプトを足します。 - -```json -{ - "scripts": { - "dev": "vite", - "build": "vite build" - } -} -``` - -これで次のことができます。 -- `npm run dev` - ホットリロードの付いた開発サーバーを立ち上げます -- `npm run build` - 最適化された本番のファイルを作ります - - -入口 -=== - -**入口** は、あなたのアプリケーションが始まる主なファイルです。このファイルからほかのファイル(CSS、JavaScript のモジュール、画像)を import して、依存関係の木を作ります。Vite はこれらの import をたどり、すべてをひとつにまとめます。 - -入口 `assets/app.js` の例です。 - -```js -// スタイルを import します -import './style.css' - -// JavaScript のモジュールを import します -import netteForms from 'nette-forms'; -import naja from 'naja'; - -// アプリケーションを立ち上げます -netteForms.initOnLoad(); -naja.initialize(); -``` - -テンプレートでは、入口を次のように差し込めます。 - -```latte -{asset 'app.js'} -``` - -Nette Assets は必要なすべての HTML のタグを自動的に作ります。JavaScript、CSS、そのほかの依存関係もです。 - - -複数の入口 ------ - -大きめのアプリケーションでは、別々の入口が要ることがよくあります。 - -```js -export default defineConfig({ - plugins: [ - nette({ - entry: [ - 'app.js', // 公開のページ - 'admin.js', // 管理画面 - ], - }), - ], -}); -``` - -それらを違うテンプレートで使います。 - -```latte -{* 公開のページで *} -{asset 'app.js'} - -{* 管理画面で *} -{asset 'admin.js'} -``` - - -大事な点: もとのファイルとビルドされたファイル ------------------------- - -本番では、Vite が使えるようにしたファイルしか読み込めないことを分かっておくのが肝心です。それはマニフェスト経由か、public のフォルダからそのままコピーされたものです。 - -1. `entry` で定めた**入口**(それが動的に import するモジュールも含みます)と、**JavaScript や CSS から参照されるアセット**(画像、フォントなど)。これらはすべてマニフェストに記録されます -2. **`assets/public/` のディレクトリのファイル**。これらはマニフェストには *ありません*。そのままコピーされ、`{asset}` はファイルシステムの代わりの手立てでそれを見つけます - -`assets/` の中の好きなファイルを `{asset}` で読み込むことは**できません**。どこからも参照されていないファイルはビルドされないからです。ほかのアセットを Vite に知らせたいなら、[public のフォルダ |#public のフォルダ]へ移せます。 - -既定では、Vite は 4KB より小さいアセットをすべて埋め込むので、それらのファイルを直接参照できないことに注意してください([Vite のドキュメント |https://vite.dev/guide/assets.html]をご覧ください)。 - -```latte -{* ✓ これは働きます - 入口だからです *} -{asset 'app.js'} - -{* ✓ これは働きます - assets/public/ にあるからです *} -{asset 'favicon.ico'} - -{* ✗ これは働きません - assets/ の中のただのファイルです *} -{asset 'components/button.js'} -``` - - -開発モード -===== - -開発モードは完全に省略できますが、使うと大きな恩恵があります。いちばんの利点は **Hot Module Replacement(HMR)** で、アプリケーションの状態を失わずに変更がすぐ見えるので、開発の体験がずっとなめらかで速くなります。 - -Vite は今どきのビルドの道具で、開発を信じられないほど速くします。昔ながらのバンドラと違い、Vite は開発中コードをブラウザへ直接配るので、プロジェクトがどれだけ大きくてもサーバーはすぐ立ち上がり、更新は稲妻のように速くなります。 - - -開発サーバーを立ち上げる ------------- - -開発サーバーを走らせます。 - -```shell -npm run dev -``` - -次のように出ます。 - -``` - ➜ Local: http://localhost:5173/ - ➜ Network: use --host to expose -``` - -開発しているあいだ、この端末は開いたままにしてください。 - -開発サーバーが動いているあいだ、プラグインはその URL の入った小さな合図のファイル `www/assets/.vite/nette.json` を書きます。PHP 側の Nette Assets はこのファイルを読み、次の 2 つがそろったときに開発サーバーからの読み込みへ切り替えます。 -1. Vite の開発サーバーが動いている(合図のファイルがある)こと、そして -2. Nette のアプリケーションがデバッグモードであること。 - -結果は次のとおりです。 - -```latte -{asset 'app.js'} -{* 開発中: - *} -{* 本番: *} -``` - -設定は要りません。ただ動きます。この判別を切ったり、開発サーバーの URL を手で決めたりするには、[devServer のオプション |configuration#Vite のマッパー]をご覧ください。 - -.[note] -合図のファイルは `www/assets/.vite/nette.json` にあります(本番の `manifest.json` のすぐ隣です)。プラグインの `infoFile` のオプションで名前を変えられ、既定は `.vite/nette.json` です。Nette が動いている開発サーバーに気づかないなら、このファイルがあって正しい URL を指しているかを確かめてください。 - - -違うドメインで作業する ------------ - -開発サーバーが `localhost` 以外(たとえば `myapp.local`)で動いているなら、CORS(Cross-Origin Resource Sharing)の問題に出くわすかもしれません。CORS はウェブブラウザの安全の機能で、違うドメインどうしのリクエストを既定で遮ります。PHP のアプリケーションが `myapp.local` で動き、Vite が `localhost:5173` で動いていると、ブラウザはこれらを違うドメインと見なしてリクエストを遮ります。 - -解決の方法は 2 つあります。 - -**方法 1: CORS を設定する** - -いちばん単純な解は、PHP のアプリケーションからの別オリジンのリクエストを許すことです。 - -```js -export default defineConfig({ - // ... そのほかの設定 ... - - server: { - cors: { - origin: 'http://myapp.local', // あなたの PHP のアプリケーションの URL - }, - }, -}); -``` - -**方法 2: Vite を同じドメインで動かす** - -もうひとつの解は、Vite を PHP のアプリケーションと同じドメインで動かすことです。 - -```js -export default defineConfig({ - // ... そのほかの設定 ... - - server: { - host: 'myapp.local', // PHP のアプリケーションと同じ - }, -}); -``` - -実のところこの場合も CORS の設定が要ります。開発サーバーは同じホスト名でも違うポートで動くからです。とはいえこの場合、CORS は Nette の Vite のプラグインが自動的に設定します。 - - -HTTPS での開発 ----------- - -HTTPS で開発しているなら、Vite の開発サーバーに証明書が要ります。いちばん簡単なのは、証明書を自動的に作るプラグインを使うことです。 - -```shell -npm install -D vite-plugin-mkcert -``` - -`vite.config.ts` での設定の仕方です。 - -```js -import mkcert from 'vite-plugin-mkcert'; - -export default defineConfig({ - // ... そのほかの設定 ... - - plugins: [ - mkcert(), // 証明書を自動的に作り、https を有効にします - nette(), - ], -}); -``` - -CORS の設定(上の方法 1)を使っているなら、origin の URL を `http://` ではなく `https://` に更新する必要があることに注意してください。 - - -Docker での開発 ------------ - -Vite を Docker のコンテナの中で走らせるときは、2 つのことに気を配る必要があります。あなたの機械のブラウザが開発サーバーに届けること、そして Vite がコンテナの境をまたいでファイルの変更に気づけることです。 - -まず、コンテナから Vite のポートを [公開し |https://docs.docker.com/get-started/docker-concepts/running-containers/publishing-ports/]、開発サーバーをすべてのインターフェースに結び付けて、コンテナの外から届くようにします。 - -```js -export default defineConfig({ - // ... そのほかの設定 ... - - plugins: [ - nette(), - ], - server: { - host: '0.0.0.0', // すべてのインターフェースで待ち受けます(コンテナでは必須) - port: 5173, // 公開したポートと合わせなければなりません - strictPort: true, // 別のポートを選ばず失敗させます - watch: { - usePolling: true, // マウントしたボリュームでファイルの変更に気づかないなら有効にします - }, - }, -}); -``` - -プラグインは開発サーバーの URL を PHP 側のために `nette.json` へ書きます。`host: '0.0.0.0'` はブラウザからは使えない(アセットごとに `localhost` へリダイレクトされてしまう)ので、プラグインはその URL の中でそれを自動的に `localhost` へ書き換え、アセットが正しく読み込まれるようにします。 - -`localhost` ではなく独自のドメインでアプリケーションを開くなら、プラグインの `host` のオプションをそのドメインにしてください。 - -```js - plugins: [ - nette({ host: 'myapp.local' }), // PHP のアプリケーションと同じドメイン - ], -``` - -そうするとプラグインはそのホストを開発サーバーの URL に使い、CORS の origin に足し、Vite の `allowedHosts` にも入れるので、CORS を手で設定しなくても働きます。 - -もっと込み入った構えでは、たとえば Vite がリバースプロキシの後ろで動いていて、公のホスト、ポート、プロトコルがどれも内側のアドレスと違う場合、Vite の `server.origin` を公の URL 全体に設定してください。プラグインはそれを尊重し、手元のソケットから URL を導く代わりに、そのまま `nette.json` へ書きます。 - -```js - server: { - origin: 'https://myapp.local:8443', // ブラウザが Vite に届く公の URL - }, -``` - - -本番のビルド -====== - -最適化された本番のファイルを作ります。 - -```shell -npm run build -``` - -Vite は次のことをします。 -- すべての JavaScript と CSS を最小化します -- コードを最適なかたまりに分けます -- キャッシュを無効にするためにハッシュの付いたファイル名を作ります -- Nette Assets のためにマニフェストのファイルを作ります - -出力の例です。 - -``` -www/assets/ -├── app-4f3a2b1c.js # あなたの主な JavaScript(最小化済み) -├── app-7d8e9f2a.css # 抜き出された CSS(最小化済み) -├── vendor-8c4b5e6d.js # 共有の依存関係 -└── .vite/ - └── manifest.json # Nette Assets のための対応づけ -``` - -ハッシュの付いたファイル名のおかげで、ブラウザはいつも最新の版を読み込みます。 - - -public のフォルダ -============ - -`assets/public/` のディレクトリのファイルは、処理されずに出力先へコピーされます。 - -``` -assets/ -├── public/ -│ ├── favicon.ico -│ ├── robots.txt -│ └── images/ -│ └── og-image.jpg -├── app.js -└── style.css -``` - -ふつうに参照します。 - -```latte -{* これらのファイルはそのままコピーされます *} - - -``` - -public のファイルでは FilesystemMapper の機能を使えます。`extension` のオプションは拡張子のない参照に当たります(たとえば `{asset 'images/og-image'}` はまず `og-image.webp` を見つけます)。 - -```neon -assets: - mapping: - default: - type: vite - path: assets - extension: [webp, jpg, png] # 拡張子のない参照のため - versioning: true # キャッシュを無効にする値を足します -``` - -`vite.config.ts` の設定では、`publicDir` のオプションで public のフォルダを変えられます。 - - -動的な import -========== - -Vite は最適な読み込みのためにコードを自動的に分けます。動的な import を使うと、本当に必要になったときにだけコードを読み込めるので、最初のかたまりの大きさを減らせます。 - -```js -// 重い部品を必要になったときに読み込みます -button.addEventListener('click', async () => { - let { Chart } = await import('./components/chart.js') - new Chart(data) -}) -``` - -動的な import は、必要になったときにだけ読み込まれる別々のかたまりを作ります。これは「コードの分割」と呼ばれ、Vite のもっとも強力な機能のひとつです。動的な import を使うと、Vite は動的に import されるモジュールごとに別々の JavaScript のファイルを自動的に作ります。 - -`{asset 'app.js'}` のタグは、これらの動的なかたまりを自動的に先読み**しません**。これは意図した振る舞いです。使われないかもしれないコードをダウンロードしたくないからです。かたまりは、動的な import が実行されたときにだけダウンロードされます。 - -とはいえ、ある動的な import が肝心で、すぐ必要になると分かっているなら、先読みできます。 - -```latte -{* 主な入口 *} -{asset 'app.js'} - -{* 肝心な動的な import を先読みします *} -{preload 'components/chart.js'} -``` - -これでブラウザは chart の部品を裏でダウンロードするので、必要になったときにはすぐ使えます。 - - -TypeScript への対応 -=============== - -TypeScript はそのまま動きます。 - -```ts -// assets/main.ts -interface User { - name: string - email: string -} - -export function greetUser(user: User): void { - console.log(`Hello, ${user.name}!`) -} -``` - -TypeScript のファイルをふつうに参照します(どのファイルとも同じく、`main.ts` は入口でなければなりません)。 - -```latte -{asset 'main.ts'} -``` - -TypeScript に完全に対応させるには、それを入れます。 - -```shell -npm install -D typescript -``` - - -そのほかの Vite の設定 -============== - -役に立つ Vite の設定オプションを、詳しい説明とともにいくつか挙げます。 - -```js -export default defineConfig({ - // もとのアセットの入った根のディレクトリ - root: 'assets', - - // 中身が出力先のディレクトリへそのままコピーされるフォルダ - // 既定: 'public'('root' からの相対) - publicDir: 'public', - - build: { - // ビルドされたファイルの置き場所('root' からの相対) - outDir: '../www/assets', - - // ビルドの前に出力先のディレクトリを空にしますか - // 前のビルドの古いファイルを取り除くのに役立ちます - emptyOutDir: true, - - // 作られるかたまりとアセットのための outDir の中の下位のディレクトリ - // 出力の構造を整えるのに役立ちます - assetsDir: 'static', - - rollupOptions: { - // 入口。ひとつのファイルでもファイルの配列でもかまいません - // それぞれの入口が別々のかたまりになります - input: [ - 'app.js', // 主なアプリケーション - 'admin.js', // 管理画面 - ], - }, - }, - - server: { - // 開発サーバーを結び付けるホスト - // ネットワークへ公開するには '0.0.0.0' を使います - host: 'localhost', - - // 開発サーバーのポート - port: 5173, - - // 別オリジンのリクエストのための CORS の設定 - cors: { - origin: 'http://myapp.local', - }, - }, - - css: { - // 開発中に CSS のソースマップを有効にします - devSourcemap: true, - }, - - plugins: [ - nette(), - ], -}); -``` - -.[warning] -上の `rollupOptions.input` の入口のパスには気をつけてください。Rollup は相対パスを今の作業ディレクトリ(あなたのプロジェクトの根)から解決し、`root: 'assets'` からは**解決しません**。ですから裸の `'app.js'` はビルドのときに存在しません。プラグインの `entry` のオプション(入口のパスを `root` からの相対で解決します)を使うか、パスをプロジェクトの根からの相対で、たとえば `'assets/app.js'` と書いてください。 - -これで終わりです。今どきのビルドのしくみが Nette Assets と組み合わさりました。 diff --git a/assets/meta.json b/assets/meta.json deleted file mode 100644 index b47a6e6d2d..0000000000 --- a/assets/meta.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": "1.x", - "repo": "nette/assets", - "composer": "nette/assets", - "api": "https://api.nette.org/assets/" -} diff --git a/assets/pl/@home.texy b/assets/pl/@home.texy deleted file mode 100644 index 77b4f068ef..0000000000 --- a/assets/pl/@home.texy +++ /dev/null @@ -1,467 +0,0 @@ -Nette Assets -************ - -
    - -Zmęczony ręcznym zarządzaniem plikami statycznymi w swoich aplikacjach webowych? Zapomnij o zapisywaniu ścieżek na sztywno, o walce z inwalidacją cache i o martwieniu się wersjonowaniem plików. Nette Assets zmienia sposób, w jaki pracujesz z obrazkami, arkuszami stylów, skryptami i innymi zasobami statycznymi. - -- **Sprytne wersjonowanie** zapewnia, że przeglądarki zawsze wczytują najnowsze pliki -- **Automatyczne wykrywanie** typów plików i wymiarów -- **Płynna integracja z Latte** dzięki intuicyjnym tagom -- **Elastyczna architektura** wspierająca systemy plików, CDN-y i Vite -- **Leniwe wczytywanie** dla optymalnej wydajności - -
    - - -Dlaczego Nette Assets? -====================== - -Praca z plikami statycznymi często oznacza powtarzalny, podatny na błędy kod. Ręcznie składasz URL-e, dodajesz parametry wersji na potrzeby cache bustingu i obsługujesz różne typy plików w różny sposób. Prowadzi to do kodu w rodzaju: - -```latte -Logo - -``` - -Z Nette Assets cała ta złożoność znika: - -```latte -{* Wszystko zautomatyzowane - URL, wersjonowanie, wymiary *} - - - -{* Albo po prostu *} -{asset 'css/style.css'} -``` - -I to wszystko! Biblioteka automatycznie: -- Dodaje parametry wersji na podstawie czasu modyfikacji pliku -- Wykrywa wymiary obrazków i umieszcza je w HTML-u -- Generuje właściwy element HTML dla każdego typu pliku -- Obsługuje zarówno środowisko deweloperskie, jak i produkcyjne - - -Instalacja -========== - -Zainstaluj Nette Assets za pomocą [Composera|best-practices:composer]: - -```shell -composer require nette/assets -``` - -Wymaga PHP 8.1 albo wyższego i doskonale współpracuje z Nette Framework, ale można go używać także samodzielnie. - - -Pierwsze kroki -============== - -Nette Assets działa od razu, bez żadnej konfiguracji. Umieść swoje pliki statyczne w katalogu `www/assets/` i zacznij ich używać: - -```latte -{* Wyświetlenie obrazka z automatycznymi wymiarami *} -{asset 'logo.png'} - -{* Dołączenie arkusza stylów z wersjonowaniem *} -{asset 'style.css'} - -{* Wczytanie skryptu *} -{asset 'app.js'} -``` - -Dla większej kontroli nad generowanym HTML-em użyj atrybutu `n:asset` albo funkcji `asset()`. - - -Jak to działa -============= - -Nette Assets zbudowane jest wokół trzech kluczowych pojęć, które czynią je potężnym, a zarazem prostym w użyciu: - - -Zasoby - Twoje pliki stają się sprytne --------------------------------------- - -**Zasób** reprezentuje dowolny plik statyczny w Twojej aplikacji. Każdy plik staje się obiektem z przydatnymi właściwościami readonly: - -```php -$image = $assets->getAsset('photo.jpg'); -echo $image->url; // '/assets/photo.jpg?v=1699123456' -echo $image->file; // '/var/www/assets/photo.jpg' (ścieżka lokalna albo null) -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' -``` - -Różne typy plików udostępniają różne właściwości: -- **Obrazki**: szerokość, wysokość, tekst alternatywny, leniwe wczytywanie -- **Skrypty**: typ modułu, hashe integralności, crossorigin -- **Arkusze stylów**: zapytania media, integralność -- **Audio/wideo**: czas trwania, wymiary (tylko wideo) -- **Fonty**: właściwe preładowanie z CORS - -Biblioteka automatycznie wykrywa typy plików i tworzy odpowiednią klasę zasobu. - - -Mappery - skąd biorą się pliki ------------------------------- - -**Mapper** wie, jak znaleźć pliki i utworzyć dla nich URL-e. Możesz mieć wiele mapperów do różnych celów: pliki lokalne, CDN, magazyn w chmurze albo narzędzia budujące (każdy z nich ma nazwę). Wbudowany `FilesystemMapper` obsługuje pliki lokalne, a `ViteMapper` integruje się z nowoczesnymi narzędziami budującymi. - -Mappery definiuje się w [konfiguracji |configuration]. - - -Rejestr - Twój główny interfejs -------------------------------- - -**Rejestr** zarządza wszystkimi mapperami i udostępnia główne API: - -```php -// Wstrzykujemy rejestr do swojej usługi -public function __construct( - private Nette\Assets\Registry $assets -) {} -``` - -```php -// Pobieramy zasoby z różnych mapperów -$logo = $this->assets->getAsset('images:logo.png'); // mapper 'images' -$app = $this->assets->getAsset('app:main.js'); // mapper 'app' -$style = $this->assets->getAsset('style.css'); // używa mappera domyślnego -``` - -Rejestr automatycznie wybiera właściwy mapper i buforuje wyniki dla wydajności. - - -Praca z zasobami w PHP -====================== - -Registry udostępnia dwie metody do pobierania zasobów: - -```php -// Rzuca Nette\Assets\AssetNotFoundException, jeśli plik nie istnieje -$logo = $assets->getAsset('logo.png'); - -// Zwraca null, jeśli plik nie istnieje -$banner = $assets->tryGetAsset('banner.jpg'); -if ($banner) { - echo $banner->url; -} -``` - - -Wskazywanie mapperów --------------------- - -Możesz jawnie wybrać, którego mappera użyć: - -```php -// Używamy mappera domyślnego -$file = $assets->getAsset('document.pdf'); - -// Używamy konkretnego mappera z prefiksem -$image = $assets->getAsset('images:photo.jpg'); - -// Używamy konkretnego mappera ze składnią tablicową -$script = $assets->getAsset(['scripts', 'app.js']); -``` - - -Właściwości i typy zasobów --------------------------- - -Każdy typ zasobu udostępnia odpowiednie właściwości readonly: - -```php -// Właściwości obrazka -$image = $assets->getAsset('photo.jpg'); -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' - -// Właściwości skryptu -$script = $assets->getAsset('app.js'); -echo $script->type; // null ('module' dla punktów wejścia Vite) - -// Właściwości audio -$audio = $assets->getAsset('song.mp3'); -echo $audio->duration; // czas trwania w sekundach - -// Wszystkie zasoby da się rzutować na ciąg (zwraca URL) -$url = (string) $assets->getAsset('document.pdf'); -``` - -.[note] -Właściwości takie jak wymiary czy czas trwania wczytywane są leniwie, dopiero przy dostępie, dzięki czemu biblioteka pozostaje szybka. - -.[tip] -Dla precyzyjnej analizy statycznej zainstaluj rozszerzenie [nette/phpstan-rules |tools:phpstan-rules#Assets]. PHPStan zna wtedy konkretny typ każdego zasobu, więc `getAsset('photo.jpg')` rozumiane jest jako `ImageAsset`, a dostęp do `->width` nie zgłasza błędu. - - -Używanie zasobów w szablonach Latte -=================================== - -Nette Assets daje intuicyjną integrację z [Latte|latte:] przez tagi i funkcje. - - -`{asset}` ---------- - -Tag `{asset}` renderuje kompletne elementy HTML: - -```latte -{* Renderuje: *} -{asset 'hero.jpg'} - -{* Renderuje: *} -{asset 'app.js'} - -{* Renderuje: *} -{asset 'style.css'} -``` - -Tag automatycznie: -- Wykrywa typ zasobu i generuje odpowiedni HTML -- Dodaje wersjonowanie na potrzeby cache bustingu -- Dodaje wymiary obrazków -- Ustawia poprawne atrybuty (type, media itd.) - -Użyty wewnątrz atrybutów HTML albo wewnątrz elementów ` -``` - - -Własne mappery -============== - -Twórz własne mappery na specjalne potrzeby, jak magazyn w chmurze albo dynamiczne generowanie: - -```php -use Nette\Assets\Mapper; -use Nette\Assets\Asset; -use Nette\Assets\Helpers; - -class CloudStorageMapper implements Mapper -{ - public function __construct( - private CloudClient $client, - private string $bucket, - ) {} - - public function getAsset(string $reference, array $options = []): Asset - { - if (!$this->client->exists($this->bucket, $reference)) { - throw new Nette\Assets\AssetNotFoundException("Asset '$reference' not found"); - } - - $url = $this->client->getPublicUrl($this->bucket, $reference); - return Helpers::createAssetFromUrl($url); - } -} -``` - -Rejestracja w konfiguracji: - -```neon -assets: - mapping: - cloud: CloudStorageMapper(@cloudClient, 'my-bucket') -``` - -Używaj jak każdego innego mappera: - -```latte -{asset 'cloud:user-uploads/photo.jpg'} -``` - -Metoda `Helpers::createAssetFromUrl()` automatycznie tworzy właściwy typ zasobu na podstawie rozszerzenia pliku. - -Typy zasobów implementujące interfejs `Nette\Assets\HtmlRenderable` (obrazki, skrypty, style itd.) mogą być renderowane przez `{asset}` jako kompletny element HTML. Pozostałe typy plików stają się `GenericAsset` (na przykład PDF), którego nie da się wyrenderować jako elementu HTML, ale nadal daje URL (i inne metadane). Próba wyrenderowania takiego zasobu jako elementu HTML rzuca `Nette\InvalidArgumentException`; jego URL możesz jednak nadal użyć wewnątrz atrybutu. - - -Dalsza lektura -============== - -- [Nette Assets: wreszcie jednolite API na wszystko od obrazków po Vite |https://blog.nette.org/en/introducing-nette-assets] diff --git a/assets/pl/@left-menu.texy b/assets/pl/@left-menu.texy deleted file mode 100644 index fa65a11cd0..0000000000 --- a/assets/pl/@left-menu.texy +++ /dev/null @@ -1,14 +0,0 @@ -Nette Assets -************ -- [Pierwsze kroki |@home] -- [Vite |vite] -- [Konfiguracja|configuration] - - -Dalsza lektura -************** -- [Dokumentacja Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Dobre praktyki |best-practices:] -- [Rozwiązywanie problemów |nette:troubleshooting] diff --git a/assets/pl/@meta.texy b/assets/pl/@meta.texy deleted file mode 100644 index 61ac92d1af..0000000000 --- a/assets/pl/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Dokumentacja Nette}} diff --git a/assets/pl/configuration.texy b/assets/pl/configuration.texy deleted file mode 100644 index 2ec47b6608..0000000000 --- a/assets/pl/configuration.texy +++ /dev/null @@ -1,195 +0,0 @@ -Konfiguracja Assets -******************* - -.[perex] -Przegląd opcji konfiguracyjnych dla Nette Assets. - - -```neon -assets: - # ścieżka bazowa do rozwiązywania względnych ścieżek mapperów - basePath: ... # (string) domyślnie %wwwDir% - - # URL bazowy do rozwiązywania względnych URL-i mapperów - baseUrl: ... # (string) domyślnie %baseUrl% - - # włączyć wersjonowanie zasobów globalnie? - versioning: ... # (bool) domyślnie true - - # definiuje mappery zasobów - mapping: ... # (array) domyślnie ścieżka 'assets' -``` - -`basePath` ustawia domyślny katalog systemu plików do rozwiązywania ścieżek względnych w mapperach. Domyślnie używa katalogu webowego (`%wwwDir%`). - -`baseUrl` ustawia domyślny prefiks URL do rozwiązywania URL-i względnych w mapperach. Domyślnie używa URL głównego (`%baseUrl%`). - -Opcja `versioning` steruje globalnie tym, czy do URL-i zasobów dodawane są parametry wersji na potrzeby cache bustingu. Poszczególne mappery mogą to ustawienie nadpisać. - - -Mappery -------- - -Mappery można skonfigurować na trzy sposoby: prostym zapisem tekstowym, szczegółowym zapisem tablicowym albo jako usługę (zapisem encji `ClassName(...)` albo `@serviceName()`). - -Najprostszy sposób zdefiniowania mappera: - -```neon -assets: - mapping: - default: assets # Tworzy mapper systemu plików dla %wwwDir%/assets/ - images: img # Tworzy mapper systemu plików dla %wwwDir%/img/ - scripts: js # Tworzy mapper systemu plików dla %wwwDir%/js/ -``` - -Każdy mapper tworzy `FilesystemMapper`, który: -- Szuka plików w `%wwwDir%/` -- Generuje URL-e w rodzaju `%baseUrl%/` -- Dziedziczy globalne ustawienie wersjonowania - - -Dla większej kontroli użyj zapisu szczegółowego: - -```neon -assets: - mapping: - images: - # katalog, w którym przechowywane są pliki - path: ... # (string) opcjonalne, domyślnie ścieżka bazowa (basePath) - - # prefiks URL dla generowanych odnośników - url: ... # (string) opcjonalne, domyślnie path - - # włączyć wersjonowanie dla tego mappera? - versioning: ... # (bool) opcjonalne, dziedziczy ustawienie globalne - - # automatyczne dodawanie rozszerzenia (rozszerzeń) przy szukaniu plików - extension: ... # (string|array) opcjonalne, domyślnie null -``` - -Jak rozwiązywane są wartości konfiguracyjne: - -Rozwiązywanie ścieżki: - - Ścieżki względne rozwiązywane są od `basePath` (albo `%wwwDir%`, jeśli `basePath` nie jest ustawione) - - Ścieżki absolutne używane są bez zmian - -Rozwiązywanie URL: - - URL-e względne rozwiązywane są od `baseUrl` (albo `%baseUrl%`, jeśli `baseUrl` nie jest ustawione) - - URL-e absolutne (ze schematem albo `//`) używane są bez zmian (URL `//` bierze schemat z `baseUrl`) - - Jeśli `url` nie jest podane, używa wartości `path` - - -```neon -assets: - basePath: /var/www/project/www - baseUrl: https://example.com/assets - - mapping: - # Ścieżka i URL względne - images: - path: img # Rozwiązane do: /var/www/project/www/img - url: images # Rozwiązane do: https://example.com/assets/images - - # Ścieżka i URL absolutne - uploads: - path: /var/shared/uploads # Użyte bez zmian: /var/shared/uploads - url: https://cdn.example.com # Użyte bez zmian: https://cdn.example.com - - # Podana tylko ścieżka - styles: - path: css # Ścieżka: /var/www/project/www/css - # URL: https://example.com/assets/css -``` - - -Własne mappery --------------- - -Dla własnych mapperów odwołaj się do istniejącej usługi przez `@serviceName` albo zdefiniuj ją bezpośrednio za pomocą `ClassName(argumenty)` czy samej nazwy klasy: - -```neon -services: - s3mapper: App\Assets\S3Mapper(%s3.bucket%) - -assets: - mapping: - cloud: @s3mapper - database: App\Assets\DatabaseMapper(@database.connection) -``` - - -Mapper Vite ------------ - -Mapper Vite wymaga tylko dodania `type: vite`. Oto kompletna lista opcji konfiguracyjnych: - -```neon -assets: - mapping: - default: - # typ mappera (wymagany dla Vite) - type: vite # (string) wymagane, musi być 'vite' - - # katalog wyjściowy buildu Vite - path: ... # (string) opcjonalne, domyślnie ścieżka bazowa (basePath) - - # prefiks URL dla zbudowanych zasobów - url: ... # (string) opcjonalne, domyślnie path - - # lokalizacja pliku manifestu Vite - manifest: ... # (string) opcjonalne, względem path, domyślnie /.vite/manifest.json - - # konfiguracja serwera deweloperskiego Vite - devServer: ... # (bool|string) opcjonalne, domyślnie true - - # wersjonowanie plików z katalogu public - versioning: ... # (bool) opcjonalne, dziedziczy ustawienie globalne - - # automatyczne rozszerzenie dla plików z katalogu public - extension: ... # (string|array) opcjonalne, domyślnie null -``` - -Opcja `devServer` steruje tym, jak zasoby wczytywane są podczas tworzenia: - -- `true` (domyślnie) - Automatycznie wykrywa działający serwer deweloperski Vite (przez plik `.vite/nette.json`, który plugin Vite dla Nette tworzy w katalogu buildu). Jeśli serwer deweloperski działa **i Twoja aplikacja jest w trybie debug**, zasoby wczytywane są z niego ze wsparciem dla hot module replacement. Jeśli serwer deweloperski nie działa, zasoby wczytywane są ze zbudowanych plików w katalogu publicznym. -- `false` - Całkowicie wyłącza integrację z serwerem deweloperskim. Zasoby zawsze wczytywane są ze zbudowanych plików. -- Własny URL (np. `https://localhost:5173`) - Ręcznie podaje URL serwera deweloperskiego wraz z protokołem i portem. Przydaje się, gdy serwer deweloperski działa na innym hoście albo porcie. Podobnie jak autodetekcja, stosuje się tylko w trybie debug; w produkcji zawsze używane są zbudowane pliki. - -Opcje `versioning` i `extension` dotyczą wyłącznie plików w katalogu public Vite, które nie są przez Vite przetwarzane. - - -Konfiguracja ręczna -------------------- - -Gdy nie używasz Nette DI, skonfiguruj mappery ręcznie: - -```php -use Nette\Assets\Registry; -use Nette\Assets\FilesystemMapper; -use Nette\Assets\ViteMapper; - -$registry = new Registry; - -// Dodajemy mapper systemu plików -$registry->addMapper('images', new FilesystemMapper( - baseUrl: 'https://example.com/img', - basePath: __DIR__ . '/www/img', - extensions: ['webp', 'jpg', 'png'], - versioning: true, -)); - -// Dodajemy mapper Vite -$registry->addMapper('app', new ViteMapper( - baseUrl: '/build', - basePath: __DIR__ . '/www/build', - manifestPath: __DIR__ . '/www/build/.vite/manifest.json', - devServer: 'https://localhost:5173', -)); -``` - -Dowolny zarejestrowany mapper możesz też pobrać po nazwie metodą `getMapper()`: - -```php -$mapper = $registry->getMapper('images'); // zwraca zarejestrowany mapper -$default = $registry->getMapper(); // zwraca mapper 'default' -``` diff --git a/assets/pl/vite.texy b/assets/pl/vite.texy deleted file mode 100644 index 66a2110b07..0000000000 --- a/assets/pl/vite.texy +++ /dev/null @@ -1,567 +0,0 @@ -Integracja z Vite -***************** - -
    - -Nowoczesne aplikacje JavaScriptowe wymagają zaawansowanych narzędzi budujących. Nette Assets daje pierwszorzędną integrację z [Vite |https://vite.dev/], narzędziem budującym frontend nowej generacji. Uzyskaj błyskawiczne tworzenie z Hot Module Replacement (HMR) i zoptymalizowane buildy produkcyjne bez cierpienia z konfiguracją. - -- **Zero konfiguracji** - automatyczny most między Vite a szablonami PHP -- **Kompletne zarządzanie zależnościami** - jeden tag obsługuje wszystkie zasoby -- **Hot Module Replacement** - natychmiastowe aktualizacje JavaScriptu i CSS -- **Zoptymalizowane buildy produkcyjne** - code splitting i tree shaking - -
    - - -Nette Assets integruje się z Vite płynnie, więc dostajesz wszystkie te korzyści, pisząc szablony jak zwykle. - - -Ustawienie Vite -=============== - -Ustawmy Vite krok po kroku. Nie martw się, jeśli narzędzia budujące są dla Ciebie nowe, wszystko wyjaśnimy! - - -Krok 1: instalacja Vite ------------------------ - -Najpierw zainstaluj w swoim projekcie Vite i plugin Nette: - -```shell -npm install -D vite @nette/vite-plugin -``` - -Instaluje to Vite i specjalny plugin, który pomaga Vite doskonale współpracować z Nette. - - -Krok 2: struktura projektu --------------------------- - -Standardowe podejście to umieszczenie źródłowych plików zasobów w folderze `assets/` w katalogu głównym projektu, a wersji skompilowanych w `www/assets/`: - -/--pre -web-project/ -├── assets/ ← pliki źródłowe (SCSS, TypeScript, obrazki źródłowe) -│ ├── public/ ← pliki statyczne (kopiowane bez zmian) -│ │ └── favicon.ico -│ ├── images/ -│ │ └── logo.png -│ ├── app.js ← główny punkt wejścia -│ └── style.css ← Twoje style -└── www/ ← katalog publiczny (document root) - ├── assets/ ← tutaj trafią pliki skompilowane - └── index.php -\-- - -Folder `assets/` zawiera Twoje pliki źródłowe, czyli kod, który piszesz. Vite przetworzy te pliki i umieści wersje skompilowane w `www/assets/`. - - -Krok 3: konfiguracja Vite -------------------------- - -Utwórz w katalogu głównym projektu plik `vite.config.ts`. Plik ten mówi Vite, gdzie znaleźć Twoje pliki źródłowe i gdzie umieścić skompilowane. - -Plugin Vite dla Nette przychodzi ze sprytnymi wartościami domyślnymi, które upraszczają konfigurację. Zakłada, że Twoje źródłowe pliki front-endowe są w katalogu `assets/` (opcja `root`), a pliki skompilowane trafiają do `www/assets/` (opcja `outDir`). Musisz podać tylko [punkt wejścia|#Punkty wejścia]: - -```js -import { defineConfig } from 'vite'; -import nette from '@nette/vite-plugin'; - -export default defineConfig({ - plugins: [ - nette({ - entry: 'app.js', - }), - ], -}); -``` - -Pod maską, oprócz `root` i `outDir`, plugin ustawia jeszcze kilka opcji Vite, żeby wszystko do siebie pasowało: `base` na `''` (zasoby serwowane są prosto z document rootu), `build.manifest` na `true` (żeby Nette Assets mogło zmapować zahashowane nazwy plików) i `build.assetsDir` na `''` (pliki skompilowane lądują bezpośrednio w `outDir`, bez podfolderu `static/`). Każdą z nich możesz nadpisać. - -.[note] -Domyślny `outDir` (`www/assets`) wymaga, żeby katalog `www/` już istniał. Jeśli nie istnieje, plugin zatrzymuje się z błędem *"The output directory ... does not exist"*. - -Jeśli chcesz podać inną nazwę katalogu do budowania swoich zasobów, musisz zmienić kilka opcji: - -```js -export default defineConfig({ - root: 'assets', // katalog główny zasobów źródłowych - - build: { - outDir: '../www/assets', // gdzie trafiają pliki skompilowane - }, - - // ... pozostała konfiguracja ... -}); -``` - -.[note] -Ścieżka `outDir` traktowana jest jako względna wobec `root`, dlatego na początku jest `../`. - - -Krok 4: konfiguracja Nette --------------------------- - -Powiedz Nette Assets o Vite w swoim `common.neon`: - -```neon -assets: - mapping: - default: - type: vite # mówi Nette, żeby użyło ViteMappera - path: assets -``` - - -Krok 5: dodanie skryptów ------------------------- - -Dodaj te skrypty do swojego `package.json`: - -```json -{ - "scripts": { - "dev": "vite", - "build": "vite build" - } -} -``` - -Teraz możesz: -- `npm run dev` - uruchomić serwer deweloperski z hot reloadingiem -- `npm run build` - utworzyć zoptymalizowane pliki produkcyjne - - -Punkty wejścia -============== - -**Punkt wejścia** to główny plik, w którym zaczyna się Twoja aplikacja. Z tego pliku importujesz inne pliki (CSS, moduły JavaScriptu, obrazki), tworząc drzewo zależności. Vite podąża za tymi importami i pakuje wszystko razem. - -Przykładowy punkt wejścia `assets/app.js`: - -```js -// Importujemy style -import './style.css' - -// Importujemy moduły JavaScriptu -import netteForms from 'nette-forms'; -import naja from 'naja'; - -// Inicjalizujemy swoją aplikację -netteForms.initOnLoad(); -naja.initialize(); -``` - -W szablonie punkt wejścia możesz wstawić tak: - -```latte -{asset 'app.js'} -``` - -Nette Assets automatycznie generuje wszystkie potrzebne tagi HTML: JavaScript, CSS i wszelkie inne zależności. - - -Wiele punktów wejścia ---------------------- - -Większe aplikacje często potrzebują osobnych punktów wejścia: - -```js -export default defineConfig({ - plugins: [ - nette({ - entry: [ - 'app.js', // strony publiczne - 'admin.js', // panel administracyjny - ], - }), - ], -}); -``` - -Używaj ich w różnych szablonach: - -```latte -{* Na stronach publicznych *} -{asset 'app.js'} - -{* W panelu administracyjnym *} -{asset 'admin.js'} -``` - - -Ważne: pliki źródłowe kontra skompilowane ------------------------------------------ - -Kluczowe jest zrozumienie, że w produkcji możesz wczytywać tylko te pliki, które Vite udostępnia: albo przez swój manifest, albo kopiując je bez zmian z folderu public: - -1. **Punkty wejścia** zdefiniowane w `entry` (wraz z modułami, które dynamicznie importują) i **zasoby, do których odwołuje się JavaScript albo CSS** (obrazki, fonty, ...) - wszystko to zapisane jest w manifeście -2. **Pliki z katalogu `assets/public/`** - tych *nie ma* w manifeście; kopiowane są bez zmian, a `{asset}` znajduje je przez fallback do systemu plików - -**Nie możesz** wczytywać przez `{asset}` dowolnych plików z `assets/`: jeśli plik nie jest nigdzie referencjonowany, nie zostanie skompilowany. Jeśli chcesz, żeby Vite wiedziało o innych zasobach, możesz przenieść je do [folderu public |#Folder public]. - -Zwróć uwagę, że domyślnie Vite osadza inline wszystkie zasoby mniejsze niż 4 kB, więc do tych plików nie będziesz mógł odwołać się bezpośrednio. (Patrz [dokumentacja Vite |https://vite.dev/guide/assets.html]). - -```latte -{* ✓ To działa - to punkt wejścia *} -{asset 'app.js'} - -{* ✓ To działa - jest w assets/public/ *} -{asset 'favicon.ico'} - -{* ✗ To nie zadziała - przypadkowy plik w assets/ *} -{asset 'components/button.js'} -``` - - -Tryb deweloperski -================= - -Tryb deweloperski jest całkowicie opcjonalny, ale po włączeniu daje znaczne korzyści. Główną zaletą jest **Hot Module Replacement (HMR)**: widzisz zmiany natychmiast, bez utraty stanu aplikacji, przez co tworzenie jest znacznie płynniejsze i szybsze. - -Vite to nowoczesne narzędzie budujące, które czyni tworzenie niesamowicie szybkim. W przeciwieństwie do tradycyjnych bundlerów Vite serwuje Twój kod przeglądarce bezpośrednio podczas tworzenia, co oznacza natychmiastowy start serwera niezależnie od wielkości projektu i błyskawiczne aktualizacje. - - -Uruchomienie serwera deweloperskiego ------------------------------------- - -Uruchom serwer deweloperski: - -```shell -npm run dev -``` - -Zobaczysz: - -``` - ➜ Local: http://localhost:5173/ - ➜ Network: use --host to expose -``` - -Zostaw ten terminal otwarty podczas tworzenia. - -Gdy serwer deweloperski działa, plugin zapisuje mały plik sygnalizacyjny `www/assets/.vite/nette.json` zawierający jego URL. Nette Assets po stronie PHP czyta ten plik i przełącza się na wczytywanie z serwera deweloperskiego, gdy jednocześnie: -1. serwer deweloperski Vite działa (plik sygnalizacyjny istnieje) oraz -2. Twoja aplikacja Nette jest w trybie debug. - -Wynik: - -```latte -{asset 'app.js'} -{* Przy tworzeniu: - *} -{* W produkcji: *} -``` - -Żadna konfiguracja nie jest potrzebna, po prostu działa! Żeby wyłączyć wykrywanie albo ustawić URL serwera deweloperskiego ręcznie, patrz [opcja devServer |configuration#Mapper Vite]. - -.[note] -Plik sygnalizacyjny leży w `www/assets/.vite/nette.json` (zaraz obok produkcyjnego `manifest.json`). Możesz go przemianować opcją pluginu `infoFile`, która domyślnie ma wartość `.vite/nette.json`. Jeśli Nette nie wykrywa działającego serwera deweloperskiego, sprawdź, czy plik ten istnieje i wskazuje właściwy URL. - - -Praca na różnych domenach -------------------------- - -Jeśli Twój serwer deweloperski działa na czymś innym niż `localhost` (jak `myapp.local`), możesz natrafić na problemy z CORS (Cross-Origin Resource Sharing). CORS to funkcja bezpieczeństwa przeglądarek, która domyślnie blokuje żądania między różnymi domenami. Gdy Twoja aplikacja PHP działa na `myapp.local`, a Vite na `localhost:5173`, przeglądarka widzi je jako różne domeny i blokuje żądania. - -Masz dwie możliwości, żeby to rozwiązać: - -**Opcja 1: skonfiguruj CORS** - -Najprostszym rozwiązaniem jest dopuszczenie żądań cross-origin z Twojej aplikacji PHP: - -```js -export default defineConfig({ - // ... pozostała konfiguracja ... - - server: { - cors: { - origin: 'http://myapp.local', // URL Twojej aplikacji PHP - }, - }, -}); -``` - -**Opcja 2: uruchom Vite na swojej domenie** - -Drugim rozwiązaniem jest sprawienie, żeby Vite działało na tej samej domenie co Twoja aplikacja PHP. - -```js -export default defineConfig({ - // ... pozostała konfiguracja ... - - server: { - host: 'myapp.local', // tak samo jak Twoja aplikacja PHP - }, -}); -``` - -Właściwie nawet w tym przypadku musisz skonfigurować CORS, bo serwer deweloperski działa na tej samej nazwie hosta, ale na innym porcie. W tym przypadku CORS konfiguruje jednak automatycznie plugin Vite dla Nette. - - -Tworzenie na HTTPS ------------------- - -Jeśli tworzysz na HTTPS, potrzebujesz certyfikatów dla swojego serwera deweloperskiego Vite. Najprościej użyć pluginu generującego certyfikaty automatycznie: - -```shell -npm install -D vite-plugin-mkcert -``` - -Oto jak skonfigurować go w `vite.config.ts`: - -```js -import mkcert from 'vite-plugin-mkcert'; - -export default defineConfig({ - // ... pozostała konfiguracja ... - - plugins: [ - mkcert(), // generuje certyfikaty automatycznie i włącza https - nette(), - ], -}); -``` - -Zwróć uwagę, że jeśli używasz konfiguracji CORS (opcja 1 powyżej), musisz zaktualizować URL origin, żeby używał `https://` zamiast `http://`. - - -Tworzenie w Dockerze --------------------- - -Gdy uruchamiasz Vite wewnątrz kontenera Dockera, dwie rzeczy wymagają uwagi: przeglądarka na Twojej maszynie musi móc dosięgnąć serwera deweloperskiego, a Vite musi wykrywać zmiany plików przez granicę kontenera. - -Najpierw [opublikuj |https://docs.docker.com/get-started/docker-concepts/running-containers/publishing-ports/] port Vite z kontenera i przypnij serwer deweloperski do wszystkich interfejsów, żeby był osiągalny spoza kontenera: - -```js -export default defineConfig({ - // ... pozostała konfiguracja ... - - plugins: [ - nette(), - ], - server: { - host: '0.0.0.0', // nasłuchuj na wszystkich interfejsach (wymagane w kontenerze) - port: 5173, // musi odpowiadać opublikowanemu portowi - strictPort: true, // raczej zawiedź, niż wybieraj inny port - watch: { - usePolling: true, // włącz, jeśli zmiany plików nie są wykrywane na zamontowanych wolumenach - }, - }, -}); -``` - -Plugin zapisuje URL serwera deweloperskiego do `nette.json` dla strony PHP. Ponieważ `host: '0.0.0.0'` nie nadaje się do użycia przez przeglądarkę (dla każdego zasobu przekierowuje na `localhost`), plugin automatycznie przepisuje go w tym URL na `localhost`, żeby zasoby wczytywały się poprawnie. - -Jeśli otwierasz aplikację na własnej domenie zamiast na `localhost`, ustaw opcję pluginu `host` na tę domenę: - -```js - plugins: [ - nette({ host: 'myapp.local' }), // ta sama domena co Twoja aplikacja PHP - ], -``` - -Plugin używa wtedy tego hosta w URL serwera deweloperskiego, dodaje go do origins CORS i wpisuje na białą listę `allowedHosts` Vite, więc działa bez żadnej ręcznej konfiguracji CORS. - -Dla bardziej złożonych konfiguracji, na przykład gdy Vite działa za reverse proxy, gdzie publiczny host, port i protokół różnią się od adresu wewnętrznego, ustaw `server.origin` Vite na pełny publiczny URL. Plugin respektuje go i zapisuje do `nette.json` bez zmian, zamiast wyprowadzać URL z lokalnego gniazda: - -```js - server: { - origin: 'https://myapp.local:8443', // publiczny URL, pod którym przeglądarka sięga po Vite - }, -``` - - -Buildy produkcyjne -================== - -Utwórz zoptymalizowane pliki produkcyjne: - -```shell -npm run build -``` - -Vite: -- Zminifikuje cały JavaScript i CSS -- Podzieli kod na optymalne chunki -- Wygeneruje zahashowane nazwy plików na potrzeby cache bustingu -- Utworzy plik manifestu dla Nette Assets - -Przykładowe wyjście: - -``` -www/assets/ -├── app-4f3a2b1c.js # Twój główny JavaScript (zminifikowany) -├── app-7d8e9f2a.css # Wyodrębniony CSS (zminifikowany) -├── vendor-8c4b5e6d.js # Wspólne zależności -└── .vite/ - └── manifest.json # Mapowanie dla Nette Assets -``` - -Zahashowane nazwy plików zapewniają, że przeglądarki zawsze wczytują najnowszą wersję. - - -Folder public -============= - -Pliki w katalogu `assets/public/` kopiowane są na wyjście bez przetwarzania: - -``` -assets/ -├── public/ -│ ├── favicon.ico -│ ├── robots.txt -│ └── images/ -│ └── og-image.jpg -├── app.js -└── style.css -``` - -Odwołuj się do nich normalnie: - -```latte -{* Te pliki kopiowane są bez zmian *} - - -``` - -Dla plików publicznych możesz używać funkcji FilesystemMappera. Opcja `extension` dotyczy odwołań bez rozszerzenia (np. `{asset 'images/og-image'}` znajdzie najpierw `og-image.webp`): - -```neon -assets: - mapping: - default: - type: vite - path: assets - extension: [webp, jpg, png] # dla odwołań bez rozszerzenia - versioning: true # Dodaje cache busting -``` - -W konfiguracji `vite.config.ts` możesz zmienić folder public opcją `publicDir`. - - -Importy dynamiczne -================== - -Vite automatycznie dzieli kod na potrzeby optymalnego wczytywania. Importy dynamiczne pozwalają wczytywać kod dopiero wtedy, gdy jest faktycznie potrzebny, zmniejszając początkowy rozmiar bundle'a: - -```js -// Wczytujemy ciężkie komponenty na żądanie -button.addEventListener('click', async () => { - let { Chart } = await import('./components/chart.js') - new Chart(data) -}) -``` - -Importy dynamiczne tworzą osobne chunki wczytywane tylko wtedy, gdy są potrzebne. Nazywa się to "code splitting" i jest jedną z najpotężniejszych funkcji Vite. Gdy używasz importów dynamicznych, Vite automatycznie tworzy osobne pliki JavaScript dla każdego dynamicznie importowanego modułu. - -Tag `{asset 'app.js'}` **nie** preładowuje tych dynamicznych chunków automatycznie. To zachowanie zamierzone: nie chcemy pobierać kodu, który może nigdy nie zostać użyty. Chunki pobierane są dopiero wtedy, gdy import dynamiczny zostanie wykonany. - -Jeśli jednak wiesz, że pewne importy dynamiczne są krytyczne i będą wkrótce potrzebne, możesz je preładować: - -```latte -{* Główny punkt wejścia *} -{asset 'app.js'} - -{* Preładowanie krytycznych importów dynamicznych *} -{preload 'components/chart.js'} -``` - -Mówi to przeglądarce, żeby pobrała komponent wykresu w tle, dzięki czemu jest gotowy natychmiast, gdy będzie potrzebny. - - -Wsparcie dla TypeScriptu -======================== - -TypeScript działa od razu: - -```ts -// assets/main.ts -interface User { - name: string - email: string -} - -export function greetUser(user: User): void { - console.log(`Hello, ${user.name}!`) -} -``` - -Odwołuj się do plików TypeScript normalnie (jak przy każdym pliku, `main.ts` musi być punktem wejścia): - -```latte -{asset 'main.ts'} -``` - -Dla pełnego wsparcia TypeScriptu zainstaluj go: - -```shell -npm install -D typescript -``` - - -Dodatkowa konfiguracja Vite -=========================== - -Oto kilka przydatnych opcji konfiguracyjnych Vite ze szczegółowymi wyjaśnieniami: - -```js -export default defineConfig({ - // Katalog główny zawierający zasoby źródłowe - root: 'assets', - - // Folder, którego zawartość kopiowana jest do katalogu wyjściowego bez zmian - // Domyślnie: 'public' (względem 'root') - publicDir: 'public', - - build: { - // Gdzie umieszczać pliki skompilowane (względem 'root') - outDir: '../www/assets', - - // Opróżnić katalog wyjściowy przed budowaniem? - // Przydatne do usunięcia starych plików z poprzednich buildów - emptyOutDir: true, - - // Podkatalog w outDir na generowane chunki i zasoby - // Pomaga to uporządkować strukturę wyjścia - assetsDir: 'static', - - rollupOptions: { - // Punkt(y) wejścia - może być pojedynczy plik albo tablica plików - // Każdy punkt wejścia staje się osobnym bundlem - input: [ - 'app.js', // główna aplikacja - 'admin.js', // panel administracyjny - ], - }, - }, - - server: { - // Host, do którego przypinany jest serwer deweloperski - // Użyj '0.0.0.0', żeby udostępnić go w sieci - host: 'localhost', - - // Port serwera deweloperskiego - port: 5173, - - // Konfiguracja CORS dla żądań cross-origin - cors: { - origin: 'http://myapp.local', - }, - }, - - css: { - // Włącza source mapy CSS przy tworzeniu - devSourcemap: true, - }, - - plugins: [ - nette(), - ], -}); -``` - -.[warning] -Uważaj na ścieżki punktów wejścia w powyższym `rollupOptions.input`: Rollup rozwiązuje ścieżki względne wobec bieżącego katalogu roboczego (katalogu głównego Twojego projektu), a **nie** wobec `root: 'assets'`. Samo `'app.js'` nie będzie więc istnieć w czasie budowania. Albo użyj opcji pluginu `entry` (która rozwiązuje ścieżki punktów wejścia względem `root`), albo zapisz ścieżki względem katalogu głównego projektu, np. `'assets/app.js'`. - -I to wszystko! Masz teraz nowoczesny system budowania zintegrowany z Nette Assets. diff --git a/assets/ru/@home.texy b/assets/ru/@home.texy deleted file mode 100644 index 2f70db4b47..0000000000 --- a/assets/ru/@home.texy +++ /dev/null @@ -1,467 +0,0 @@ -Nette Assets -************ - -
    - -Устали вручную управлять статическими файлами в своих веб-приложениях? Забудьте о жёстко прописанных путях, возне со сбросом кеша и заботах о версиях файлов. Nette Assets меняет то, как вы работаете с изображениями, таблицами стилей, скриптами и другими статическими ресурсами. - -- **Умное версионирование** обеспечивает, что браузеры всегда загружают свежие файлы -- **Автоматическое определение** типов файлов и размеров -- **Бесшовная интеграция с Latte** через интуитивные теги -- **Гибкая архитектура** с поддержкой файловых систем, CDN и Vite -- **Ленивая загрузка** ради максимальной производительности - -
    - - -Зачем нужен Nette Assets? -========================= - -Работа со статическими файлами часто означает повторяющийся код, в котором легко ошибиться. Вы вручную составляете URL, добавляете параметры версии для сброса кеша и по-разному обрабатываете разные типы файлов. Это приводит к коду вроде такого: - -```latte -Logo - -``` - -С Nette Assets вся эта сложность исчезает: - -```latte -{* Всё автоматизировано - URL, версионирование, размеры *} - - - -{* Или просто *} -{asset 'css/style.css'} -``` - -Вот и всё! Библиотека автоматически: -- добавляет параметры версии по времени изменения файла -- определяет размеры изображений и включает их в HTML -- порождает правильный HTML-элемент для каждого типа файла -- справляется и со средой разработки, и с продакшном - - -Установка -========= - -Установите Nette Assets с помощью [Composer|best-practices:composer]: - -```shell -composer require nette/assets -``` - -Он требует PHP 8.1 или новее и прекрасно работает с Nette Framework, но может использоваться и самостоятельно. - - -Первые шаги -=========== - -Nette Assets работает сразу, без всякой настройки. Поместите свои статические файлы в каталог `www/assets/` и начинайте их использовать: - -```latte -{* Показываем изображение с автоматическими размерами *} -{asset 'logo.png'} - -{* Подключаем таблицу стилей с версионированием *} -{asset 'style.css'} - -{* Загружаем скрипт *} -{asset 'app.js'} -``` - -Для большего контроля над порождаемым HTML используйте атрибут `n:asset` или функцию `asset()`. - - -Как это работает -================ - -Nette Assets построен вокруг трёх основных понятий, которые делают его мощным и при этом простым в использовании: - - -Ресурсы: ваши файлы становятся умнее ------------------------------------- - -**Ресурс** (asset) представляет любой статический файл в вашем приложении. Каждый файл становится объектом с полезными свойствами только для чтения: - -```php -$image = $assets->getAsset('photo.jpg'); -echo $image->url; // '/assets/photo.jpg?v=1699123456' -echo $image->file; // '/var/www/assets/photo.jpg' (локальный путь либо null) -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' -``` - -Разные типы файлов дают разные свойства: -- **Изображения**: ширина, высота, альтернативный текст, ленивая загрузка -- **Скрипты**: тип модуля, хеши целостности, crossorigin -- **Таблицы стилей**: медиазапросы, целостность -- **Аудио и видео**: длительность, размеры (только видео) -- **Шрифты**: правильная предзагрузка с CORS - -Библиотека автоматически определяет типы файлов и создаёт подходящий класс ресурса. - - -Мапперы: откуда берутся файлы ------------------------------ - -**Маппер** знает, как найти файлы и создать для них URL. У вас может быть несколько мапперов для разных задач: локальные файлы, CDN, облачное хранилище или инструменты сборки (у каждого из них есть имя). Встроенный `FilesystemMapper` занимается локальными файлами, а `ViteMapper` интегрируется с современными инструментами сборки. - -Мапперы определяются в [конфигурации |configuration]. - - -Реестр: ваш основной интерфейс ------------------------------- - -**Реестр** управляет всеми мапперами и предоставляет основной API: - -```php -// Внедряем реестр в свой сервис -public function __construct( - private Nette\Assets\Registry $assets -) {} -``` - -```php -// Получаем ресурсы из разных мапперов -$logo = $this->assets->getAsset('images:logo.png'); // маппер 'images' -$app = $this->assets->getAsset('app:main.js'); // маппер 'app' -$style = $this->assets->getAsset('style.css'); // использует маппер по умолчанию -``` - -Реестр автоматически выбирает нужный маппер и кеширует результаты ради производительности. - - -Работа с ресурсами в PHP -======================== - -Реестр предоставляет два метода получения ресурсов: - -```php -// Выбрасывает Nette\Assets\AssetNotFoundException, если файла нет -$logo = $assets->getAsset('logo.png'); - -// Возвращает null, если файла нет -$banner = $assets->tryGetAsset('banner.jpg'); -if ($banner) { - echo $banner->url; -} -``` - - -Указание мапперов ------------------ - -Вы можете явно выбрать, какой маппер использовать: - -```php -// Использовать маппер по умолчанию -$file = $assets->getAsset('document.pdf'); - -// Использовать конкретный маппер через приставку -$image = $assets->getAsset('images:photo.jpg'); - -// Использовать конкретный маппер через запись массивом -$script = $assets->getAsset(['scripts', 'app.js']); -``` - - -Свойства и типы ресурсов ------------------------- - -Каждый тип ресурса даёт подходящие свойства только для чтения: - -```php -// Свойства изображения -$image = $assets->getAsset('photo.jpg'); -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' - -// Свойства скрипта -$script = $assets->getAsset('app.js'); -echo $script->type; // null ('module' для точек входа Vite) - -// Свойства аудио -$audio = $assets->getAsset('song.mp3'); -echo $audio->duration; // длительность в секундах - -// Все ресурсы можно привести к строке (вернётся URL) -$url = (string) $assets->getAsset('document.pdf'); -``` - -.[note] -Свойства вроде размеров или длительности загружаются лениво, только при обращении, благодаря чему библиотека остаётся быстрой. - -.[tip] -Для точного статического анализа установите расширение [nette/phpstan-rules |tools:phpstan-rules#Assets]. PHPStan тогда знает конкретный тип каждого ресурса, так что `getAsset('photo.jpg')` понимается как `ImageAsset`, а обращение к `->width` не вызывает ошибки. - - -Использование ресурсов в шаблонах Latte -======================================= - -Nette Assets предлагает интуитивную интеграцию с [Latte|latte:] через теги и функции. - - -`{asset}` ---------- - -Тег `{asset}` отрисовывает целые HTML-элементы: - -```latte -{* Отрисует: *} -{asset 'hero.jpg'} - -{* Отрисует: *} -{asset 'app.js'} - -{* Отрисует: *} -{asset 'style.css'} -``` - -Тег автоматически: -- определяет тип ресурса и порождает подходящий HTML -- добавляет версионирование для сброса кеша -- добавляет размеры для изображений -- задаёт правильные атрибуты (type, media и т. д.) - -Внутри HTML-атрибутов и внутри элементов ` -``` - - -Собственные мапперы -=================== - -Создавайте собственные мапперы для особых нужд вроде облачного хранилища или динамического порождения: - -```php -use Nette\Assets\Mapper; -use Nette\Assets\Asset; -use Nette\Assets\Helpers; - -class CloudStorageMapper implements Mapper -{ - public function __construct( - private CloudClient $client, - private string $bucket, - ) {} - - public function getAsset(string $reference, array $options = []): Asset - { - if (!$this->client->exists($this->bucket, $reference)) { - throw new Nette\Assets\AssetNotFoundException("Asset '$reference' not found"); - } - - $url = $this->client->getPublicUrl($this->bucket, $reference); - return Helpers::createAssetFromUrl($url); - } -} -``` - -Регистрируем в конфигурации: - -```neon -assets: - mapping: - cloud: CloudStorageMapper(@cloudClient, 'my-bucket') -``` - -Используем как любой другой маппер: - -```latte -{asset 'cloud:user-uploads/photo.jpg'} -``` - -Метод `Helpers::createAssetFromUrl()` автоматически создаёт правильный тип ресурса по расширению файла. - -Типы ресурсов, реализующие интерфейс `Nette\Assets\HtmlRenderable` (изображения, скрипты, стили и так далее), может отрисовать `{asset}` как целый HTML-элемент. Остальные типы файлов становятся `GenericAsset` (например, PDF), который нельзя отрисовать как HTML-элемент, но он всё равно предоставляет URL (и другие метаданные). Попытка отрисовать такой ресурс как HTML-элемент выбрасывает `Nette\InvalidArgumentException`; его URL внутри атрибута использовать по-прежнему можно. - - -Дополнительные материалы -======================== - -- [Nette Assets: наконец-то единый API для всего, от изображений до Vite |https://blog.nette.org/en/introducing-nette-assets] diff --git a/assets/ru/@left-menu.texy b/assets/ru/@left-menu.texy deleted file mode 100644 index 2625f335a3..0000000000 --- a/assets/ru/@left-menu.texy +++ /dev/null @@ -1,14 +0,0 @@ -Nette Assets -************ -- [Первые шаги |@home] -- [Vite |vite] -- [Конфигурация|configuration] - - -Дополнительные материалы -************************ -- [Документация Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Лучшие практики |best-practices:] -- [Устранение неполадок |nette:troubleshooting] diff --git a/assets/ru/@meta.texy b/assets/ru/@meta.texy deleted file mode 100644 index 7f329adfce..0000000000 --- a/assets/ru/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документация Nette}} diff --git a/assets/ru/configuration.texy b/assets/ru/configuration.texy deleted file mode 100644 index a49ba9fb82..0000000000 --- a/assets/ru/configuration.texy +++ /dev/null @@ -1,195 +0,0 @@ -Конфигурация Assets -******************* - -.[perex] -Обзор параметров конфигурации Nette Assets. - - -```neon -assets: - # базовый путь для разрешения относительных путей мапперов - basePath: ... # (string) по умолчанию %wwwDir% - - # базовый URL для разрешения относительных URL мапперов - baseUrl: ... # (string) по умолчанию %baseUrl% - - # включить версионирование ресурсов глобально? - versioning: ... # (bool) по умолчанию true - - # определяет мапперы ресурсов - mapping: ... # (array) по умолчанию путь 'assets' -``` - -`basePath` задаёт каталог файловой системы по умолчанию для разрешения относительных путей в мапперах. По умолчанию используется веб-каталог (`%wwwDir%`). - -`baseUrl` задаёт приставку URL по умолчанию для разрешения относительных URL в мапперах. По умолчанию используется корневой URL (`%baseUrl%`). - -Параметр `versioning` глобально управляет тем, добавляются ли к URL ресурсов параметры версии для сброса кеша. Отдельные мапперы могут эту настройку переопределить. - - -Мапперы -------- - -Мапперы можно настроить тремя способами: простой строковой записью, подробной записью массивом или как сервис (записью сущности `ClassName(...)` либо `@serviceName()`). - -Проще всего определить маппер так: - -```neon -assets: - mapping: - default: assets # Создаёт файловый маппер для %wwwDir%/assets/ - images: img # Создаёт файловый маппер для %wwwDir%/img/ - scripts: js # Создаёт файловый маппер для %wwwDir%/js/ -``` - -Каждый маппер создаёт `FilesystemMapper`, который: -- ищет файлы в `%wwwDir%/` -- порождает URL вида `%baseUrl%/` -- наследует глобальную настройку версионирования - - -Для большего контроля используйте подробную запись: - -```neon -assets: - mapping: - images: - # каталог, где хранятся файлы - path: ... # (string) необязательно, по умолчанию базовый путь (basePath) - - # приставка URL для порождаемых ссылок - url: ... # (string) необязательно, по умолчанию значение path - - # включить версионирование для этого маппера? - versioning: ... # (bool) необязательно, наследует глобальную настройку - - # автоматически дописывать расширение или расширения при поиске файлов - extension: ... # (string|array) необязательно, по умолчанию null -``` - -Как разрешаются значения конфигурации: - -Разрешение пути: - - относительные пути разрешаются от `basePath` (или от `%wwwDir%`, если `basePath` не задан) - - абсолютные пути используются как есть - -Разрешение URL: - - относительные URL разрешаются от `baseUrl` (или от `%baseUrl%`, если `baseUrl` не задан) - - абсолютные URL (со схемой или `//`) используются как есть (URL с `//` берёт схему из `baseUrl`) - - если `url` не указан, используется значение `path` - - -```neon -assets: - basePath: /var/www/project/www - baseUrl: https://example.com/assets - - mapping: - # Относительный путь и URL - images: - path: img # Разрешится в: /var/www/project/www/img - url: images # Разрешится в: https://example.com/assets/images - - # Абсолютный путь и URL - uploads: - path: /var/shared/uploads # Используется как есть: /var/shared/uploads - url: https://cdn.example.com # Используется как есть: https://cdn.example.com - - # Указан только path - styles: - path: css # Путь: /var/www/project/www/css - # URL: https://example.com/assets/css -``` - - -Собственные мапперы -------------------- - -Для собственных мапперов сошлитесь на существующий сервис через `@serviceName` либо определите его прямо через `ClassName(аргументы)` или голое имя класса: - -```neon -services: - s3mapper: App\Assets\S3Mapper(%s3.bucket%) - -assets: - mapping: - cloud: @s3mapper - database: App\Assets\DatabaseMapper(@database.connection) -``` - - -Маппер Vite ------------ - -Мапперу Vite достаточно добавить `type: vite`. Вот полный список параметров конфигурации: - -```neon -assets: - mapping: - default: - # тип маппера (обязателен для Vite) - type: vite # (string) обязательно, должно быть 'vite' - - # каталог вывода сборки Vite - path: ... # (string) необязательно, по умолчанию базовый путь (basePath) - - # приставка URL для собранных ресурсов - url: ... # (string) необязательно, по умолчанию значение path - - # расположение файла манифеста Vite - manifest: ... # (string) необязательно, относительно path, по умолчанию /.vite/manifest.json - - # настройка dev-сервера Vite - devServer: ... # (bool|string) необязательно, по умолчанию true - - # версионирование файлов из публичного каталога - versioning: ... # (bool) необязательно, наследует глобальную настройку - - # автоматическое расширение для файлов из публичного каталога - extension: ... # (string|array) необязательно, по умолчанию null -``` - -Параметр `devServer` управляет тем, как ресурсы загружаются при разработке: - -- `true` (по умолчанию) - автоматически определяет запущенный dev-сервер Vite (по файлу `.vite/nette.json`, который плагин Vite для Nette создаёт в каталоге сборки). Если dev-сервер запущен **и ваше приложение в режиме отладки**, ресурсы загружаются из него с поддержкой горячей замены модулей. Если dev-сервер не запущен, ресурсы загружаются из собранных файлов в публичном каталоге. -- `false` - полностью отключает интеграцию с dev-сервером. Ресурсы всегда загружаются из собранных файлов. -- Собственный URL (например, `https://localhost:5173`) - вручную задаёт URL dev-сервера вместе с протоколом и портом. Полезно, когда dev-сервер работает на другом хосте или порте. Как и автоопределение, это действует только в режиме отладки; в продакшне всегда используются собранные файлы. - -Параметры `versioning` и `extension` относятся только к файлам из публичного каталога Vite, которые сам Vite не обрабатывает. - - -Настройка вручную ------------------ - -Когда вы не используете Nette DI, настраивайте мапперы вручную: - -```php -use Nette\Assets\Registry; -use Nette\Assets\FilesystemMapper; -use Nette\Assets\ViteMapper; - -$registry = new Registry; - -// Добавляем файловый маппер -$registry->addMapper('images', new FilesystemMapper( - baseUrl: 'https://example.com/img', - basePath: __DIR__ . '/www/img', - extensions: ['webp', 'jpg', 'png'], - versioning: true, -)); - -// Добавляем маппер Vite -$registry->addMapper('app', new ViteMapper( - baseUrl: '/build', - basePath: __DIR__ . '/www/build', - manifestPath: __DIR__ . '/www/build/.vite/manifest.json', - devServer: 'https://localhost:5173', -)); -``` - -Любой зарегистрированный маппер можно получить по имени методом `getMapper()`: - -```php -$mapper = $registry->getMapper('images'); // вернёт зарегистрированный маппер -$default = $registry->getMapper(); // вернёт маппер 'default' -``` diff --git a/assets/ru/vite.texy b/assets/ru/vite.texy deleted file mode 100644 index 8818c83bbe..0000000000 --- a/assets/ru/vite.texy +++ /dev/null @@ -1,567 +0,0 @@ -Интеграция с Vite -***************** - -
    - -Современные JavaScript-приложения требуют продуманных инструментов сборки. Nette Assets предлагает первоклассную интеграцию с [Vite |https://vite.dev/], сборщиком фронтенда нового поколения. Получите молниеносную разработку с горячей заменой модулей (HMR) и оптимизированные сборки для продакшна без возни с настройкой. - -- **Никакой настройки** - автоматический мост между Vite и PHP-шаблонами -- **Полное управление зависимостями** - один тег занимается всеми ресурсами -- **Горячая замена модулей** - мгновенные обновления JavaScript и CSS -- **Оптимизированные продакшн-сборки** - разделение кода и tree shaking - -
    - - -Nette Assets бесшовно интегрируется с Vite, так что вы получаете все эти преимущества, а шаблоны пишете как обычно. - - -Настройка Vite -============== - -Настроим Vite шаг за шагом. Не переживайте, если вы новичок в инструментах сборки, мы всё объясним! - - -Шаг 1: установка Vite ---------------------- - -Сначала установите в свой проект Vite и плагин для Nette: - -```shell -npm install -D vite @nette/vite-plugin -``` - -Так устанавливается Vite и особый плагин, который помогает Vite прекрасно работать с Nette. - - -Шаг 2: структура проекта ------------------------- - -Стандартный подход - разместить исходные файлы ресурсов в папке `assets/` в корне проекта, а скомпилированные версии в `www/assets/`: - -/--pre -web-project/ -├── assets/ ← исходные файлы (SCSS, TypeScript, исходные изображения) -│ ├── public/ ← статические файлы (копируются как есть) -│ │ └── favicon.ico -│ ├── images/ -│ │ └── logo.png -│ ├── app.js ← главная точка входа -│ └── style.css ← ваши стили -└── www/ ← публичный каталог (document root) - ├── assets/ ← сюда попадут скомпилированные файлы - └── index.php -\-- - -Папка `assets/` содержит ваши исходные файлы - код, который вы пишете. Vite обработает эти файлы и положит скомпилированные версии в `www/assets/`. - - -Шаг 3: настройка Vite ---------------------- - -Создайте в корне проекта файл `vite.config.ts`. Этот файл говорит Vite, где искать исходные файлы и куда класть скомпилированные. - -У плагина Vite для Nette есть умные значения по умолчанию, которые упрощают настройку. Он предполагает, что исходные файлы фронтенда лежат в каталоге `assets/` (параметр `root`), а скомпилированные попадают в `www/assets/` (параметр `outDir`). Вам нужно указать только [точку входа|#Точки входа]: - -```js -import { defineConfig } from 'vite'; -import nette from '@nette/vite-plugin'; - -export default defineConfig({ - plugins: [ - nette({ - entry: 'app.js', - }), - ], -}); -``` - -Под капотом, кроме `root` и `outDir`, плагин задаёт ещё несколько параметров Vite, чтобы всё сходилось: `base` в `''` (ресурсы отдаются прямо из document root), `build.manifest` в `true` (чтобы Nette Assets мог сопоставлять имена файлов с хешами) и `build.assetsDir` в `''` (скомпилированные файлы попадают прямо в `outDir`, без подпапки `static/`). Любой из них можно переопределить. - -.[note] -`outDir` по умолчанию (`www/assets`) требует, чтобы каталог `www/` уже существовал. Если его нет, плагин останавливается с ошибкой *"The output directory ... does not exist"*. - -Если вы хотите собирать ресурсы в каталог с другим именем, вам потребуется изменить несколько параметров: - -```js -export default defineConfig({ - root: 'assets', // корневой каталог исходных ресурсов - - build: { - outDir: '../www/assets', // куда попадают скомпилированные файлы - }, - - // ... остальная конфигурация ... -}); -``` - -.[note] -Путь `outDir` считается относительно `root`, поэтому в начале стоит `../`. - - -Шаг 4: настройка Nette ----------------------- - -Расскажите Nette Assets о Vite в своём `common.neon`: - -```neon -assets: - mapping: - default: - type: vite # говорит Nette использовать ViteMapper - path: assets -``` - - -Шаг 5: добавление скриптов --------------------------- - -Добавьте в свой `package.json` эти скрипты: - -```json -{ - "scripts": { - "dev": "vite", - "build": "vite build" - } -} -``` - -Теперь вы можете: -- `npm run dev` - запустить сервер разработки с горячей перезагрузкой -- `npm run build` - создать оптимизированные файлы для продакшна - - -Точки входа -=========== - -**Точка входа** - главный файл, с которого начинается ваше приложение. Из этого файла вы импортируете другие файлы (CSS, модули JavaScript, изображения), создавая дерево зависимостей. Vite идёт по этим импортам и собирает всё вместе. - -Пример точки входа `assets/app.js`: - -```js -// Импортируем стили -import './style.css' - -// Импортируем модули JavaScript -import netteForms from 'nette-forms'; -import naja from 'naja'; - -// Инициализируем приложение -netteForms.initOnLoad(); -naja.initialize(); -``` - -В шаблоне точку входа можно вставить так: - -```latte -{asset 'app.js'} -``` - -Nette Assets автоматически породит все необходимые HTML-теги: JavaScript, CSS и любые другие зависимости. - - -Несколько точек входа ---------------------- - -Более крупным приложениям часто нужны отдельные точки входа: - -```js -export default defineConfig({ - plugins: [ - nette({ - entry: [ - 'app.js', // публичные страницы - 'admin.js', // административная панель - ], - }), - ], -}); -``` - -Используйте их в разных шаблонах: - -```latte -{* На публичных страницах *} -{asset 'app.js'} - -{* В административной панели *} -{asset 'admin.js'} -``` - - -Важно: исходные и скомпилированные файлы ----------------------------------------- - -Принципиально важно понимать, что в продакшне вы можете загружать только файлы, которые Vite делает доступными: либо через свой манифест, либо копируя их без изменений из публичной папки: - -1. **Точки входа**, определённые в `entry` (включая модули, которые они динамически импортируют), и **ресурсы, на которые ссылаются из JavaScript или CSS** (изображения, шрифты, ...) - всё это записывается в манифест -2. **Файлы из каталога `assets/public/`** - их в манифесте *нет*; они копируются как есть, и `{asset}` находит их запасным путём через файловую систему - -Вы **не можете** загружать через `{asset}` произвольные файлы из `assets/`: если на файл нигде нет ссылки, он не будет скомпилирован. Если вы хотите, чтобы Vite знал о других ресурсах, перенесите их в [публичную папку |#Публичная папка]. - -Учтите, что по умолчанию Vite встраивает все ресурсы меньше 4 КБ, так что сослаться на эти файлы напрямую вы не сможете. (См. [документацию Vite |https://vite.dev/guide/assets.html]). - -```latte -{* ✓ Это работает - это точка входа *} -{asset 'app.js'} - -{* ✓ Это работает - файл в assets/public/ *} -{asset 'favicon.ico'} - -{* ✗ Это не сработает - произвольный файл в assets/ *} -{asset 'components/button.js'} -``` - - -Режим разработки -================ - -Режим разработки совершенно необязателен, но, если его включить, он даёт существенные преимущества. Главное из них - **горячая замена модулей (HMR)**: изменения видны мгновенно и без потери состояния приложения, что делает разработку намного плавнее и быстрее. - -Vite - современный инструмент сборки, который делает разработку невероятно быстрой. В отличие от традиционных сборщиков, Vite при разработке отдаёт ваш код прямо в браузер, а значит сервер запускается мгновенно независимо от размера проекта, и обновления происходят молниеносно. - - -Запуск сервера разработки -------------------------- - -Запустите сервер разработки: - -```shell -npm run dev -``` - -Вы увидите: - -``` - ➜ Local: http://localhost:5173/ - ➜ Network: use --host to expose -``` - -Держите этот терминал открытым во время разработки. - -Пока dev-сервер работает, плагин записывает небольшой сигнальный файл `www/assets/.vite/nette.json` с его URL. Nette Assets на стороне PHP читает этот файл и переключается на загрузку с dev-сервера, когда выполняется и то и другое: -1. dev-сервер Vite запущен (сигнальный файл существует) и -2. ваше приложение на Nette находится в режиме отладки. - -Результат: - -```latte -{asset 'app.js'} -{* При разработке: - *} -{* В продакшне: *} -``` - -Никакой настройки не нужно, оно просто работает! Чтобы отключить определение или задать URL dev-сервера вручную, смотрите [параметр devServer |configuration#Маппер Vite]. - -.[note] -Сигнальный файл живёт в `www/assets/.vite/nette.json` (прямо рядом с продакшн-файлом `manifest.json`). Переименовать его можно параметром плагина `infoFile`, который по умолчанию равен `.vite/nette.json`. Если Nette не подхватывает запущенный dev-сервер, проверьте, что этот файл существует и указывает на верный URL. - - -Работа на разных доменах ------------------------- - -Если ваш сервер разработки работает не на `localhost` (например, на `myapp.local`), вы можете столкнуться с проблемами CORS (Cross-Origin Resource Sharing). CORS - механизм безопасности веб-браузеров, который по умолчанию блокирует запросы между разными доменами. Когда ваше PHP-приложение работает на `myapp.local`, а Vite на `localhost:5173`, браузер видит их как разные домены и блокирует запросы. - -Решить это можно двумя способами: - -**Вариант 1: настроить CORS** - -Проще всего разрешить запросы с другого источника из вашего PHP-приложения: - -```js -export default defineConfig({ - // ... остальная конфигурация ... - - server: { - cors: { - origin: 'http://myapp.local', // URL вашего PHP-приложения - }, - }, -}); -``` - -**Вариант 2: запустить Vite на своём домене** - -Другое решение - сделать так, чтобы Vite работал на том же домене, что и ваше PHP-приложение. - -```js -export default defineConfig({ - // ... остальная конфигурация ... - - server: { - host: 'myapp.local', // так же, как у вашего PHP-приложения - }, -}); -``` - -На самом деле даже в этом случае CORS настроить нужно, потому что dev-сервер работает на том же имени хоста, но на другом порту. Однако в этом случае CORS настраивается автоматически плагином Vite для Nette. - - -Разработка по HTTPS -------------------- - -Если вы разрабатываете по HTTPS, вашему серверу разработки Vite нужны сертификаты. Проще всего воспользоваться плагином, который порождает сертификаты автоматически: - -```shell -npm install -D vite-plugin-mkcert -``` - -Вот как настроить его в `vite.config.ts`: - -```js -import mkcert from 'vite-plugin-mkcert'; - -export default defineConfig({ - // ... остальная конфигурация ... - - plugins: [ - mkcert(), // порождает сертификаты автоматически и включает https - nette(), - ], -}); -``` - -Обратите внимание, что если вы используете настройку CORS (вариант 1 выше), вам нужно поправить URL origin, чтобы он использовал `https://` вместо `http://`. - - -Разработка в Docker -------------------- - -Когда вы запускаете Vite внутри контейнера Docker, нужно позаботиться о двух вещах: браузер на вашей машине должен добираться до dev-сервера, а Vite должен замечать изменения файлов через границу контейнера. - -Сначала [опубликуйте |https://docs.docker.com/get-started/docker-concepts/running-containers/publishing-ports/] порт Vite из контейнера и привяжите dev-сервер ко всем интерфейсам, чтобы он был доступен снаружи контейнера: - -```js -export default defineConfig({ - // ... остальная конфигурация ... - - plugins: [ - nette(), - ], - server: { - host: '0.0.0.0', // слушаем на всех интерфейсах (в контейнере обязательно) - port: 5173, // должен совпадать с опубликованным портом - strictPort: true, // лучше упасть, чем выбрать другой порт - watch: { - usePolling: true, // включите, если изменения файлов на смонтированных томах не замечаются - }, - }, -}); -``` - -Плагин записывает URL dev-сервера в `nette.json` для стороны PHP. Поскольку `host: '0.0.0.0'` браузеру не годится (он перенаправляет на `localhost` для каждого ресурса), плагин автоматически переписывает его в этом URL на `localhost`, чтобы ресурсы загружались правильно. - -Если вы открываете приложение на собственном домене, а не на `localhost`, задайте параметр плагина `host` равным этому домену: - -```js - plugins: [ - nette({ host: 'myapp.local' }), // тот же домен, что и у вашего PHP-приложения - ], -``` - -Плагин тогда использует этот хост для URL dev-сервера, добавляет его в источники CORS и вносит в белый список `allowedHosts` в Vite, так что всё работает без ручной настройки CORS. - -Для более сложных схем, например когда Vite работает за обратным прокси, у которого публичный хост, порт и протокол отличаются от внутреннего адреса, задайте в Vite `server.origin` равным полному публичному URL. Плагин его учитывает и записывает в `nette.json` как есть, вместо того чтобы выводить URL из локального сокета: - -```js - server: { - origin: 'https://myapp.local:8443', // публичный URL, по которому браузер добирается до Vite - }, -``` - - -Сборки для продакшна -==================== - -Создайте оптимизированные файлы для продакшна: - -```shell -npm run build -``` - -Vite: -- минифицирует весь JavaScript и CSS -- разделит код на оптимальные куски -- породит имена файлов с хешами для сброса кеша -- создаст файл манифеста для Nette Assets - -Пример вывода: - -``` -www/assets/ -├── app-4f3a2b1c.js # Ваш основной JavaScript (минифицированный) -├── app-7d8e9f2a.css # Извлечённый CSS (минифицированный) -├── vendor-8c4b5e6d.js # Общие зависимости -└── .vite/ - └── manifest.json # Сопоставление для Nette Assets -``` - -Имена файлов с хешами обеспечивают, что браузеры всегда загружают свежую версию. - - -Публичная папка -=============== - -Файлы из каталога `assets/public/` копируются в вывод без обработки: - -``` -assets/ -├── public/ -│ ├── favicon.ico -│ ├── robots.txt -│ └── images/ -│ └── og-image.jpg -├── app.js -└── style.css -``` - -Ссылайтесь на них обычным образом: - -```latte -{* Эти файлы копируются как есть *} - - -``` - -Для публичных файлов можно использовать возможности FilesystemMapper. Параметр `extension` относится к ссылкам без расширения (например, `{asset 'images/og-image'}` сначала найдёт `og-image.webp`): - -```neon -assets: - mapping: - default: - type: vite - path: assets - extension: [webp, jpg, png] # для ссылок без расширения - versioning: true # Добавляет сброс кеша -``` - -В конфигурации `vite.config.ts` публичную папку можно сменить параметром `publicDir`. - - -Динамические импорты -==================== - -Vite автоматически разделяет код ради оптимальной загрузки. Динамические импорты позволяют загружать код только тогда, когда он действительно нужен, уменьшая размер начального пакета: - -```js -// Загружаем тяжёлые компоненты по требованию -button.addEventListener('click', async () => { - let { Chart } = await import('./components/chart.js') - new Chart(data) -}) -``` - -Динамические импорты создают отдельные куски, которые загружаются только при надобности. Это называется "разделением кода" и является одной из самых мощных возможностей Vite. Когда вы используете динамические импорты, Vite автоматически создаёт отдельные файлы JavaScript для каждого динамически импортируемого модуля. - -Тег `{asset 'app.js'}` **не** предзагружает эти динамические куски автоматически. Это намеренное поведение: мы не хотим скачивать код, который, возможно, никогда не понадобится. Куски скачиваются, только когда выполняется динамический импорт. - -Однако если вы знаете, что определённые динамические импорты критичны и скоро понадобятся, вы можете их предзагрузить: - -```latte -{* Главная точка входа *} -{asset 'app.js'} - -{* Предзагружаем критичные динамические импорты *} -{preload 'components/chart.js'} -``` - -Это говорит браузеру скачать компонент диаграммы в фоне, чтобы он был готов сразу, как только понадобится. - - -Поддержка TypeScript -==================== - -TypeScript работает сразу: - -```ts -// assets/main.ts -interface User { - name: string - email: string -} - -export function greetUser(user: User): void { - console.log(`Hello, ${user.name}!`) -} -``` - -Ссылайтесь на файлы TypeScript обычным образом (как и с любым файлом, `main.ts` должен быть точкой входа): - -```latte -{asset 'main.ts'} -``` - -Для полной поддержки TypeScript установите его: - -```shell -npm install -D typescript -``` - - -Дополнительная настройка Vite -============================= - -Вот несколько полезных параметров конфигурации Vite с подробными пояснениями: - -```js -export default defineConfig({ - // Корневой каталог с исходными ресурсами - root: 'assets', - - // Папка, содержимое которой копируется в каталог вывода как есть - // По умолчанию: 'public' (относительно 'root') - publicDir: 'public', - - build: { - // Куда класть скомпилированные файлы (относительно 'root') - outDir: '../www/assets', - - // Очищать каталог вывода перед сборкой? - // Полезно, чтобы убрать старые файлы прошлых сборок - emptyOutDir: true, - - // Подкаталог внутри outDir для порождённых кусков и ресурсов - // Помогает упорядочить структуру вывода - assetsDir: 'static', - - rollupOptions: { - // Точка входа или несколько - может быть одним файлом или массивом файлов - // Каждая точка входа становится отдельным пакетом - input: [ - 'app.js', // основное приложение - 'admin.js', // административная панель - ], - }, - }, - - server: { - // Хост, к которому привязывается dev-сервер - // Используйте '0.0.0.0', чтобы открыть его в сеть - host: 'localhost', - - // Порт dev-сервера - port: 5173, - - // Настройка CORS для запросов с другого источника - cors: { - origin: 'http://myapp.local', - }, - }, - - css: { - // Включить карты исходников CSS при разработке - devSourcemap: true, - }, - - plugins: [ - nette(), - ], -}); -``` - -.[warning] -Осторожно с путями точек входа в `rollupOptions.input` выше: Rollup разрешает относительные пути относительно текущего рабочего каталога (корня вашего проекта), а **не** относительно `root: 'assets'`. Так что голого `'app.js'` во время сборки не окажется. Либо используйте параметр плагина `entry` (который разрешает пути точек входа относительно `root`), либо пишите пути относительно корня проекта, например `'assets/app.js'`. - -Вот и всё! Теперь у вас есть современная система сборки, интегрированная с Nette Assets. diff --git a/assets/tr/@home.texy b/assets/tr/@home.texy deleted file mode 100644 index 6bd13bc926..0000000000 --- a/assets/tr/@home.texy +++ /dev/null @@ -1,467 +0,0 @@ -Nette Assets -************ - -
    - -Web uygulamalarınızdaki statik dosyaları elle yönetmekten bıktınız mı? Yolları koda gömmeyi, önbellek geçersizleştirmeyle uğraşmayı ya da dosya sürümlemesi için endişelenmeyi unutun. Nette Assets, görsellerle, stil sayfalarıyla, betiklerle ve diğer statik kaynaklarla çalışma biçiminizi dönüştürür. - -- **Akıllı sürümleme** tarayıcıların her zaman en son dosyaları yüklemesini sağlar -- **Dosya tiplerinin ve boyutlarının otomatik algılanması** -- Sezgisel etiketlerle **kusursuz Latte entegrasyonu** -- Dosya sistemlerini, CDN'leri ve Vite'ı destekleyen **esnek mimari** -- En iyi başarım için **tembel yükleme** - -
    - - -Neden Nette Assets? -=================== - -Statik dosyalarla çalışmak sıklıkla yinelenen, hataya açık kod anlamına gelir. URL'leri elle kurar, önbellek geçersizleştirme için sürüm parametreleri ekler ve farklı dosya tiplerini farklı biçimde ele alırsınız. Bu, şöyle bir koda yol açar: - -```latte -Logo - -``` - -Nette Assets ile tüm bu karmaşıklık ortadan kalkar: - -```latte -{* Her şey otomatik - URL, sürümleme, boyutlar *} - - - -{* Ya da yalnızca *} -{asset 'css/style.css'} -``` - -Hepsi bu! Kütüphane otomatik olarak: -- Dosyanın değiştirilme zamanına göre sürüm parametreleri ekler -- Görsel boyutlarını algılar ve onları HTML'e ekler -- Her dosya tipi için doğru HTML elemanını üretir -- Hem geliştirme hem üretim ortamlarını ele alır - - -Kurulum -======= - -Nette Assets'i [Composer|best-practices:composer] kullanarak kurun: - -```shell -composer require nette/assets -``` - -PHP 8.1 ya da daha yenisini gerektirir ve Nette Framework ile kusursuz çalışır, ama tek başına da kullanılabilir. - - -İlk Adımlar -=========== - -Nette Assets sıfır yapılandırmayla, kutudan çıktığı gibi çalışır. Statik dosyalarınızı `www/assets/` dizinine koyun ve onları kullanmaya başlayın: - -```latte -{* Bir görseli otomatik boyutlarla göster *} -{asset 'logo.png'} - -{* Sürümlemeyle bir stil sayfası ekle *} -{asset 'style.css'} - -{* Bir betik yükle *} -{asset 'app.js'} -``` - -Üretilen HTML üzerinde daha fazla denetim için `n:asset` niteliğini ya da `asset()` fonksiyonunu kullanın. - - -Nasıl Çalışır -============= - -Nette Assets, onu güçlü ama kullanımı basit kılan üç temel kavram üzerine kurulmuştur: - - -Varlıklar - Akıllanmış Dosyalarınız ------------------------------------ - -Bir **varlık** (asset), uygulamanızdaki herhangi bir statik dosyayı temsil eder. Her dosya, yararlı salt okunur özelliklere sahip bir nesneye dönüşür: - -```php -$image = $assets->getAsset('photo.jpg'); -echo $image->url; // '/assets/photo.jpg?v=1699123456' -echo $image->file; // '/var/www/assets/photo.jpg' (yerel yol ya da null) -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' -``` - -Farklı dosya tipleri farklı özellikler sunar: -- **Görseller**: genişlik, yükseklik, alternatif metin, tembel yükleme -- **Betikler**: modül tipi, bütünlük hash'leri, crossorigin -- **Stil sayfaları**: medya sorguları, bütünlük -- **Ses/Video**: süre, boyutlar (yalnızca video) -- **Yazı tipleri**: CORS ile doğru önyükleme - -Kütüphane dosya tiplerini otomatik algılar ve uygun varlık sınıfını oluşturur. - - -Mapper'lar - Dosyaların Geldiği Yer ------------------------------------ - -Bir **mapper**, dosyaları nasıl bulacağını ve onlar için URL'leri nasıl oluşturacağını bilir. Farklı amaçlar için birden çok mapper'ınız olabilir: yerel dosyalar, CDN, bulut depolama ya da derleme araçları (her birinin bir adı vardır). Yerleşik `FilesystemMapper` yerel dosyaları ele alırken, `ViteMapper` modern derleme araçlarıyla bütünleşir. - -Mapper'lar [yapılandırmada |configuration] tanımlanır. - - -Registry - Ana Arayüzünüz -------------------------- - -**Registry**, tüm mapper'ları yönetir ve ana API'yi sunar: - -```php -// Registry'yi servisinize enjekte edin -public function __construct( - private Nette\Assets\Registry $assets -) {} -``` - -```php -// Farklı mapper'lardan varlık alın -$logo = $this->assets->getAsset('images:logo.png'); // 'images' mapper'ı -$app = $this->assets->getAsset('app:main.js'); // 'app' mapper'ı -$style = $this->assets->getAsset('style.css'); // varsayılan mapper'ı kullanır -``` - -Registry doğru mapper'ı otomatik seçer ve başarım için sonuçları önbelleğe alır. - - -PHP'de Varlıklarla Çalışma -========================== - -Registry, varlıkları almak için iki metot sunar: - -```php -// Dosya yoksa Nette\Assets\AssetNotFoundException fırlatır -$logo = $assets->getAsset('logo.png'); - -// Dosya yoksa null döndürür -$banner = $assets->tryGetAsset('banner.jpg'); -if ($banner) { - echo $banner->url; -} -``` - - -Mapper Belirtme ---------------- - -Hangi mapper'ın kullanılacağını açıkça seçebilirsiniz: - -```php -// Varsayılan mapper'ı kullan -$file = $assets->getAsset('document.pdf'); - -// Belirli bir mapper'ı önekle kullan -$image = $assets->getAsset('images:photo.jpg'); - -// Belirli bir mapper'ı dizi sözdizimiyle kullan -$script = $assets->getAsset(['scripts', 'app.js']); -``` - - -Varlık Özellikleri ve Tipleri ------------------------------ - -Her varlık tipi, ilgili salt okunur özellikleri sunar: - -```php -// Görsel özellikleri -$image = $assets->getAsset('photo.jpg'); -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' - -// Betik özellikleri -$script = $assets->getAsset('app.js'); -echo $script->type; // null (Vite giriş noktaları için 'module') - -// Ses özellikleri -$audio = $assets->getAsset('song.mp3'); -echo $audio->duration; // saniye cinsinden süre - -// Tüm varlıklar dizeye dönüştürülebilir (URL döndürür) -$url = (string) $assets->getAsset('document.pdf'); -``` - -.[note] -Boyutlar ya da süre gibi özellikler yalnızca erişildiğinde tembel yüklenir; bu da kütüphaneyi hızlı tutar. - -.[tip] -Kesin statik çözümleme için [nette/phpstan-rules |tools:phpstan-rules#Assets] uzantısını kurun. PHPStan o zaman her varlığın somut tipini bilir, dolayısıyla `getAsset('photo.jpg')` çağrısı `ImageAsset` olarak anlaşılır ve `->width` erişimi hata vermez. - - -Latte Şablonlarında Varlıkları Kullanma -======================================= - -Nette Assets, etiketler ve fonksiyonlarla sezgisel [Latte|latte:] entegrasyonu sunar. - - -`{asset}` ---------- - -`{asset}` etiketi tam HTML elemanları render eder: - -```latte -{* Render eder: *} -{asset 'hero.jpg'} - -{* Render eder: *} -{asset 'app.js'} - -{* Render eder: *} -{asset 'style.css'} -``` - -Etiket otomatik olarak: -- Varlık tipini algılar ve uygun HTML'i üretir -- Önbellek geçersizleştirme için sürümleme ekler -- Görseller için boyutları ekler -- Doğru nitelikleri ayarlar (type, media vb.) - -HTML niteliklerinin içinde ya da ` -``` - - -Özel Mapper'lar -=============== - -Bulut depolama ya da dinamik üretim gibi özel gereksinimler için özel mapper'lar oluşturun: - -```php -use Nette\Assets\Mapper; -use Nette\Assets\Asset; -use Nette\Assets\Helpers; - -class CloudStorageMapper implements Mapper -{ - public function __construct( - private CloudClient $client, - private string $bucket, - ) {} - - public function getAsset(string $reference, array $options = []): Asset - { - if (!$this->client->exists($this->bucket, $reference)) { - throw new Nette\Assets\AssetNotFoundException("Asset '$reference' not found"); - } - - $url = $this->client->getPublicUrl($this->bucket, $reference); - return Helpers::createAssetFromUrl($url); - } -} -``` - -Yapılandırmada kaydedin: - -```neon -assets: - mapping: - cloud: CloudStorageMapper(@cloudClient, 'my-bucket') -``` - -Diğer mapper'lar gibi kullanın: - -```latte -{asset 'cloud:user-uploads/photo.jpg'} -``` - -`Helpers::createAssetFromUrl()` metodu, dosya uzantısına göre doğru varlık tipini otomatik oluşturur. - -`Nette\Assets\HtmlRenderable` arayüzünü gerçekleştiren varlık tipleri (görseller, betikler, stiller vb.) `{asset}` tarafından tam bir HTML elemanı olarak render edilebilir. Diğer dosya tipleri bir `GenericAsset` olur (örneğin bir PDF); bu HTML elemanı olarak render edilemez, ama yine de bir URL (ve başka üstveri) sunar. Böyle bir varlığı HTML elemanı olarak render etmeye çalışmak `Nette\InvalidArgumentException` fırlatır; URL'sini yine de bir niteliğin içinde kullanabilirsiniz. - - -Devamını Okuyun -=============== - -- [Nette Assets: Görsellerden Vite'a dek her şey için nihayet birleşik bir API |https://blog.nette.org/en/introducing-nette-assets] diff --git a/assets/tr/@left-menu.texy b/assets/tr/@left-menu.texy deleted file mode 100644 index 9f2530b753..0000000000 --- a/assets/tr/@left-menu.texy +++ /dev/null @@ -1,14 +0,0 @@ -Nette Assets -************ -- [Başlarken |@home] -- [Vite |vite] -- [Yapılandırma|configuration] - - -Devamını okuyun -*************** -- [Nette dokümantasyonu |nette:] -- [Nette Application |application:how-it-works] -- [Yardımcı araçlar |utils:] -- [En iyi uygulamalar |best-practices:] -- [Sorun giderme |nette:troubleshooting] diff --git a/assets/tr/@meta.texy b/assets/tr/@meta.texy deleted file mode 100644 index 8dfe82f311..0000000000 --- a/assets/tr/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokümantasyonu}} diff --git a/assets/tr/configuration.texy b/assets/tr/configuration.texy deleted file mode 100644 index 9ff0d4413f..0000000000 --- a/assets/tr/configuration.texy +++ /dev/null @@ -1,195 +0,0 @@ -Assets Yapılandırması -********************* - -.[perex] -Nette Assets için yapılandırma seçeneklerine genel bakış. - - -```neon -assets: - # göreli mapper yollarını çözmek için temel yol - basePath: ... # (string) varsayılan %wwwDir% - - # göreli mapper URL'lerini çözmek için temel URL - baseUrl: ... # (string) varsayılan %baseUrl% - - # varlık sürümlemesi küresel olarak etkinleştirilsin mi? - versioning: ... # (bool) varsayılan true - - # varlık mapper'larını tanımlar - mapping: ... # (array) varsayılan 'assets' yolu -``` - -`basePath`, mapper'lardaki göreli yolları çözmek için varsayılan dosya sistemi dizinini belirler. Varsayılan olarak web dizinini (`%wwwDir%`) kullanır. - -`baseUrl`, mapper'lardaki göreli URL'leri çözmek için varsayılan URL önekini belirler. Varsayılan olarak kök URL'yi (`%baseUrl%`) kullanır. - -`versioning` seçeneği, önbellek geçersizleştirme için varlık URL'lerine sürüm parametreleri eklenip eklenmeyeceğini küresel olarak denetler. Tek tek mapper'lar bu ayarı geçersiz kılabilir. - - -Mapper'lar ----------- - -Mapper'lar üç şekilde yapılandırılabilir: basit dize yazımı, ayrıntılı dizi yazımı ya da bir servis olarak (`ClassName(...)` veya `@serviceName()` varlık yazımıyla). - -Bir mapper tanımlamanın en basit yolu: - -```neon -assets: - mapping: - default: assets # %wwwDir%/assets/ için dosya sistemi mapper'ı oluşturur - images: img # %wwwDir%/img/ için dosya sistemi mapper'ı oluşturur - scripts: js # %wwwDir%/js/ için dosya sistemi mapper'ı oluşturur -``` - -Her mapper şunları yapan bir `FilesystemMapper` oluşturur: -- Dosyaları `%wwwDir%/` içinde arar -- `%baseUrl%/` gibi URL'ler üretir -- Küresel sürümleme ayarını kalıtır - - -Daha fazla denetim için ayrıntılı yazımı kullanın: - -```neon -assets: - mapping: - images: - # dosyaların saklandığı dizin - path: ... # (string) isteğe bağlı, varsayılan temel yol (basePath) - - # üretilen bağlantılar için URL öneki - url: ... # (string) isteğe bağlı, varsayılan path - - # bu mapper için sürümleme etkinleştirilsin mi? - versioning: ... # (bool) isteğe bağlı, küresel ayarı kalıtır - - # dosyaları ararken uzantı(lar) otomatik eklensin - extension: ... # (string|array) isteğe bağlı, varsayılan null -``` - -Yapılandırma değerlerinin nasıl çözüldüğünü anlamak: - -Yol çözümü: - - Göreli yollar `basePath` değerinden çözülür (`basePath` ayarlanmamışsa `%wwwDir%` değerinden) - - Mutlak yollar olduğu gibi kullanılır - -URL çözümü: - - Göreli URL'ler `baseUrl` değerinden çözülür (`baseUrl` ayarlanmamışsa `%baseUrl%` değerinden) - - Mutlak URL'ler (şemalı ya da `//` ile başlayan) olduğu gibi kullanılır (`//` ile başlayan bir URL şemasını `baseUrl` değerinden alır) - - `url` belirtilmemişse `path` değeri kullanılır - - -```neon -assets: - basePath: /var/www/project/www - baseUrl: https://example.com/assets - - mapping: - # Göreli yol ve URL - images: - path: img # Şuna çözülür: /var/www/project/www/img - url: images # Şuna çözülür: https://example.com/assets/images - - # Mutlak yol ve URL - uploads: - path: /var/shared/uploads # Olduğu gibi kullanılır: /var/shared/uploads - url: https://cdn.example.com # Olduğu gibi kullanılır: https://cdn.example.com - - # Yalnızca path belirtilmiş - styles: - path: css # Yol: /var/www/project/www/css - # URL: https://example.com/assets/css -``` - - -Özel Mapper'lar ---------------- - -Özel mapper'lar için, var olan bir servise `@serviceName` ile başvurun ya da onu doğrudan `ClassName(arguments)` veya yalın bir sınıf adıyla tanımlayın: - -```neon -services: - s3mapper: App\Assets\S3Mapper(%s3.bucket%) - -assets: - mapping: - cloud: @s3mapper - database: App\Assets\DatabaseMapper(@database.connection) -``` - - -Vite Mapper ------------ - -Vite mapper yalnızca `type: vite` eklemenizi gerektirir. Yapılandırma seçeneklerinin tam listesi şudur: - -```neon -assets: - mapping: - default: - # mapper tipi (Vite için gereklidir) - type: vite # (string) gerekli, 'vite' olmalıdır - - # Vite derleme çıktı dizini - path: ... # (string) isteğe bağlı, varsayılan temel yol (basePath) - - # derlenmiş varlıklar için URL öneki - url: ... # (string) isteğe bağlı, varsayılan path - - # Vite manifest dosyasının konumu - manifest: ... # (string) isteğe bağlı, path'e göreli, varsayılan /.vite/manifest.json - - # Vite dev sunucusu yapılandırması - devServer: ... # (bool|string) isteğe bağlı, varsayılan true - - # public dizinindeki dosyalar için sürümleme - versioning: ... # (bool) isteğe bağlı, küresel ayarı kalıtır - - # public dizinindeki dosyalar için otomatik uzantı - extension: ... # (string|array) isteğe bağlı, varsayılan null -``` - -`devServer` seçeneği, varlıkların geliştirme sırasında nasıl yükleneceğini denetler: - -- `true` (varsayılan) - Çalışan bir Vite dev sunucusunu otomatik algılar (Nette Vite eklentisinin derleme dizininde oluşturduğu `.vite/nette.json` dosyası aracılığıyla). Dev sunucusu çalışıyorsa **ve uygulamanız hata ayıklama kipindeyse**, varlıklar hot module replacement desteğiyle ondan yüklenir. Dev sunucusu çalışmıyorsa, varlıklar public dizinindeki derlenmiş dosyalardan yüklenir. -- `false` - Dev sunucusu entegrasyonunu tümüyle kapatır. Varlıklar her zaman derlenmiş dosyalardan yüklenir. -- Özel URL (örneğin `https://localhost:5173`) - Dev sunucusu URL'sini protokol ve port dahil elle belirtin. Dev sunucusu farklı bir host ya da portta çalışıyorsa yararlıdır. Otomatik algılamada olduğu gibi yalnızca hata ayıklama kipinde geçerlidir; üretimde her zaman derlenmiş dosyalar kullanılır. - -`versioning` ve `extension` seçenekleri yalnızca Vite'ın public dizinindeki, Vite tarafından işlenmeyen dosyalar için geçerlidir. - - -Elle Yapılandırma ------------------ - -Nette DI kullanmıyorsanız mapper'ları elle yapılandırın: - -```php -use Nette\Assets\Registry; -use Nette\Assets\FilesystemMapper; -use Nette\Assets\ViteMapper; - -$registry = new Registry; - -// Dosya sistemi mapper'ı ekle -$registry->addMapper('images', new FilesystemMapper( - baseUrl: 'https://example.com/img', - basePath: __DIR__ . '/www/img', - extensions: ['webp', 'jpg', 'png'], - versioning: true, -)); - -// Vite mapper ekle -$registry->addMapper('app', new ViteMapper( - baseUrl: '/build', - basePath: __DIR__ . '/www/build', - manifestPath: __DIR__ . '/www/build/.vite/manifest.json', - devServer: 'https://localhost:5173', -)); -``` - -Kayıtlı herhangi bir mapper'ı `getMapper()` metoduyla adına göre de alabilirsiniz: - -```php -$mapper = $registry->getMapper('images'); // kayıtlı mapper'ı döndürür -$default = $registry->getMapper(); // 'default' mapper'ı döndürür -``` diff --git a/assets/tr/vite.texy b/assets/tr/vite.texy deleted file mode 100644 index 2fff4a339a..0000000000 --- a/assets/tr/vite.texy +++ /dev/null @@ -1,567 +0,0 @@ -Vite Entegrasyonu -***************** - -
    - -Modern JavaScript uygulamaları gelişmiş derleme araçları gerektirir. Nette Assets, yeni nesil frontend derleme aracı [Vite |https://vite.dev/] ile birinci sınıf entegrasyon sunar. Hot Module Replacement (HMR) ile ışık hızında geliştirme ve sıfır yapılandırma derdiyle iyileştirilmiş üretim derlemeleri elde edin. - -- **Sıfır yapılandırma** - Vite ile PHP şablonları arasında otomatik köprü -- **Eksiksiz bağımlılık yönetimi** - tek bir etiket tüm varlıkları üstlenir -- **Hot Module Replacement** - anında JavaScript ve CSS güncellemeleri -- **İyileştirilmiş üretim derlemeleri** - kod bölme ve tree shaking - -
    - - -Nette Assets, Vite ile kusursuz bütünleşir; böylece şablonlarınızı her zamanki gibi yazarken tüm bu avantajları elde edersiniz. - - -Vite'ı Kurma -============ - -Vite'ı adım adım kuralım. Derleme araçlarına yeniyseniz endişelenmeyin, her şeyi açıklayacağız! - - -Adım 1: Vite'ı kurun --------------------- - -Önce projenize Vite'ı ve Nette eklentisini kurun: - -```shell -npm install -D vite @nette/vite-plugin -``` - -Bu, Vite'ı ve Vite'ın Nette ile kusursuz çalışmasına yardım eden özel bir eklentiyi kurar. - - -Adım 2: Proje yapısı --------------------- - -Standart yaklaşım, kaynak varlık dosyalarını projenizin kökündeki bir `assets/` klasörüne, derlenmiş sürümleri ise `www/assets/` içine koymaktır: - -/--pre -web-project/ -├── assets/ ← kaynak dosyalar (SCSS, TypeScript, kaynak görseller) -│ ├── public/ ← statik dosyalar (olduğu gibi kopyalanır) -│ │ └── favicon.ico -│ ├── images/ -│ │ └── logo.png -│ ├── app.js ← ana giriş noktası -│ └── style.css ← stilleriniz -└── www/ ← genel dizin (document root) - ├── assets/ ← derlenmiş dosyalar buraya gidecek - └── index.php -\-- - -`assets/` klasörü kaynak dosyalarınızı, yani yazdığınız kodu içerir. Vite bu dosyaları işleyecek ve derlenmiş sürümleri `www/assets/` içine koyacak. - - -Adım 3: Vite'ı yapılandırın ---------------------------- - -Proje kökünüzde bir `vite.config.ts` dosyası oluşturun. Bu dosya Vite'a kaynak dosyalarınızı nerede bulacağını ve derlenmişleri nereye koyacağını söyler. - -Nette Vite eklentisi, yapılandırmayı basit kılan akıllı varsayılanlarla gelir. Frontend kaynak dosyalarınızın `assets/` dizininde (`root` seçeneği), derlenmiş dosyaların ise `www/assets/` içinde (`outDir` seçeneği) olduğunu varsayar. Yalnızca [giriş noktasını|#Giriş Noktaları] belirtmeniz gerekir: - -```js -import { defineConfig } from 'vite'; -import nette from '@nette/vite-plugin'; - -export default defineConfig({ - plugins: [ - nette({ - entry: 'app.js', - }), - ], -}); -``` - -Kaputun altında eklenti, `root` ve `outDir` dışında her şeyin birbirine oturması için birkaç Vite seçeneğini daha ayarlar: `base` seçeneğini `''` yapar (varlıklar doğrudan document root'tan sunulur), `build.manifest` seçeneğini `true` yapar (böylece Nette Assets hash'lenmiş dosya adlarını eşleyebilir) ve `build.assetsDir` seçeneğini `''` yapar (derlenmiş dosyalar `static/` alt klasörü olmadan doğrudan `outDir` içine iner). Bunların herhangi birini geçersiz kılabilirsiniz. - -.[note] -Varsayılan `outDir` (`www/assets`), `www/` dizininin zaten var olmasını gerektirir. Yoksa eklenti *"The output directory ... does not exist"* hatasıyla durur. - -Varlıklarınızı derlemek için başka bir dizin adı belirtmek isterseniz, birkaç seçeneği değiştirmeniz gerekecek: - -```js -export default defineConfig({ - root: 'assets', // kaynak varlıkların kök dizini - - build: { - outDir: '../www/assets', // derlenmiş dosyaların gideceği yer - }, - - // ... diğer yapılandırma ... -}); -``` - -.[note] -`outDir` yolu `root` dizinine göreli sayılır; başındaki `../` bu yüzdendir. - - -Adım 4: Nette'i yapılandırın ----------------------------- - -`common.neon` dosyanızda Nette Assets'e Vite'tan söz edin: - -```neon -assets: - mapping: - default: - type: vite # Nette'e ViteMapper kullanmasını söyler - path: assets -``` - - -Adım 5: Betikleri ekleyin -------------------------- - -`package.json` dosyanıza şu betikleri ekleyin: - -```json -{ - "scripts": { - "dev": "vite", - "build": "vite build" - } -} -``` - -Artık şunları yapabilirsiniz: -- `npm run dev` - hot reloading'li geliştirme sunucusunu başlatır -- `npm run build` - iyileştirilmiş üretim dosyalarını oluşturur - - -Giriş Noktaları -=============== - -**Giriş noktası** (entry point), uygulamanızın başladığı ana dosyadır. Bu dosyadan başka dosyaları (CSS, JavaScript modülleri, görseller) import edersiniz ve böylece bir bağımlılık ağacı oluşur. Vite bu import'ları izler ve her şeyi bir araya paketler. - -Örnek giriş noktası `assets/app.js`: - -```js -// Stilleri import et -import './style.css' - -// JavaScript modüllerini import et -import netteForms from 'nette-forms'; -import naja from 'naja'; - -// Uygulamanı başlat -netteForms.initOnLoad(); -naja.initialize(); -``` - -Şablonda bir giriş noktasını şöyle ekleyebilirsiniz: - -```latte -{asset 'app.js'} -``` - -Nette Assets gereken tüm HTML etiketlerini otomatik üretir: JavaScript, CSS ve diğer bağımlılıklar. - - -Birden Çok Giriş Noktası ------------------------- - -Daha büyük uygulamalar sıklıkla ayrı giriş noktalarına gereksinim duyar: - -```js -export default defineConfig({ - plugins: [ - nette({ - entry: [ - 'app.js', // genel sayfalar - 'admin.js', // yönetim paneli - ], - }), - ], -}); -``` - -Onları farklı şablonlarda kullanın: - -```latte -{* Genel sayfalarda *} -{asset 'app.js'} - -{* Yönetim panelinde *} -{asset 'admin.js'} -``` - - -Önemli: Kaynak Dosyalar ile Derlenmiş Dosyalar ----------------------------------------------- - -Üretimde yalnızca Vite'ın kullanılabilir kıldığı dosyaları yükleyebileceğinizi anlamak çok önemlidir; bu ya manifest aracılığıyla ya da onları public klasöründen değiştirmeden kopyalayarak olur: - -1. `entry` içinde tanımlı **giriş noktaları** (dinamik olarak import ettikleri modüller dahil) ve **JavaScript'ten ya da CSS'ten başvurulan varlıklar** (görseller, yazı tipleri, ...) - bunların hepsi manifest'e kaydedilir -2. **`assets/public/` dizinindeki dosyalar** - bunlar manifest'te *değildir*; olduğu gibi kopyalanırlar ve `{asset}` onları bir dosya sistemi yedeğiyle bulur - -`assets/` içindeki rastgele dosyaları `{asset}` ile **yükleyemezsiniz**; bir dosyaya hiçbir yerden başvurulmuyorsa derlenmez. Vite'ın başka varlıklardan haberdar olmasını istiyorsanız, onları [public klasörüne |#Public Klasörü] taşıyabilirsiniz. - -Vite'ın varsayılan olarak 4 KB'den küçük tüm varlıkları satır içine alacağını, dolayısıyla bu dosyalara doğrudan başvuramayacağınızı unutmayın. (Bkz. [Vite belgeleri |https://vite.dev/guide/assets.html]). - -```latte -{* ✓ Bu çalışır - bir giriş noktası *} -{asset 'app.js'} - -{* ✓ Bu çalışır - assets/public/ içinde *} -{asset 'favicon.ico'} - -{* ✗ Bu çalışmaz - assets/ içinde rastgele bir dosya *} -{asset 'components/button.js'} -``` - - -Geliştirme Kipi -=============== - -Geliştirme kipi tümüyle isteğe bağlıdır, ama etkinleştirildiğinde belirgin avantajlar sağlar. Ana avantajı **Hot Module Replacement (HMR)**: uygulama durumunu yitirmeden değişiklikleri anında görürsünüz; bu da geliştirme deneyimini çok daha akıcı ve hızlı kılar. - -Vite, geliştirmeyi inanılmaz hızlı kılan modern bir derleme aracıdır. Geleneksel paketleyicilerin tersine Vite, geliştirme sırasında kodunuzu doğrudan tarayıcıya sunar; bu da projeniz ne kadar büyük olursa olsun anında sunucu başlangıcı ve ışık hızında güncellemeler demektir. - - -Geliştirme Sunucusunu Başlatma ------------------------------- - -Geliştirme sunucusunu çalıştırın: - -```shell -npm run dev -``` - -Şunu göreceksiniz: - -``` - ➜ Local: http://localhost:5173/ - ➜ Network: use --host to expose -``` - -Geliştirme yaparken bu terminali açık tutun. - -Dev sunucusu çalışırken eklenti, URL'sini içeren küçük bir sinyal dosyası olan `www/assets/.vite/nette.json` dosyasını yazar. PHP tarafındaki Nette Assets bu dosyayı okur ve şu iki koşul birlikte sağlandığında dev sunucusundan yüklemeye geçer: -1. Vite dev sunucusu çalışıyor (sinyal dosyası var) ve -2. Nette uygulamanız hata ayıklama kipinde. - -Sonuç: - -```latte -{asset 'app.js'} -{* Geliştirmede: - *} -{* Üretimde: *} -``` - -Hiçbir yapılandırma gerekmez, öylece çalışır! Algılamayı kapatmak ya da dev sunucusu URL'sini elle ayarlamak için [devServer seçeneğine |configuration#Vite Mapper] bakın. - -.[note] -Sinyal dosyası `www/assets/.vite/nette.json` konumundadır (üretimdeki `manifest.json` dosyasının hemen yanında). Onu, varsayılanı `.vite/nette.json` olan eklentinin `infoFile` seçeneğiyle yeniden adlandırabilirsiniz. Nette çalışan dev sunucusunu yakalamıyorsa, bu dosyanın var olduğunu ve doğru URL'ye işaret ettiğini denetleyin. - - -Farklı Alan Adlarında Çalışma ------------------------------ - -Geliştirme sunucunuz `localhost` dışında bir yerde çalışıyorsa (örneğin `myapp.local`), CORS (Cross-Origin Resource Sharing) sorunlarıyla karşılaşabilirsiniz. CORS, web tarayıcılarında varsayılan olarak farklı alan adları arasındaki istekleri engelleyen bir güvenlik özelliğidir. PHP uygulamanız `myapp.local` üzerinde, Vite ise `localhost:5173` üzerinde çalışırken, tarayıcı bunları farklı alan adları olarak görür ve istekleri engeller. - -Bunu çözmek için iki seçeneğiniz var: - -**Seçenek 1: CORS'u yapılandırın** - -En basit çözüm, PHP uygulamanızdan gelen çapraz kaynak isteklerine izin vermektir: - -```js -export default defineConfig({ - // ... diğer yapılandırma ... - - server: { - cors: { - origin: 'http://myapp.local', // PHP uygulamanızın URL'si - }, - }, -}); -``` - -**Seçenek 2: Vite'ı kendi alan adınızda çalıştırın** - -Diğer çözüm, Vite'ı PHP uygulamanızla aynı alan adında çalıştırmaktır. - -```js -export default defineConfig({ - // ... diğer yapılandırma ... - - server: { - host: 'myapp.local', // PHP uygulamanızla aynı - }, -}); -``` - -Aslında bu durumda da CORS'u yapılandırmanız gerekir, çünkü dev sunucusu aynı hostname'de ama farklı bir portta çalışır. Ancak bu durumda CORS, Nette Vite eklentisi tarafından otomatik yapılandırılır. - - -HTTPS ile Geliştirme --------------------- - -HTTPS üzerinde geliştiriyorsanız, Vite geliştirme sunucunuz için sertifikalara gereksinim duyarsınız. En kolay yol, sertifikaları otomatik üreten bir eklenti kullanmaktır: - -```shell -npm install -D vite-plugin-mkcert -``` - -Onu `vite.config.ts` içinde şöyle yapılandırırsınız: - -```js -import mkcert from 'vite-plugin-mkcert'; - -export default defineConfig({ - // ... diğer yapılandırma ... - - plugins: [ - mkcert(), // sertifikaları otomatik üretir ve https'i etkinleştirir - nette(), - ], -}); -``` - -CORS yapılandırmasını (yukarıdaki Seçenek 1) kullanıyorsanız, origin URL'sini `http://` yerine `https://` kullanacak biçimde güncellemeniz gerektiğini unutmayın. - - -Docker ile Geliştirme ---------------------- - -Vite'ı bir Docker konteynerinin içinde çalıştırdığınızda iki şeye dikkat etmek gerekir: makinenizdeki tarayıcının dev sunucusuna ulaşabilmesi ve Vite'ın konteyner sınırının ötesindeki dosya değişikliklerini algılayabilmesi. - -Önce Vite portunu konteynerden [yayımlayın |https://docs.docker.com/get-started/docker-concepts/running-containers/publishing-ports/] ve dev sunucusunu tüm arayüzlere bağlayın; böylece konteynerin dışından erişilebilir olur: - -```js -export default defineConfig({ - // ... diğer yapılandırma ... - - plugins: [ - nette(), - ], - server: { - host: '0.0.0.0', // tüm arayüzleri dinle (konteynerde gereklidir) - port: 5173, // yayımlanan portla eşleşmelidir - strictPort: true, // başka bir port seçmek yerine başarısız ol - watch: { - usePolling: true, // bağlı birimlerde dosya değişiklikleri algılanmıyorsa etkinleştir - }, - }, -}); -``` - -Eklenti, dev sunucusu URL'sini PHP tarafı için `nette.json` içine yazar. `host: '0.0.0.0'` bir tarayıcı tarafından kullanılamayacağından (her varlık için `localhost` adresine yönlendirir), eklenti onu bu URL'de otomatik olarak `localhost` biçimine yeniden yazar; böylece varlıklar doğru yüklenir. - -Uygulamayı `localhost` yerine özel bir alan adında açıyorsanız, eklentinin `host` seçeneğini o alan adına ayarlayın: - -```js - plugins: [ - nette({ host: 'myapp.local' }), // PHP uygulamanızla aynı alan adı - ], -``` - -Eklenti bu host'u dev sunucusu URL'si için kullanır, onu CORS origin'lerine ekler ve Vite'ın `allowedHosts` listesine alır; böylece elle CORS ayarı yapmadan çalışır. - -Daha karmaşık kurulumlar için, örneğin Vite'ın; genel host, port ve protokolün iç adresten ayrıldığı bir ters vekil sunucunun arkasında çalıştığı durumlarda, Vite'ın `server.origin` seçeneğini tam genel URL'ye ayarlayın. Eklenti buna saygı gösterir ve URL'yi yerel soketten türetmek yerine olduğu gibi `nette.json` içine yazar: - -```js - server: { - origin: 'https://myapp.local:8443', // tarayıcının Vite'a ulaştığı genel URL - }, -``` - - -Üretim Derlemeleri -================== - -İyileştirilmiş üretim dosyalarını oluşturun: - -```shell -npm run build -``` - -Vite şunları yapacak: -- Tüm JavaScript ve CSS'i küçültür -- Kodu en uygun parçalara böler -- Önbellek geçersizleştirme için hash'lenmiş dosya adları üretir -- Nette Assets için bir manifest dosyası oluşturur - -Örnek çıktı: - -``` -www/assets/ -├── app-4f3a2b1c.js # Ana JavaScript'iniz (küçültülmüş) -├── app-7d8e9f2a.css # Ayıklanmış CSS (küçültülmüş) -├── vendor-8c4b5e6d.js # Paylaşılan bağımlılıklar -└── .vite/ - └── manifest.json # Nette Assets için eşleme -``` - -Hash'lenmiş dosya adları, tarayıcıların her zaman en son sürümü yüklemesini sağlar. - - -Public Klasörü -============== - -`assets/public/` dizinindeki dosyalar, işlenmeden çıktıya kopyalanır: - -``` -assets/ -├── public/ -│ ├── favicon.ico -│ ├── robots.txt -│ └── images/ -│ └── og-image.jpg -├── app.js -└── style.css -``` - -Onlara normal biçimde başvurun: - -```latte -{* Bu dosyalar olduğu gibi kopyalanır *} - - -``` - -Public dosyalar için FilesystemMapper özelliklerini kullanabilirsiniz. `extension` seçeneği, uzantısız başvurular için geçerlidir (örneğin `{asset 'images/og-image'}` önce `og-image.webp` dosyasını bulur): - -```neon -assets: - mapping: - default: - type: vite - path: assets - extension: [webp, jpg, png] # uzantısız başvurular için - versioning: true # Önbellek geçersizleştirme ekle -``` - -`vite.config.ts` yapılandırmasında public klasörünü `publicDir` seçeneğiyle değiştirebilirsiniz. - - -Dinamik Import'lar -================== - -Vite, en uygun yükleme için kodu otomatik böler. Dinamik import'lar, kodu yalnızca gerçekten gerektiğinde yüklemenizi sağlar ve başlangıç paket boyutunu küçültür: - -```js -// Ağır bileşenleri istendiğinde yükle -button.addEventListener('click', async () => { - let { Chart } = await import('./components/chart.js') - new Chart(data) -}) -``` - -Dinamik import'lar, yalnızca gerektiğinde yüklenen ayrı parçalar oluşturur. Buna "kod bölme" denir ve Vite'ın en güçlü özelliklerinden biridir. Dinamik import kullandığınızda, Vite dinamik olarak import edilen her modül için otomatik olarak ayrı JavaScript dosyaları oluşturur. - -`{asset 'app.js'}` etiketi bu dinamik parçaları otomatik olarak **önceden yüklemez**. Bu kasıtlı bir davranıştır; hiç kullanılmayabilecek kodu indirmek istemeyiz. Parçalar yalnızca dinamik import çalıştırıldığında indirilir. - -Ancak belirli dinamik import'ların kritik olduğunu ve yakında gerekeceğini biliyorsanız, onları önceden yükleyebilirsiniz: - -```latte -{* Ana giriş noktası *} -{asset 'app.js'} - -{* Kritik dinamik import'ları önceden yükle *} -{preload 'components/chart.js'} -``` - -Bu, tarayıcıya chart bileşenini arka planda indirmesini söyler; böylece gerektiğinde hemen hazır olur. - - -TypeScript Desteği -================== - -TypeScript kutudan çıktığı gibi çalışır: - -```ts -// assets/main.ts -interface User { - name: string - email: string -} - -export function greetUser(user: User): void { - console.log(`Hello, ${user.name}!`) -} -``` - -TypeScript dosyalarına normal biçimde başvurun (her dosyada olduğu gibi, `main.ts` bir giriş noktası olmalıdır): - -```latte -{asset 'main.ts'} -``` - -Tam TypeScript desteği için onu kurun: - -```shell -npm install -D typescript -``` - - -Ek Vite Yapılandırması -====================== - -İşte ayrıntılı açıklamalarıyla birlikte bazı yararlı Vite yapılandırma seçenekleri: - -```js -export default defineConfig({ - // Kaynak varlıkları içeren kök dizin - root: 'assets', - - // İçeriği çıktı dizinine olduğu gibi kopyalanan klasör - // Varsayılan: 'public' ('root' dizinine göreli) - publicDir: 'public', - - build: { - // Derlenmiş dosyaların konacağı yer ('root' dizinine göreli) - outDir: '../www/assets', - - // Derlemeden önce çıktı dizini boşaltılsın mı? - // Önceki derlemelerden kalan eski dosyaları kaldırmak için yararlıdır - emptyOutDir: true, - - // Üretilen parçalar ve varlıklar için outDir içindeki alt dizin - // Bu, çıktı yapısını düzenlemeye yardım eder - assetsDir: 'static', - - rollupOptions: { - // Giriş noktası/noktaları - tek bir dosya ya da dosya dizisi olabilir - // Her giriş noktası ayrı bir paket olur - input: [ - 'app.js', // ana uygulama - 'admin.js', // yönetim paneli - ], - }, - }, - - server: { - // Dev sunucusunun bağlanacağı host - // Ağa açmak için '0.0.0.0' kullanın - host: 'localhost', - - // Dev sunucusunun portu - port: 5173, - - // Çapraz kaynak istekleri için CORS yapılandırması - cors: { - origin: 'http://myapp.local', - }, - }, - - css: { - // Geliştirmede CSS source map'leri etkinleştir - devSourcemap: true, - }, - - plugins: [ - nette(), - ], -}); -``` - -.[warning] -Yukarıdaki `rollupOptions.input` içindeki giriş yollarına dikkat edin: Rollup göreli yolları, `root: 'assets'` dizinine **değil**, geçerli çalışma dizinine (proje kökünüze) göre çözer. Dolayısıyla yalın bir `'app.js'` derleme zamanında var olmayacak. Ya eklentinin `entry` seçeneğini kullanın (o, giriş yollarını `root` dizinine göreli çözer) ya da yolları proje köküne göreli yazın, örneğin `'assets/app.js'`. - -Hepsi bu! Artık Nette Assets ile bütünleşik modern bir derleme sisteminiz var. diff --git a/best-practices/cs/@home.texy b/best-practices/cs/@home.texy deleted file mode 100644 index 5fae2af477..0000000000 --- a/best-practices/cs/@home.texy +++ /dev/null @@ -1,72 +0,0 @@ -Návody a postupy -**************** - -.[perex] -Návody, řešení častých úloh a *best practices* pro Nette. - - -
    -
    - - -Nette Aplikace --------------- -- [Metody a atributy inject |inject-method-attribute] -- [Skládání presenterů z trait |presenter-traits] -- [Předání nastavení do presenterů |passing-settings-to-presenters] -- [Jak se vrátit k dřívější stránce |restore-request] -- [Stránkování výsledků databáze |pagination] -- [Dynamické snippety |dynamic-snippets] -- [Jak používat atribut #Requires |attribute-requires] -- [Jak správně používat POST odkazy |post-links] -- [Hezké URL se slugem |pretty-urls] - -
    -
    - - -Formuláře ---------- -- [Znovupoužití formulářů |form-reuse] -- [Formulář pro vytvoření i editaci záznamu |creating-editing-form] -- [Vytváříme kontaktní formulář |lets-create-contact-form] -- [Závislé selectboxy |https://blog.nette.org/cs/zavisle-selectboxy-elegantne-v-nette-a-cistem-javascriptu] - -
    -
    - - -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] -- [Úvod do objektově orientovaného programování |nette:introduction-to-object-oriented-programming] - -
    -
    - - -Ukázkové řešení ---------------- -- [Nette examples |https://github.com/nette-examples] -- [Doctrine & Nette |https://contributte.org/nettrine/] -- [Contributte examples |https://contributte.org/examples.html] -- [Doctrine ORM Website |https://github.com/MinecordNetwork/Website] -- [Quick start |quickstart:] - -
    -
    - - -Videa ------ -Stovky záznamů z Posledních sobot a videí o Nette naleznete pod jednou střechou na "Youtube kanálu Nette Frameworku":https://www.youtube.com/user/NetteFramework. - -
    -
    - -{{maintitle: Návody a best practices pro Nette: řešení častých úloh}} -{{description: Návody, řešení častých úloh a best practices pro Nette: formuláře, stránkování, hezké URL, presentery, Composer a další.}} diff --git a/best-practices/cs/@left-menu.texy b/best-practices/cs/@left-menu.texy deleted file mode 100644 index b991715da9..0000000000 --- a/best-practices/cs/@left-menu.texy +++ /dev/null @@ -1,34 +0,0 @@ -Návody a postupy -**************** -- [Přehled |@home] - -Nette Aplikace -************** -- [Metody a atributy inject |inject-method-attribute] -- [Skládání presenterů z trait |presenter-traits] -- [Předání nastavení do presenterů |passing-settings-to-presenters] -- [Jak se vrátit k dřívější stránce |restore-request] -- [Stránkování výsledků databáze |pagination] -- [Dynamické snippety |dynamic-snippets] -- [Jak používat atribut #Requires |attribute-requires] -- [Jak správně používat POST odkazy |post-links] -- [Hezké URL se slugem |pretty-urls] - -Formuláře -********* -- [Znovupoužití formulářů |form-reuse] -- [Formulář pro vytvoření i editaci záznamu |creating-editing-form] -- [Vytváříme kontaktní formulář |lets-create-contact-form] - -Obecné -****** -- [Jak psát mikro-weby |microsites] -- [Composer: tipy pro použití |composer] - - -Další četba -*********** -- [Dokumentace Nette |nette:] -- [Aplikace v Nette |application:how-it-works] -- [Utilities |utils:] -- [Řešení problémů |nette:troubleshooting] diff --git a/best-practices/cs/@meta.texy b/best-practices/cs/@meta.texy deleted file mode 100644 index 68d2f76d0d..0000000000 --- a/best-practices/cs/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Návody a postupy}} diff --git a/best-practices/cs/attribute-requires.texy b/best-practices/cs/attribute-requires.texy deleted file mode 100644 index ff1e795871..0000000000 --- a/best-practices/cs/attribute-requires.texy +++ /dev/null @@ -1,180 +0,0 @@ -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 určit, 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#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á view, 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 určité 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. - -{{maintitle: Atribut #[Requires] v Nette: omezení přístupu k presenterům}} -{{description: Omezte presenter nebo akci jen na požadavky POST, AJAX, stejný původ nebo přístup přes forward, a to jediným atributem #[Requires].}} diff --git a/best-practices/cs/composer.texy b/best-practices/cs/composer.texy deleted file mode 100644 index fafd158920..0000000000 --- a/best-practices/cs/composer.texy +++ /dev/null @@ -1,285 +0,0 @@ -Composer: tipy pro použití -************************** - -
    - -Composer je nástroj pro správu závislostí v PHP. Umožní nám vyjmenovat knihovny, na kterých náš projekt závisí, a bude je za nás instalovat a aktualizovat. Ukážeme si: - -- jak Composer nainstalovat -- jeho použití v novém či stávajícím projektu - -
    - - -Instalace -========= - -Composer je spustitelný `.phar` soubor, který si stáhnete a nainstalujete následujícím způsobem: - - -Windows -------- - -Použijte oficiální instalátor [Composer-Setup.exe |https://getcomposer.org/Composer-Setup.exe]. - - -Linux, macOS ------------- - -Stačí 4 příkazy, které si zkopírujte z [této stránky |https://getcomposer.org/download/]. - -Když jej vložíte do složky, která je v systémovém `PATH`, stane se Composer přístupný globálně: - -```shell -$ mv ./composer.phar ~/bin/composer # nebo /usr/local/bin/composer -``` - - -Použití v projektu -================== - -Abychom mohli ve svém projektu začít používat Composer, potřebujeme pouze soubor `composer.json`. Ten popisuje závislosti našeho projektu a může také obsahovat další metadata. Základní `composer.json` tedy může vypadat takto: - -```js -{ - "require": { - "nette/database": "^3.0" - } -} -``` - -Říkáme zde, že naše aplikace (nebo knihovna) vyžaduje balíček `nette/database` (název balíčku se skládá z názvu organizace a názvu projektu) a chce verzi, která odpovídá podmínce `^3.0` (tj. nejnovější verzi 3). - -Máme tedy v kořenu projektu soubor `composer.json` a spustíme instalaci: - -```shell -composer update -``` - -Composer stáhne Nette Database do složky `vendor/`. Dále vytvoří soubor `composer.lock`, který obsahuje informace o tom, které verze knihoven přesně nainstaloval. - -Composer vygeneruje soubor `vendor/autoload.php`, který můžeme jednoduše inkludovat a začít používat knihovny bez jakékoli další práce: - -```php -require __DIR__ . '/vendor/autoload.php'; - -$db = new Nette\Database\Connection('sqlite::memory:'); -``` - - -Aktualizace balíčků na nejnovější verze -======================================= - -Aktualizaci používaných knihoven na nejnovější verze podle podmínek definovaných v `composer.json` má na starosti příkaz `composer update`. Např. u závislosti `"nette/database": "^3.0"` nainstaluje nejnovější verzi 3.x.x, ale nikoliv už verzi 4. - -Pro aktualizaci podmínek v souboru `composer.json` například na `"nette/database": "^4.1"`, aby bylo možné nainstalovat nejnovější verzi, použijte příkaz `composer require nette/database`. - -Pro aktualizaci všech používaných balíčků Nette by bylo nutné je všechny v příkazové řádce vyjmenovat, např.: - -```shell -composer require nette/application nette/forms latte/latte tracy/tracy ... -``` - -Což je nepraktické. Použijte proto jednoduchý skript "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff, který to udělá za vás: - -```shell -php composer-frontline.php -``` - - -Vytvoření nového projektu -========================= - -Nový projekt na Nette vytvoříte pomocí jediného příkazu: - -```shell -composer create-project nette/web-project nazev-projektu -``` - -Jako `nazev-projektu` vložte název adresáře pro svůj projekt a potvrďte. Composer stáhne repozitář `nette/web-project` z GitHubu, který už obsahuje soubor `composer.json`, a hned potom Nette Framework. Mělo by již stačit pouze [nastavit oprávnění |nette:troubleshooting#Nastavení práv adresářů] na zápis do složek `temp/` a `log/` a projekt by měl ožít. - -Pokud víte, na jaké verzi bude PHP projekt hostován, nezapomeňte [ji nastavit |#Verze PHP]. - - -Verze PHP -========= - -Composer vždy instaluje ty verze balíčků, které jsou kompatibilní s verzí PHP, kterou právě používáte (lépe řečeno s verzí PHP používanou v příkazové řádce při spouštění Composeru). Což ale nejspíš není stejná verze, jakou používá váš hosting. Proto je velmi důležité si do souboru `composer.json` přidat informaci o verzi PHP na hostingu. Poté se budou instalovat pouze verze balíčků s hostingem kompatibilní. - -To, že projekt poběží například na PHP 8.2.3, nastavíme příkazem: - -```shell -composer config platform.php 8.2.3 -``` - -Takto se verze zapíše do souboru `composer.json`: - -```js -{ - "config": { - "platform": { - "php": "8.2.3" - } - } -} -``` - -Nicméně číslo verze PHP se uvádí ještě na jiném místě souboru, a to v sekci `require`. Zatímco první číslo určuje, pro jakou verzi se budou instalovat balíčky, druhé číslo říká, pro jakou verzi je napsaná samotná aplikace. A podle něj například PhpStorm nastavuje *PHP language level*. (Samozřejmě nedává smysl, aby se tyto verze lišily, takže dvojí zápis je nedomyšlenost.) Tuto verzi nastavíte příkazem: - -```shell -composer require php 8.2.3 --no-update -``` - -Nebo přímo v souboru `composer.json`: - -```js -{ - "require": { - "php": "8.2.3" - } -} -``` - - -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í -============= - -Při upgradu balíčků nebo změnách čísel verzí se stává, že dojde ke konfliktu. Jeden balíček má požadavky, které jsou v rozporu s jiným a podobně. Composer ale občas vypisuje plané hlášení. Hlásí konflikt, který reálně neexistuje. V takovém případě pomůže smazat soubor `composer.lock` a zkusit to znovu. - -Pokud chybová hláška přetrvává, pak je myšlena vážně a je potřeba z ní vyčíst, co a jak upravit. - - -Packagist.org - centrální repozitář -=================================== - -[Packagist |https://packagist.org] je hlavní repozitář, ve kterém se Composer snaží vyhledávat balíčky, pokud mu neřekneme jinak. Můžeme zde publikovat i vlastní balíčky. - - -Co když nechceme používat centrální repozitář? ----------------------------------------------- - -Pokud máme vnitrofiremní aplikace, které zkrátka nemůžeme hostovat veřejně, tak si pro ně vytvoříme firemní repozitář. - -Více na téma repozitářů [v oficiální dokumentaci |https://getcomposer.org/doc/05-repositories.md#repositories]. - - -Autoloading -=========== - -Zásadní vlastností Composeru je, že poskytuje autoloading pro všechny jím nainstalované třídy, který nastartujete includováním souboru `vendor/autoload.php`. - -Nicméně je možné používat Composer i pro načítání dalších tříd i mimo složku `vendor`. První možností je nechat Composer prohledat definované složky a podsložky, najít všechny třídy a zahrnout je do autoloaderu. Toho docílíte nastavením `autoload > classmap` v `composer.json`: - -```js -{ - "autoload": { - "classmap": [ - "src/", # zahrne složku src/ a její podsložky - ] - } -} -``` - -Následně je potřeba při každé změně spustit příkaz `composer dumpautoload` a nechat autoloadovací tabulky přegenerovat. To je nesmírně nepohodlné a daleko lepší je tento úkol svěřit [RobotLoaderu|robot-loader:], který stejnou činnost provádí automaticky na pozadí a mnohem rychleji. - -Druhou možností je dodržovat [PSR-4|https://www.php-fig.org/psr/psr-4/]. Zjednodušeně řečeno jde o systém, kdy jmenné prostory a názvy tříd odpovídají adresářové struktuře a názvům souborů, tedy např. `App\Core\RouterFactory` bude v souboru `/path/to/App/Core/RouterFactory.php`. Příklad konfigurace: - -```js -{ - "autoload": { - "psr-4": { - "App\\": "app/" # jmenný prostor App\ je v adresáři app/ - } - } -} -``` - -Jak přesně chování nakonfigurovat se dozvíte v [dokumentaci Composeru|https://getcomposer.org/doc/04-schema.md#psr-4]. - - -Testování nových verzí -====================== - -Chcete otestovat novou vývojovou verzi balíčku. Jak na to? Nejprve do souboru `composer.json` přidejte tuto dvojici voleb, která dovolí instalovat vývojové verze balíčků, avšak uchýlí se k tomu pouze v případě, že neexistuje žádná kombinace stable verzí, která by vyhovovala požadavkům: - -```js -{ - "minimum-stability": "dev", - "prefer-stable": true, -} -``` - -Dále doporučujeme smazat soubor `composer.lock`, někdy totiž Composer nepochopitelně odmítá instalaci a tohle problém vyřeší. - -Dejme tomu, že jde o balíček `nette/utils` a nová verze má číslo 4.0. Nainstalujete ji příkazem: - -```shell -composer require nette/utils:4.0.x-dev -``` - -Nebo můžete nainstalovat konkrétní verzi, například 4.0.0-RC2: - -```shell -composer require nette/utils:4.0.0-RC2 -``` - -Když ale na knihovně závisí jiný balíček, který je uzamčený na starší verzi (např. `^3.1`), tak je ideální balík zaktualizovat, aby s novou verzí fungoval. Pokud však chcete omezení jen obejít a donutit Composer nainstalovat vývojovou verzi a předstírat, že jde o verzi starší (např. 3.1.6), můžete použít klíčové slovo `as`: - -```shell -composer require nette/utils "4.0.x-dev as 3.1.6" -``` - - -Volání příkazů -============== - -Přes Composer lze volat vlastní předpřipravené příkazy a skripty, jako by šlo o nativní příkazy Composeru. U skriptů, které se nacházejí ve složce `vendor/bin`, není potřeba tuto složku uvádět. - -Jako příklad si definujeme v souboru `composer.json` skript, který pomocí [Nette Testeru|tester:] spustí testy: - -```js -{ - "scripts": { - "tester": "tester tests -s" - } -} -``` - -Testy pak spustíme pomocí `composer tester`. Příkaz můžeme zavolat i v případě, že nejsme v kořenové složce projektu, ale v některém podadresáři. - - -Pošlete dík -=========== - -Ukážeme vám trik, kterým potěšíte autory open source. Jednoduchým způsobem dáte na GitHubu hvězdičku knihovnám, které váš projekt používá. Stačí nainstalovat knihovnu `symfony/thanks`: - -```shell -composer global require symfony/thanks -``` - -A poté spustit: - -```shell -composer thanks -``` - -Zkuste si to! - - -Konfigurace -=========== - -Composer je úzce propojený s verzovacím nástrojem [Git |https://git-scm.com]. Pokud jej nemáte nainstalovaný, je třeba Composeru říct, aby jej nepoužíval: - -```shell -composer -g config preferred-install dist -``` - -{{maintitle: Composer v PHP: instalace a správa závislostí}} -{{description: Nainstalujte Composer, přidejte do PHP projektu knihovny, aktualizujte je a využijte autoloading. Praktické tipy k verzím PHP, Packagistu a planým hlášením.}} diff --git a/best-practices/cs/creating-editing-form.texy b/best-practices/cs/creating-editing-form.texy deleted file mode 100644 index fd2927f35a..0000000000 --- a/best-practices/cs/creating-editing-form.texy +++ /dev/null @@ -1,206 +0,0 @@ -Formulář pro vytvoření i editaci záznamu -**************************************** - -.[perex] -Jak správně v Nette implementovat přidání a editaci záznamu, s tím, že pro obojí využijeme tentýž formulář? - -V mnoha případech bývají formuláře pro přidání i editaci záznamu stejné, liší se třeba jen popiskou na tlačítku. Ukážeme příklady jednoduchých presenterů, kde formulář použijeme nejprve pro přidání záznamu, poté pro editaci a nakonec obě řešení spojíme. - - -Přidání záznamu ---------------- - -Příklad presenteru sloužícího k přidání záznamu. Samotnou práci s databází necháme na třídě `Facade`, jejíž kód není pro ukázku podstatný. - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentRecordForm(): Form - { - $form = new Form; - - // ... přidáme políčka formuláře ... - - $form->onSuccess[] = $this->recordFormSucceeded(...); - return $form; - } - - private function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // přidání záznamu do databáze - $this->flashMessage('Záznam byl přidán'); - $this->redirect('...'); - } - - public function renderAdd(): void - { - // ... - } -} -``` - - -Editace záznamu ---------------- - -Nyní si ukážeme, jak by vypadal presenter sloužící k editaci záznamu: - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - private $record; - - public function __construct( - private Facade $facade, - ) { - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // ověření existence záznamu - || !$this->facade->isEditAllowed(/*...*/) // kontrola oprávnění - ) { - $this->error(); // chyba 404 - } - - $this->record = $record; - } - - protected function createComponentRecordForm(): Form - { - // ověříme, že akce je 'edit' - if ($this->getAction() !== 'edit') { - $this->error(); - } - - $form = new Form; - - // ... přidáme políčka formuláře ... - - $form->setDefaults($this->record); // nastavení výchozích hodnot - $form->onSuccess[] = $this->recordFormSucceeded(...); - return $form; - } - - private function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->update($this->record->id, $data); // aktualizace záznamu - $this->flashMessage('Záznam byl upraven'); - $this->redirect('...'); - } -} -``` - -V metodě *action*, která se spouští hned na začátku [životního cyklu presenteru |application:presenters#Životní cyklus presenteru], ověříme existenci záznamu a oprávnění uživatele jej editovat. - -Záznam si uložíme do property `$record`, abychom jej měli k dispozici v metodě `createComponentRecordForm()` kvůli nastavení výchozích hodnot, a `recordFormSucceeded()` kvůli ID. Alternativním řešením by bylo nastavit výchozí hodnoty přímo v `actionEdit()` a hodnotu ID, která je součástí URL, získat pomocí `getParameter('id')`: - - -```php - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - // ověření existence a kontrola oprávnění - ) { - $this->error(); - } - - // nastavení výchozích hodnot formuláře - $this->getComponent('recordForm') - ->setDefaults($record); - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); - // ... - } -``` - -Nicméně, a to by mělo být **nejdůležitějším poznatkem celého kódu**, musíme se při tvorbě formuláře ujistit, že akce je skutečně `edit`. Protože jinak by ověření v metodě `actionEdit()` vůbec neproběhlo! - - -Stejný formulář pro přidání i editaci -------------------------------------- - -A nyní oba presentery spojíme do jednoho. Buď bychom mohli v metodě `createComponentRecordForm()` rozlišit, o kterou akci jde, a podle toho formulář nakonfigurovat, nebo to můžeme nechat přímo na action-metodách a zbavit se podmínky: - - -```php -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - public function actionAdd(): void - { - $form = $this->getComponent('recordForm'); - $form->onSuccess[] = $this->addingFormSucceeded(...); - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // ověření existence záznamu - || !$this->facade->isEditAllowed(/*...*/) // kontrola oprávnění - ) { - $this->error(); // chyba 404 - } - - $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // nastavení výchozích hodnot - $form->onSuccess[] = $this->editingFormSucceeded(...); - } - - protected function createComponentRecordForm(): Form - { - // ověříme, že akce je 'add' nebo 'edit' - if (!in_array($this->getAction(), ['add', 'edit'])) { - $this->error(); - } - - $form = new Form; - - // ... přidáme políčka formuláře ... - - return $form; - } - - private function addingFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // přidání záznamu do databáze - $this->flashMessage('Záznam byl přidán'); - $this->redirect('...'); - } - - private function editingFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // aktualizace záznamu - $this->flashMessage('Záznam byl upraven'); - $this->redirect('...'); - } -} -``` - -{{priority: -1}} -{{maintitle: Jeden formulář pro přidání i editaci záznamu v Nette}} -{{description: Jak v Nette správně implementovat přidání i editaci záznamu v databázi pomocí jednoho společného formuláře, včetně výchozích hodnot a zpracování.}} diff --git a/best-practices/cs/dynamic-snippets.texy b/best-practices/cs/dynamic-snippets.texy deleted file mode 100644 index 7a8f806fc3..0000000000 --- a/best-practices/cs/dynamic-snippets.texy +++ /dev/null @@ -1,178 +0,0 @@ -Dynamické snippety -****************** - -.[perex] -Jak pomocí AJAXu obnovit jen ty části stránky, které se opravdu mění (třeba jednotlivé položky v seznamu), s využitím dynamických snippetů v Latte. - -Poměrně často při vývoji aplikací vyvstává potřeba provádět AJAXové operace například nad jednotlivými řádky tabulky či položkami seznamu. Pro příklad si můžeme zvolit výpis článků, přičemž u každého z nich umožníme přihlášenému uživateli zvolit hodnocení "líbí/nelíbí". Kód presenteru a odpovídající šablony bez AJAXu bude vypadat přibližně následovně (uvádím nejdůležitější výseky, kód počítá s existencí služby pro ukládání hodnocení a se získáním kolekce článků - konkrétní implementace není pro účely tohoto návodu důležitá): - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - $this->redirect('this'); -} - -public function handleUnlike(int $articleId): void -{ - $this->ratingService->removeLike($articleId, $this->user->id); - $this->redirect('this'); -} -``` - -Šablona: - -```latte - -``` - - -Ajaxizace -========= - -Pojďme nyní tuto jednoduchou aplikaci vybavit AJAXem. Změna hodnocení článku není natolik důležitá, aby muselo dojít k přesměrování, a proto by ideálně měla probíhat AJAXem na pozadí. Využijeme [obslužného skriptu z doplňků |application:ajax#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ě mají svá pro a proti, a proto si je ukážeme jednu po druhé. - - -Cesta dynamických snippetů -========================== - -Dynamický snippet znamená v terminologii Latte specifický případ užití značky `{snippet}`, kdy je v názvu snippetu použita proměnná. Takový snippet se nemůže v šabloně nalézat jen tak kdekoliv - musí být obalen statickým snippetem, tj. obyčejným, nebo uvnitř `{snippetArea}`. Naši šablonu bychom mohli upravit následovně. - - -```latte -{snippet articlesContainer} - -{/snippet} -``` - -Každý článek nyní definuje jeden snippet, který má v názvu ID článku. Všechny tyto snippety jsou pak dohromady zabalené jedním snippetem s názvem `articlesContainer`. Pokud bychom tento obalující snippet opomněli, Latte nás na to upozorní výjimkou. - -Zbývá nám doplnit do presenteru překreslení - stačí překreslit statickou obálku. - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - if ($this->isAjax()) { - $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- není potřeba - } else { - $this->redirect('this'); - } -} -``` - -Obdobně upravíme i sesterskou metodu `handleUnlike()`, a AJAX je funkční! - -Řešení má však jednu stinnou stránku. Pokud bychom více zkoumali, jak AJAXový požadavek probíhá, zjistili bychom, že ačkoliv se aplikace navenek tváří úsporně (vrátí pouze jeden jediný snippet pro daný článek), ve skutečnosti na serveru vykreslila snippety všechny. Kýžený snippet nám umístila do payloadu, a ostatní zahodila (zcela zbytečně je tedy také získala z databáze). - -Abychom tento proces zoptimalizovali, budeme muset zasáhnout tam, kde si do šablony předáváme kolekci `$articles` (dejme tomu v metodě `renderDefault()`). Využijeme faktu, že zpracování signálů probíhá před metodami `render`: - -```php -public function handleLike(int $articleId): void -{ - // ... - if ($this->isAjax()) { - // ... - $this->template->articles = [ - $this->db->table('articles')->get($articleId), - ]; - } else { - // ... -} - -public function renderDefault(): void -{ - if (!isset($this->template->articles)) { - $this->template->articles = $this->db->table('articles'); - } -} -``` - -Nyní se při zpracování signálu do šablony předá místo kolekce se všemi články jen pole s jediným článkem - tím, který chceme vykreslit a odeslat v payloadu do prohlížeče. `{foreach}` tedy proběhne jen jednou a žádné snippety navíc se nevykreslí. - - -Cesta komponent -=============== - -Úplně jiný způsob řešení se dynamickým snippetům vyhne. Trik spočívá v přenesení celé logiky do zvláštní komponenty - o zadávání hodnocení se nám od teď nebude starat presenter, ale vyhrazená `LikeControl`. Třída bude vypadat následovně (kromě toho bude obsahovat i metody `render`, `handleUnlike` atd.): - -```php -class LikeControl extends Nette\Application\UI\Control -{ - public function __construct( - private Article $article, - ) { - } - - public function handleLike(): void - { - $this->ratingService->saveLike($this->article->id, $this->presenter->user->id); - if ($this->presenter->isAjax()) { - $this->redrawControl(); - } else { - $this->presenter->redirect('this'); - } - } -} -``` - -Šablona komponenty: - -```latte -{snippet} - {if !$article->liked} - to se mi líbí - {else} - už se mi to nelíbí - {/if} -{/snippet} -``` - -Samozřejmě se nám změní šablona view a do presenteru budeme muset doplnit továrničku. Protože komponentu vytvoříme tolikrát, kolik z databáze získáme článků, využijeme k jejímu "rozmnožení" třídu [application:Multiplier]. - -```php -protected function createComponentLikeControl() -{ - $articles = $this->db->table('articles'); - return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { - return new LikeControl($articles[$articleId]); - }); -} -``` - -Šablona view se zmenší na nezbytné minimum (a zcela prosté snippetů!): - -```latte -
    -

    {$article->title}

    -
    {$article->content}
    - {control "likeControl-$article->id"} -
    -``` - -Máme téměř hotovo: aplikace nyní bude fungovat AJAXově. I zde nás čeká aplikaci optimalizovat, protože vzhledem k použití Nette Database se při zpracování signálu zbytečně načtou všechny články z databáze namísto jednoho. Výhodou však je, že nedojde k jejich vykreslování, protože se vyrenderuje skutečně jen naše komponenta. - -{{priority: -1}} -{{maintitle: Dynamické snippety v Nette: obnova položek seznamu přes AJAX}} -{{description: Obnovte přes AJAX jen to, co se opravdu změnilo, třeba jednu položku seznamu. Dva způsoby v Nette: dynamické snippety v Latte a komponenty.}} diff --git a/best-practices/cs/form-reuse.texy b/best-practices/cs/form-reuse.texy deleted file mode 100644 index 29f101405c..0000000000 --- a/best-practices/cs/form-reuse.texy +++ /dev/null @@ -1,351 +0,0 @@ -Znovupoužití formulářů na více místech -************************************** - -.[perex] -V Nette máte k dispozici několik možností, jak použít stejný formulář na více místech a neduplikovat kód. V tomto článku si ukážeme různá řešení, včetně těch, kterým byste se měli vyhnout. - - -Továrna na formuláře -==================== - -Jedním ze základních přístupů k použití stejné komponenty na více místech je vytvoření metody nebo třídy, která tuto komponentu generuje, a následné volání této metody na různých místech aplikace. Takové metodě nebo třídě se říká *továrna*. Nezaměňujte prosím s návrhovým vzorem *factory method*, který popisuje specifický způsob využití továren a s tímto tématem nesouvisí. - -Jako příklad si vytvoříme továrnu, která bude sestavovat editační formulář: - -```php -use Nette\Application\UI\Form; - -class FormFactory -{ - public function createEditForm(): Form - { - $form = new Form; - $form->addText('title', 'Titulek:'); - // zde se přidávají další formulářová pole - $form->addSubmit('send', 'Odeslat'); - return $form; - } -} -``` - -Nyní můžete použít tuto továrnu na různých místech ve vaší aplikaci, například v presenterech nebo komponentách. A to tak, že si ji [vyžádáme jako závislost|dependency-injection:passing-dependencies]. Nejprve tedy třídu zapíšeme do konfiguračního souboru: - -```neon -services: - - FormFactory -``` - -A poté ji použijeme v presenteru: - - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->createEditForm(); - $form->onSuccess[] = function () { - // zpracování odeslaných dat - }; - return $form; - } -} -``` - -Formulářovou továrnu můžete rozšířit o další metody pro vytváření dalších druhů formulářů podle potřeby vaší aplikace. A samozřejmě můžeme přidat i metodu, která vytvoří základní formulář bez prvků, a tu budou ostatní metody využívat: - -```php -class FormFactory -{ - public function createForm(): Form - { - $form = new Form; - return $form; - } - - public function createEditForm(): Form - { - $form = $this->createForm(); - $form->addText('title', 'Titulek:'); - // zde se přidávají další formulářová pole - $form->addSubmit('send', 'Odeslat'); - return $form; - } -} -``` - -Metoda `createForm()` zatím nedělá nic užitečného, ale to se rychle změní. - - -Závislosti továrny -================== - -Časem se ukáže, že potřebujeme, aby formuláře byly multijazyčné. To znamená, že všem formulářům musíme nastavit tzv. [translator |forms:rendering#Překládání]. Za tím účelem upravíme třídu `FormFactory` tak, aby přijímala objekt `Translator` jako závislost v konstruktoru, a předáme jej formuláři: - -```php -use Nette\Localization\Translator; - -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function createForm(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } - - // ... -} -``` - -Jelikož metodu `createForm()` volají i ostatní metody tvořící specifické formuláře, stačí translator nastavit jen v ní. A máme hotovo. Není potřeba měnit kód žádného presenteru nebo komponenty, což je skvělé. - - -Více továrních tříd -=================== - -Alternativně můžete vytvořit více tříd pro každý formulář, který chcete použít ve vaší aplikaci. Tento přístup může zvýšit čitelnost kódu a usnadnit správu formulářů. Původní `FormFactory` necháme vytvářet jen čistý formulář se základní konfigurací (například s podporou překladů) a pro editační formulář vytvoříme novou továrnu `EditFormFactory`. - -```php -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function create(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } -} - - -// ✅ použití kompozice -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - // zde se přidávají další formulářová pole - $form->addSubmit('send', 'Odeslat'); - return $form; - } -} -``` - -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í |nette:introduction-to-object-oriented-programming#Dědičnost]: - -```php -// ⛔ TAKHLE NE! SEM DĚDIČNOST NEPATŘÍ -class EditFormFactory extends FormFactory -{ - public function create(): Form - { - $form = parent::create(); - $form->addText('title', 'Titulek:'); - // zde se přidávají další formulářová pole - $form->addSubmit('send', 'Odeslat'); - return $form; - } -} -``` - -Použití dědič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í |dependency-injection:faq#Proč se upřednostňuje kompozice před dědičností]. - - -Obsluha formuláře -================= - -Obsluha formuláře, která se zavolá po úspěšném odeslání, může být také součástí tovární třídy. Bude fungovat tak, že odeslaná data předá modelu ke zpracování. Případné chyby [předá zpět |forms:validation#Chyby při zpracování] do formuláře. Model v následujícím příkladu reprezentuje třída `Facade`: - -```php -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - private Facade $facade, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - $form->addText('title', 'Titulek:'); - // zde se přidávají další formulářová pole - $form->addSubmit('send', 'Odeslat'); - $form->onSuccess[] = $this->processForm(...); - return $form; - } - - private function processForm(Form $form, array $data): void - { - try { - // zpracování odeslaných dat - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - } - } -} -``` - -Samotné přesměrování ale necháme na presenteru. Ten přidá události `onSuccess` další handler, který přesměrování provede. Díky tomu bude možné formulář použít v různých presenterech a v každém přesměrovat jinam. - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditFormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->create(); - $form->onSuccess[] = function () { - $this->flashMessage('Záznam byl uložen'); - $this->redirect('Homepage:'); - }; - return $form; - } -} -``` - -Toto řešení využívá vlastnost formulářů, že když se nad formulářem nebo jeho prvkem zavolá `addError()`, už se další handler `onSuccess` nevolá. - - -Dědění od třídy Form -==================== - -Sestavený formulář nemá být potomkem formuláře. Jinými slovy, nepoužívejte toto řešení: - -```php -// ⛔ TAKHLE NE! SEM DĚDIČNOST NEPATŘÍ -class EditForm extends Form -{ - public function __construct(Translator $translator) - { - parent::__construct(); - $this->addText('title', 'Titulek:'); - // zde se přidávají další formulářová pole - $this->addSubmit('send', 'Odeslat'); - $this->setTranslator($translator); - } -} -``` - -Místo sestavování formuláře v konstruktoru použijte továrnu. - -Je potřeba si uvědomit, že třída `Form` je v první řadě nástrojem pro sestavení formuláře, tedy *form builder*. A sestavený formulář lze chápat jako její produkt. Jenže produkt není specifickým případem builderu, není mezi nimi vazba *is a* tvořící základ dědičnosti. - - -Komponenta s formulářem -======================= - -Zcela jiný přístup představuje tvorba [komponenty|application:components], jejíž součástí je formulář. To dává nové možnosti, například vykreslit formulář specifickým způsobem, neboť součástí komponenty je i šablona. Nebo lze využít signály pro AJAXovou komunikaci a donačítání informací do formuláře, například pro napovídání, atd. - - -```php -use Nette\Application\UI\Form; - -class EditControl extends Nette\Application\UI\Control -{ - public array $onSave = []; - - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentForm(): Form - { - $form = new Form; - $form->addText('title', 'Titulek:'); - // zde se přidávají další formulářová pole - $form->addSubmit('send', 'Odeslat'); - $form->onSuccess[] = $this->processForm(...); - - return $form; - } - - private function processForm(Form $form, array $data): void - { - try { - // zpracování odeslaných dat - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - return; - } - - // vyvolání události - $this->onSave($this, $data); - } -} -``` - -Ještě vytvoříme továrnu, která bude tuto komponentu vyrábět. Stačí [zapsat její rozhraní |application:components#Komponenty se závislostmi]: - -```php -interface EditControlFactory -{ - function create(): EditControl; -} -``` - -A přidat do konfiguračního souboru: - -```neon -services: - - EditControlFactory -``` - -A nyní už můžeme továrnu vyžádat a použít v presenteru: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditControlFactory $controlFactory, - ) { - } - - protected function createComponentEditForm(): EditControl - { - $control = $this->controlFactory->create(); - - $control->onSave[] = function (EditControl $control, $data) { - $this->redirect('this'); - // nebo přesměrujeme na výsledek editace, např.: - // $this->redirect('detail', ['id' => $data->id]); - }; - - return $control; - } -} -``` - -{{maintitle: Znovupoužití formulářů v Nette: továrny a komponenty}} -{{description: Použijte stejný formulář na více místech bez duplikace kódu: továrny a komponenty s formulářem a také řešení, kterým je lepší se vyhnout, jako je dědění.}} diff --git a/best-practices/cs/inject-method-attribute.texy b/best-practices/cs/inject-method-attribute.texy deleted file mode 100644 index 9d29772a68..0000000000 --- a/best-practices/cs/inject-method-attribute.texy +++ /dev/null @@ -1,64 +0,0 @@ -Metody a atributy inject -************************ - -.[perex] -V tomto článku se zaměříme na různé způsoby předávání závislostí do presenterů v Nette frameworku. Porovnáme preferovaný způsob, kterým je konstruktor, s dalšími možnostmi, jako jsou metody a atributy `inject`. - -I pro presentery platí, že předání závislostí pomocí [konstruktoru |dependency-injection:passing-dependencies#Předávání konstruktorem] je preferovaná cesta. Pokud ale vytváříte společného předka, od kterého dědí ostatní presentery (např. `BasePresenter`), a tento předek má také závislosti, nastane problém, kterému říkáme [constructor hell |dependency-injection:passing-dependencies#Constructor hell]. Ten lze obejít pomocí alternativních cest, které představují metody a atributy (anotace) `inject`. - - -Metody `inject*()` -================== - -Jde o formu předávání závislosti [setterem |dependency-injection:passing-dependencies#Předávání setterem]. Název těchto setterů začíná předponou `inject`. Nette DI takto pojmenované metody automaticky zavolá hned po vytvoření instance presenteru a předá jim všechny požadované závislosti. Musí být tudíž deklarované jako public. - -Metody `inject*()` lze považovat za jakési rozšíření konstruktoru do více metod. Díky tomu může `BasePresenter` převzít závislosti přes jinou metodu a ponechat konstruktor volný pro své potomky: - -```php -abstract class BasePresenter extends Nette\Application\UI\Presenter -{ - private Foo $foo; - - public function injectBase(Foo $foo): void - { - $this->foo = $foo; - } -} - -class MyPresenter extends BasePresenter -{ - private Bar $bar; - - public function __construct(Bar $bar) - { - $this->bar = $bar; - } -} -``` - -Metod `inject*()` může presenter obsahovat libovolný počet a každá může mít libovolný počet parametrů. Skvěle se hodí také v případech, kdy je presenter [složen z trait |presenter-traits] a každá z nich si žádá vlastní závislost. - - -Atributy `Inject` -================= - -Jde o formu [injektování do property |dependency-injection:passing-dependencies#Nastavením proměnné]. Stačí označit, do kterých proměnných se má injektovat, a Nette DI automaticky předá závislosti hned po vytvoření instance presenteru. Aby je mohl vložit, je nutné je deklarovat jako public. - -Properties označíme atributem (dříve se používala anotace `/** @inject */`): - -```php -use Nette\DI\Attributes\Inject; // tento řádek je důležitý - -class MyPresenter extends Nette\Application\UI\Presenter -{ - #[Inject] - public Cache $cache; -} -``` - -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 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žádoucí. - -{{maintitle: Metody a atributy inject v presenterech Nette}} -{{description: Způsoby předávání závislostí do presenterů v Nette: preferovaný konstruktor v porovnání s metodami inject a atributem Inject.}} diff --git a/best-practices/cs/lets-create-contact-form.texy b/best-practices/cs/lets-create-contact-form.texy deleted file mode 100644 index f2f06be156..0000000000 --- a/best-practices/cs/lets-create-contact-form.texy +++ /dev/null @@ -1,221 +0,0 @@ -Vytváříme kontaktní formulář -**************************** - -.[perex] -Podíváme se na to, jak v Nette vytvořit kontaktní formulář včetně odesílání na email. Tak tedy do toho! - -Nejprve musíme vytvořit nový projekt. Jak na to, vysvětluje stránka [Začínáme |nette:installation]. A pak už můžeme začít s tvorbou formuláře. - -Nejjednodušší je vytvoření [formuláře přímo v presenteru |forms:in-presenter]. Můžeme využít předpřipravený `HomePresenter`. Do něj přidáme komponentu `contactForm` představující formulář. Uděláme to tak, že do kódu zapíšeme tovární metodu `createComponentContactForm()`, která komponentu vyrobí: - -```php -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - protected function createComponentContactForm(): Form - { - $form = new Form; - $form->addText('name', 'Jméno:') - ->setRequired('Zadejte jméno'); - $form->addEmail('email', 'E-mail:') - ->setRequired('Zadejte e-mail'); - $form->addTextArea('message', 'Zpráva:') - ->setRequired('Zadejte zprávu'); - $form->addSubmit('send', 'Odeslat'); - $form->onSuccess[] = $this->contactFormSucceeded(...); - return $form; - } - - private function contactFormSucceeded(Form $form, $data): void - { - // odeslání emailu - } -} -``` - -Jak vidíte, vytvořili jsme dvě metody. První metoda `createComponentContactForm()` vytváří nový formulář. Ten má políčka pro jméno, email a zprávu, která přidáváme metodami `addText()`, `addEmail()` a `addTextArea()`. Také jsme přidali tlačítko pro odeslání formuláře. 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 `contactForm` necháme vykreslit v šabloně `Home/default.latte`: - -```latte -{block content} -

    Kontaktní formulář

    -{control contactForm} -``` - -Pro samotné odeslání emailu vytvoříme novou třídu, kterou nazveme `ContactFacade` a umístíme ji do souboru `app/Model/ContactFacade.php`: - -```php -namespace App\Model; - -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $mail = new Message; - $mail->addTo('admin@example.com') // váš email - ->setFrom($email, $name) - ->setSubject('Zpráva z kontaktního formuláře') - ->setBody($message); - - $this->mailer->send($mail); - } -} -``` - -Metoda `sendMessage()` vytvoří a odešle email. Využívá k tomu tzv. mailer, který si nechá předat jako závislost přes konstruktor. Přečtete si více o [odesílání emailů |mail:]. - -Nyní se vrátíme zpátky k presenteru a dokončíme metodu `contactFormSucceeded()`. Ta zavolá metodu `sendMessage()` třídy `ContactFacade` a předá jí údaje z formuláře. A jak získáme objekt `ContactFacade`? Necháme si jej předat konstruktorem: - -```php -use App\Model\ContactFacade; -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - public function __construct( - private ContactFacade $facade, - ) { - } - - protected function createComponentContactForm(): Form - { - // ... - } - - public function contactFormSucceeded(stdClass $data): void - { - $this->facade->sendMessage($data->email, $data->name, $data->message); - $this->flashMessage('Zpráva byla odeslána'); - $this->redirect('this'); - } -} -``` - -Poté, co se email odešle, ještě zobrazíme uživateli tzv. [flash message |application:components#Flash zprávy], potvrzující, že zpráva se odeslala, a poté přesměrujeme na další stránku, aby nebylo možné formulář opakovaně odeslat pomocí *refresh* v prohlížeči. - - -Tak, a pokud všechno funguje, měli byste být schopni odeslat email z vašeho kontaktního formuláře. Gratuluji! - - -HTML šablona emailu -------------------- - -Zatím se odesílá prostý textový email obsahující pouze zprávu odeslanou formulářem. V emailu ale můžeme využít HTML a udělat jeho podobu atraktivnější. Vytvoříme pro něj šablonu v Latte, kterou zapíšeme do `app/Model/contactEmail.latte`: - -```latte - - Zpráva z kontaktního formuláře - - -

    Jméno: {$name}

    -

    E-mail: {$email}

    -

    Zpráva: {$message}

    - - -``` - -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 šablonu]. Pomocí metody `renderToString()` šablonu vykreslíme do řetězce, prvním parametrem je cesta k šabloně a druhým jsou proměnné. - -```php -namespace App\Model; - -use Nette\Bridges\ApplicationLatte\LatteFactory; -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $latte = $this->latteFactory->create(); - $body = $latte->renderToString(__DIR__ . '/contactEmail.latte', [ - 'email' => $email, - 'name' => $name, - 'message' => $message, - ]); - - $mail = new Message; - $mail->addTo('admin@example.com') // váš email - ->setFrom($email, $name) - ->setHtmlBody($body); - - $this->mailer->send($mail); - } -} -``` - -Vygenerovaný HTML email pak předáme metodě `setHtmlBody()` místo původní `setBody()`. Taktéž nemusíme uvádět předmět emailu v `setSubject()`, protože si jej knihovna vezme z elementu `` šablony. - - -Konfigurace ------------ - -V kódu třídy `ContactFacade` je pořád natvrdo zapsaný náš administrátorský email `admin@example.com`. Bylo by lepší jej přesunout do konfiguračního souboru. Jak na to? - -Nejprve upravíme třídu `ContactFacade` a řetězec s emailem nahradíme proměnnou předanou konstruktorem: - -```php -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - private string $adminEmail, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - // ... - $mail = new Message; - $mail->addTo($this->adminEmail) - ->setFrom($email, $name) - ->setHtmlBody($body); - // ... - } -} -``` - -A druhým krokem je uvedení hodnoty této proměnné v konfiguraci. Do souboru `app/config/services.neon` zapíšeme: - -```neon -services: - - App\Model\ContactFacade(adminEmail: admin@example.com) -``` - -A je to. Pokud by položek v sekci `services` bylo hodně a měli bychom pocit, že se email mezi nimi ztrácí, můžeme z něj udělat proměnnou. Upravíme zápis na: - -```neon -services: - - App\Model\ContactFacade(adminEmail: %adminEmail%) -``` - -A v souboru `app/config/common.neon` nadefinujeme tuto proměnnou: - -```neon -parameters: - adminEmail: admin@example.com -``` - -A je hotovo! - -{{maintitle: Kontaktní formulář v PHP s Nette: odeslání na e-mail}} -{{description: Vytvořte krok za krokem kontaktní formulář v Nette, od samotného formuláře až po odeslání dat na e-mail pomocí šablony Latte.}} diff --git a/best-practices/cs/microsites.texy b/best-practices/cs/microsites.texy deleted file mode 100644 index 29fc8e5379..0000000000 --- a/best-practices/cs/microsites.texy +++ /dev/null @@ -1,66 +0,0 @@ -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 -<?php - -require __DIR__ . '/../vendor/autoload.php'; - -$configurator = new Nette\Bootstrap\Configurator; -$configurator->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); - return $presenter->redirectUrl("/$lang"); -}); - -// routa pro URL https://example.com/cs nebo https://example.com/en -$router->addRoute('<lang cs|en|de>', 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#Dynamické routování 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 spolehnout na [automatické escapování |latte:safety-first], aby nevznikla zranitelnost XSS. -- Nette taky zajistí, že se při chybě nikdy nezobrazí 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. - -{{maintitle: Mikroweby v Nette: malý web v jednom PHP souboru}} -{{description: Potřebujete rychle malý web? Celý jeho kód se vejde do jednoho index.php s routováním Nette a šablonami Latte a přitom máte Tracy i další vymoženosti.}} diff --git a/best-practices/cs/pagination.texy b/best-practices/cs/pagination.texy deleted file mode 100644 index ed02e65eb5..0000000000 --- a/best-practices/cs/pagination.texy +++ /dev/null @@ -1,275 +0,0 @@ -Stránkování výsledků databáze -***************************** - -.[perex] -Při tvorbě webových aplikací se velmi často setkáte s požadavkem na omezení počtu vypsaných položek na stránce. - -Vyjdeme ze stavu, kdy vypisujeme všechna data bez stránkování. Pro výběr dat z databáze máme třídu ArticleRepository, která kromě konstruktoru obsahuje metodu `findPublishedArticles`, která vrací všechny publikované články seřazené sestupně podle data publikace. - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC', - new \DateTime, - ); - } -} -``` - -V presenteru si pak necháme předat modelovou třídu a v render metodě si vyžádáme publikované články, které předáme do šablony: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(): void - { - $this->template->articles = $this->articleRepository->findPublishedArticles(); - } -} -``` - -V šabloně `default.latte` se pak postaráme o výpis článků: - -```latte -{block content} -<h1>Články</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> -``` - - -Tímto způsobem umíme vypsat všechny články, což však začne působit problémy ve chvíli, kdy počet článků vzroste. V tom okamžiku přijde vhod implementace stránkovacího mechanismu. - -Ten zajistí, že se všechny články rozdělí do několika stránek a my zobrazíme jen články jedné aktuální stránky. Celkový počet stránek a rozdělení článků si vypočte [utils:Paginator] sám podle toho, kolik článků celkem máme a kolik článků na stránku chceme zobrazit. - -V prvním kroku si upravíme metodu pro získání článků ve třídě repositáře tak, aby nám uměla vracet jen články pro jednu stránku. Také přidáme metodu pro zjištění celkového počtu článků v databázi, kterou budeme potřebovat pro nastavení Paginatoru: - -```php -namespace App\Model; - -use Nette; - - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(int $limit, int $offset): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC - LIMIT ? - OFFSET ?', - new \DateTime, $limit, $offset, - ); - } - - /** - * Vrací celkový počet publikovaných článků - */ - public function getPublishedArticlesCount(): int - { - return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime); - } -} -``` - -Následně se pustíme do úprav presenteru. Do render metody budeme předávat číslo aktuálně zobrazené stránky. Pro případ, kdy nebude toto číslo součástí URL, nastavíme výchozí hodnotu první stránky. - -Dále také render metodu rozšíříme o získání instance Paginatoru, jeho nastavení a výběru správných článků pro zobrazení v šabloně. HomePresenter bude po úpravách vypadat takto: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Zjistíme si celkový počet publikovaných článků - $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - - // Vyrobíme si instanci Paginatoru a nastavíme jej - $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // celkový počet článků - $paginator->setItemsPerPage(10); // počet položek na stránce - $paginator->setPage($page); // číslo aktuální stránky - - // Z databáze si vytáhneme omezenou množinu článků podle výpočtu Paginatoru - $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - - // kterou předáme do šablony - $this->template->articles = $articles; - // a také samotný Paginator pro zobrazení možností stránkování - $this->template->paginator = $paginator; - } -} -``` - -Šablona nám už nyní iteruje jen nad články jedné stránky, stačí nám přidat stránkovací odkazy: - -```latte -{block content} -<h1>Články</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if !$paginator->isFirst()} - <a n:href="default, 1">První</a> -  |  - <a n:href="default, $paginator->getPage() - 1">Předchozí</a> -  |  - {/if} - - Stránka {$paginator->getPage()} z {$paginator->getPageCount()} - - {if !$paginator->isLast()} -  |  - <a n:href="default, $paginator->getPage() + 1">Další</a> -  |  - <a n:href="default, $paginator->getPageCount()">Poslední</a> - {/if} -</div> -``` - - -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], můžeme stránkování implementovat i bez použití Paginatoru. Třída `Nette\Database\Table\Selection` totiž obsahuje metodu [page() |api:Nette\Database\Table\Selection::page()], která logiku stránkování zapouzdřuje. - -Repozitář bude při tomto způsobu implementace vypadat takto: - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Explorer $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\Table\Selection - { - return $this->database->table('articles') - ->where('created_at < ', new \DateTime) - ->order('created_at DESC'); - } -} -``` - -V presenteru nemusíme vytvářet Paginator, použijeme místo něj metodu třídy `Selection`, kterou nám vrací repozitář: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Vytáhneme si publikované články - $articles = $this->articleRepository->findPublishedArticles(); - - // a do šablony pošleme pouze jejich část omezenou podle výpočtu metody page - $lastPage = 0; - $this->template->articles = $articles->page($page, 10, $lastPage); - - // a také potřebná data pro zobrazení možností stránkování - $this->template->page = $page; - $this->template->lastPage = $lastPage; - } -} -``` - -Protože do šablony nyní neposíláme Paginator, upravíme část zobrazující stránkovací odkazy: - -```latte -{block content} -<h1>Články</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if $page > 1} - <a n:href="default, 1">První</a> -  |  - <a n:href="default, $page - 1">Předchozí</a> -  |  - {/if} - - Stránka {$page} z {$lastPage} - - {if $page < $lastPage} -  |  - <a n:href="default, $page + 1">Další</a> -  |  - <a n:href="default, $lastPage">Poslední</a> - {/if} -</div> -``` - -Tímto způsobem jsme implementovali stránkovací mechanismus bez použití Paginatoru. - -{{priority: -1}} -{{maintitle: Stránkování výsledků z databáze v PHP s Nette}} -{{description: Výpis výsledků z databáze po stránkách v Nette: metoda v repozitáři, třída Paginator, odkazy v šabloně a jednodušší cesta s Nette Database Explorerem.}} diff --git a/best-practices/cs/passing-settings-to-presenters.texy b/best-practices/cs/passing-settings-to-presenters.texy deleted file mode 100644 index f52e891163..0000000000 --- a/best-practices/cs/passing-settings-to-presenters.texy +++ /dev/null @@ -1,52 +0,0 @@ -Předání nastavení do presenterů -******************************* - -.[perex] -Potřebujete do presenterů předávat argumenty, které nejsou objekty (např. informaci, zda běží v debug režimu, cesty k adresářům apod.), a tedy nemohou být předány automaticky pomocí autowiringu? Řešením je zapouzdřit je do objektu `Settings`. - -Služba `Settings` představuje velmi snadný a přitom užitečný způsob, jak poskytovat informace o běžící aplikaci presenterům. Její konkrétní podoba záleží čistě na vašich konkrétních potřebách. Příklad: - -```php -namespace App; - -class Settings -{ - public function __construct( - // od PHP 8.1 je možné uvést readonly - public bool $debugMode, - public string $appDir, - // a tak dále - ) {} -} -``` - -Ukázka registrace do konfigurace: - -```neon -services: - - App\Settings( - %debugMode%, - %appDir%, - ) -``` - -Když bude presenter potřebovat informace poskytované touto službou, jednoduše si o ni řekne v konstruktoru: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private App\Settings $settings, - ) {} - - public function renderDefault() - { - if ($this->settings->debugMode) { - // ... - } - } -} -``` - -{{maintitle: Předání nastavení do presenterů v Nette: objekt Settings}} -{{description: Potřebujete do presenterů předat hodnoty, které nejsou objekty, třeba debug režim nebo cesty k adresářům? Autowiring to neumí, zabalte je do objektu Settings.}} diff --git a/best-practices/cs/post-links.texy b/best-practices/cs/post-links.texy deleted file mode 100644 index f986978b31..0000000000 --- a/best-practices/cs/post-links.texy +++ /dev/null @@ -1,59 +0,0 @@ -Jak správně používat POST odkazy -******************************** - -.[perex] -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. Ideální by sice 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} -<form method="post" id="postForm"></form> -``` - -Díky tomuto formuláři můžete místo klasického odkazu `<a>` použít tlačítko `<button>`, které lze vizuálně upravit tak, aby vypadalo jako běžný odkaz. Například CSS framework Bootstrap nabízí třídy `btn btn-link`, se kterými dosáhnete toho, že tlačítko nebude vizuálně odlišné od ostatních odkazů. Pomocí atributu `form="postForm"` ho provážeme s předpřipraveným formulářem: - -```latte .{file:admin.latte} -<table> - <tr n:foreach="$posts as $post"> - <td>{$post->title}</td> - <td> - <button class="btn btn-link" form="postForm" formaction="{link delete, $post->id}">delete</button> - <!-- instead of <a n:href="delete, $post->id">delete</a> --> - </td> - </tr> -</table> -``` - -Při kliknutí na odkaz se nyní vyvolá akce `delete`. Pro zajištění, že požadavky budou přijímány pouze prostřednictvím metody POST a z téže domény (což je účinná obrana proti CSRF útokům), použijte atribut `#[Requires]`: - -```php .{file:AdminPresenter.php} -use Nette\Application\Attributes\Requires; - -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST', sameOrigin: true)] - public function actionDelete(int $id): void - { - $this->facade->deletePost($id); // hypotetický kód mazající záznam - $this->redirect('default'); - } -} -``` - -Atribut existuje od Nette Application 3.2 a více o jeho možnostech se dozvíte na stránce [Jak používat atribut #Requires |attribute-requires]. - -Pokud byste místo akce `actionDelete()` používali signál `handleDelete()`, není nutné uvádět `sameOrigin: true`, protože signály mají tuto ochranu nastavenou implicitně: - -```php .{file:AdminPresenter.php} -#[Requires(methods: 'POST')] -public function handleDelete(int $id): void -{ - $this->facade->deletePost($id); - $this->redirect('this'); -} -``` - -Tento přístup nejenže zlepšuje bezpečnost vaší aplikace, ale také přispívá k dodržování správných webových standardů a praxe. Využitím metod POST pro akce měnící stav dosáhnete robustnější a bezpečnější aplikace. - -{{maintitle: POST odkazy v Nette: neměňte stav serveru přes GET}} -{{description: Akce měnící stav serveru nesmí používat GET. Jak v Nette vytvořit POST odkazy pomocí Latte a zabezpečit je atributem #[Requires].}} diff --git a/best-practices/cs/presenter-traits.texy b/best-practices/cs/presenter-traits.texy deleted file mode 100644 index 95f05f53db..0000000000 --- a/best-practices/cs/presenter-traits.texy +++ /dev/null @@ -1,50 +0,0 @@ -Skládání presenterů z trait -*************************** - -.[perex] -Pokud potřebujeme ve více presenterech implementovat stejný kód (např. ověření, že je uživatel přihlášen), nabízí se umístit kód do společného předka. Druhou možností je vytvoření jednoúčelových [trait |nette:introduction-to-object-oriented-programming#Traity]. - -Výhoda tohoto řešení je, že každý z presenterů může použít právě ty traity, které skutečně potřebuje, zatímco vícenásobná dědičnost není v PHP možná. - -Tyto traity mohou využívat skutečnosti, že při vytvoření presenteru se postupně zavolají všechny [inject metody |inject-method-attribute#Metody inject]. Jen je nutné dohlédnout na to, aby název každé inject metody byl unikátní napříč všemi použitými traitami i samotným presenterem. - -Traity mohou navěsit inicializační kód do událostí [onStartup nebo onRender |application:presenters#Události]. - -Příklady: - -```php -trait RequireLoggedUser -{ - public function injectRequireLoggedUser(): void - { - $this->onStartup[] = function () { - if (!$this->getUser()->isLoggedIn()) { - $this->redirect('Sign:in', $this->storeRequest()); - } - }; - } -} - -trait StandardTemplateFilters -{ - public function injectStandardTemplateFilters(TemplateBuilder $builder): void - { - $this->onRender[] = function () use ($builder) { - $builder->setupTemplate($this->template); - }; - } -} -``` - -Presenter pak tyto traity jednoduše použije: - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - use StandardTemplateFilters; - use RequireLoggedUser; -} -``` - -{{maintitle: Skládání presenterů v Nette z trait}} -{{description: Sdílejte kód, jako je ověření přihlášení, napříč presentery pomocí jednoúčelových trait místo společného předka, včetně toho, jak se trait zapojí do startu.}} diff --git a/best-practices/cs/pretty-urls.texy b/best-practices/cs/pretty-urls.texy deleted file mode 100644 index ca1c7bda0f..0000000000 --- a/best-practices/cs/pretty-urls.texy +++ /dev/null @@ -1,207 +0,0 @@ -Hezké URL se slugem -******************* - -.[perex] -URL jako `/clanek/123-jak-upect-chleba` vypadá lépe než `/clanek/123` a pomáhá uživatelům i vyhledávačům pochopit, co na stránce čeká. Tento návod ukazuje, jak je generovat čistě v routeru - bez zásahu do jediné šablony - a jak zařídit, aby každý návštěvník skončil na kanonické URL. - - -Proč slug v URL -=============== - -Porovnejte tyto dvě adresy: - -``` -/clanek/123 -/clanek/123-jak-upect-chleba -``` - -Druhá uživateli (a Googlu) prozradí, co ho po kliknutí čeká. To je dobré pro SEO, dělá odkazy čitelné v chatu nebo e-mailu a dá smysl i URL liště. - -Slug ale není skutečný identifikátor. Stránku určuje ID. Slug je jen dekorace, kterou aplikace generuje z titulku. Když se titulek změní, slug by se měl změnit taky. A když někdo URL ručně upraví nebo přijde po starém odkazu, aplikace by stejně měla najít správnou stránku. - - -Cíl -=== - -Chceme routu, která zvládne všechny tyto případy: - -``` -/clanek/123 → otevře článek 123, přesměruje na kanonickou URL -/clanek/123-jak-upect-chleba → otevře článek 123 přímo -/clanek/123-cokoli-co-nekdo-napsal → otevře článek 123, přesměruje na kanonickou URL -/clanek/ → 404 (chybí ID) -``` - -A chceme, aby každé `n:href` a `link()` napříč aplikací automaticky vyrobilo `/clanek/123-jak-upect-chleba` - **bez přepisování jediné šablony**. - - -Maska routy -=========== - -Trik spočívá v označení slugu v masce jako **nepovinného** pomocí hranatých závorek: - -```php -$router->addRoute('clanek/<id [0-9]+>[-<slug>]', 'Article:detail'); -``` - -Maska `[-<slug>]` říká: po ID může (ale nemusí) následovat pomlčka a slug. Routa přijímá `/clanek/123` i `/clanek/123-cokoli`. - -Poznámka k parametru `<slug>`: ve výchozím stavu odpovídá libovolným znakům **kromě lomítka** - přesně to, co chceme. Pokud napíšete `<slug .+>`, bude parametr odpovídat i lomítkům, takže `/clanek/123-neco/jineho` by se naparsovalo jako jediný slug obsahující `/`. Pokud nechcete lomítka ve slugu, zůstaňte u výchozího `<slug>`. - -URL se teď parsuje správně, ale generované odkazy slug neobsahují. Dalším krokem je routu naučit, jak slug doplnit. - - -Generování slugu bez zásahu do šablon -===================================== - -Tohle je hlavní varianta. Stávající volání `n:href="Article:detail, $id"` zůstávají beze změny napříč celou aplikací - router si titulek vyhledá sám. - -Použijeme **obecný filtr** pod klíčem prázdného stringu - ten vidí všechny parametry najednou a může slug doplnit: - -```php -use Nette\Routing\Route; -use Nette\Utils\Strings; - -$router->addRoute('clanek/<id [0-9]+>[-<slug>]', [ - 'presenter' => 'Article', - 'action' => 'detail', - '' => [ - Route::FilterOut => function (array $params) use ($slugProvider): array { - if (isset($params['id']) && empty($params['slug'])) { - $params['slug'] = $slugProvider->getSlug((int) $params['id']); - } - return $params; - }, - ], -]); -``` - -`FilterOut` se spustí pokaždé, když router **generuje** URL. Pokud slug nebyl předán, filtr titulek dohledá a doplní. - -Slugy můžete nasadit napříč celou aplikací jedinou změnou - jednou definicí routy. Každý odkaz v každé šabloně začne automaticky produkovat `/clanek/123-jak-upect-chleba`. Žádný grep, žádné hledání po šablonách, žádný přehlédnutý případ. - - -Cache pro vyhledávání -===================== - -Jedno volání odkazu znamená jeden DB dotaz, ale typická stránka jich má hodně - výpisy, drobečková navigace, "naposledy prohlížené", související články. Stejné ID článku se v rámci jednoho požadavku objeví v několika odkazech a nechceme do DB chodit pokaždé. - -Stačí drobná cache platná v rámci jednoho požadavku. Obalte DB volání malou službou: - -```php -final class SlugProvider -{ - /** @var array<int, string> */ - private array $cache = []; - - public function __construct( - private Nette\Database\Explorer $db, - ) { - } - - public function getSlug(int $id): string - { - return $this->cache[$id] ??= Strings::webalize(Strings::truncate( - (string) $this->db->fetchField('SELECT title FROM article WHERE id = ?', $id), - 100, '' - )); - } -} -``` - -To stačí - jeden DB dotaz na unikátní ID za požadavek. - - -Předání titulku ze šablony (volitelná rychlá cesta) -=================================================== - -Pokud máte titulek v šabloně po ruce, můžete se DB dotazu úplně vyhnout. Předejte titulek jako pojmenovaný parametr: - -```latte -<a n:href="Article:detail, $article->id, slug => $article->title">{$article->title}</a> -``` - -…a přidejte per-parametrový `FilterOut`, který titulek převede na URL-bezpečný tvar: - -```php -$router->addRoute('clanek/<id [0-9]+>[-<slug>]', [ - 'presenter' => 'Article', - 'action' => 'detail', - 'slug' => [ - Route::FilterOut => fn($title) => Strings::webalize(Strings::truncate($title, 100, '')), - ], - '' => [/* fallback s vyhledáním z předchozí ukázky */], -]); -``` - -Oba filtry spolupracují. Obecný filtr proběhne první; vidí, že slug je už vyplněn předaným titulkem, a vyhledání v DB přeskočí. Per-parametrový `FilterOut` pak tento titulek převede na výsledný slug. Šablony, které titulek nepředávají, dál fungují. Obecný filtr najde slug prázdný a projde cestou s vyhledáváním. - -Použijte to jen tam, kde to opravdu hraje roli (velké výpisy vykreslované stokrát za požadavek). Pro většinu aplikace cachované vyhledávání stačí. - - -Kanonizace: přesměrování na správnou URL -======================================== - -Umíme teď generovat `/clanek/123-jak-upect-chleba`, ale routa pořád přijímá `/clanek/123` i `/clanek/123-cokoli-co-nekdo-napsal`. To je záměr - chceme krátké URL (viz níže) a chceme, aby staré nebo ručně napsané odkazy fungovaly. Ale nechceme, aby vyhledávače indexovaly stejný článek pod několika adresami. - -Řešením je [kanonizace |application:presenters#kanonizace]: když uživatel přijde po nekanonické URL, aplikace ho přesměruje 301 na správnou. Stará se o to metoda `canonicalize()`: - -```php -public function actionDetail(int $id, ?string $slug = null): void -{ - $article = $this->facade->getArticle($id); - if (!$article) { - $this->error(); - } - - // vygeneruje kanonickou URL přes stejný FilterOut - // a pokud se liší od současné URL, přesměruje HTTP 301 - $this->canonicalize('detail', ['id' => $id]); - - $this->template->article = $article; -} -``` - -`canonicalize()` vygeneruje kanonickou URL stejným způsobem jako `link()` (takže projde stejným `FilterOut`) a porovná ji s aktuální URL. Pokud se liší, přesměruje HTTP 301. Návštěvník skončí na správné URL, vyhledávače vidí jen jednu kanonickou verzi. - - -Jedno místo, které určuje, jak slug vypadá -========================================== - -Všimněte si, že `Strings::webalize(Strings::truncate(..., 100, ''))` žije na **jediném místě** - uvnitř `SlugProvider` (nebo v per-parametrovém `FilterOut`). Stejná logika vyrobí odkaz v šabloně, URL v `redirect()` i kanonický tvar v `canonicalize()`. - -Když budete chtít pravidla později změnit (jiný limit délky, jiná transliterace, vyhazování dalších znaků), upravíte jeden řádek. Bez tohoto byste riskovali, že `redirect()` vygeneruje `/clanek/123-jak-upect-chleba`, zatímco `canonicalize()` bude očekávat `/clanek/123-jak-upect-chl` (protože někde někdo použil jiný `truncate`), a aplikace by se přesměrovávala donekonečna. - - -Bonus: krátké URL stále fungují -=============================== - -Protože je slug nepovinný, fungují i adresy bez něj: - -``` -/clanek/123 -``` - -To se hodí pro: -- **QR kódy** - kratší URL znamená méně hustý a lépe skenovatelný kód -- **SMS a chat** - vejde se do tweetu, vypadá úhledně -- **Tištěné materiály** - krátkou URL se rychleji napíše - -Když uživatel takovou URL otevře, `canonicalize()` ho přesměruje 301 na plnou verzi se slugem, takže vyhledávače stejně uvidí jen kanonický tvar. Můžete mít krátkost i SEO zároveň. - - -Shrnutí -======= - -- Maska `<id>[-<slug>]` dělá slug nepovinným. Defaultní `<slug>` nematchuje `/`; `<slug .+>` použijte jen tehdy, když opravdu chcete lomítka ve slugu. -- Obecný `FilterOut` pod klíčem `''` dohledá titulek podle ID - **bez zásahu do šablon kdekoli v aplikaci**. -- Vyhledávání obalte drobnou per-request cache; jeden DB dotaz na unikátní ID stačí. -- Volitelně může per-parametrový `FilterOut` umožnit šablonám titulek předat přímo a vyhledávání přeskočit. -- `$this->canonicalize()` v action přesměruje nekanonické URL na správnou s HTTP 301. -- Vzorec pro slug (`webalize` + `truncate`) žije na jednom místě - změníte ho jednou, projeví se všude. -- Krátké URL jen s ID dál fungují, což se hodí pro QR kódy a SMS. - -Více o filtrech a kanonizaci najdete v dokumentaci [routování |application:routing#obecne-filtry] a [presenterů |application:presenters#kanonizace]. - -{{maintitle: Hezké URL se slugem v Nette: routování přátelské k SEO}} -{{description: Z adresy /clanek/123 udělejte /clanek/123-jak-upect-chleba přímo v routeru, bez zásahu do šablon, a staré adresy přesměrujte na tu správnou.}} diff --git a/best-practices/cs/restore-request.texy b/best-practices/cs/restore-request.texy deleted file mode 100644 index 887e045417..0000000000 --- a/best-practices/cs/restore-request.texy +++ /dev/null @@ -1,64 +0,0 @@ -Jak se vrátit k dřívější stránce? -********************************* - -.[perex] -Co když uživatel vyplňuje formulář a vyprší mu přihlášení? Aby o data nepřišel, před přesměrováním na přihlašovací stránku data uložíme do session. V Nette to je úplná hračka. - -Aktuální požadavek lze uložit do session pomocí metody `storeRequest()`, která vrátí jeho identifikátor v podobě krátkého řetězce. Metoda uloží název aktuálního presenteru, view a jeho parametry. V případě, že byl odeslán i formulář, uloží se také obsah políček (s výjimkou uploadovaných souborů). - -Obnovení požadavku provádí metoda `restoreRequest($key)`, které předáme získaný identifikátor. Ta přesměruje na původní presenter a view. Pokud však uložený požadavek obsahuje odeslání formuláře, na původní presenter přejde metodou `forward()`, formuláři předá dříve vyplněné hodnoty a nechá jej znovu vykreslit. Uživatel tak má možnost formulář opětovně odeslat a žádná data se neztratí. - -Důležité je, že `restoreRequest()` kontroluje, zda je nově přihlášený uživatel tentýž, co formulář původně vyplňoval. Pokud ne, uložený požadavek se neobnoví a metoda nic neudělá, což zvyšuje bezpečnost. - -Ukážeme si vše na příkladu. Mějme presenter `AdminPresenter`, ve kterém se editují data a v jehož metodě `startup()` ověřujeme, zda je uživatel přihlášen. Pokud není, přesměrujeme jej na `SignPresenter`. Zároveň si uložíme aktuální požadavek a jeho klíč odešleme do `SignPresenter`. - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - protected function startup() - { - parent::startup(); - - if (!$this->user->isLoggedIn()) { - $this->redirect('Sign:in', ['backlink' => $this->storeRequest()]); - } - } -} -``` - -Presenter `SignPresenter` bude krom formuláře pro přihlášení obsahovat i persistentní parametr `$backlink`, do kterého se klíč zapíše. Jelikož parametr je persistentní, bude se přenášet i po odeslání přihlašovacího formuláře. - - -```php -use Nette\Application\Attributes\Persistent; - -class SignPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $backlink = ''; - - protected function createComponentSignInForm() - { - $form = new Nette\Application\UI\Form; - // ... přidáme políčka formuláře ... - $form->onSuccess[] = $this->signInFormSucceeded(...); - return $form; - } - - private function signInFormSucceeded($form) - { - // ... tady uživatele přihlásíme ... - - $this->restoreRequest($this->backlink); - $this->redirect('Admin:'); - } -} -``` - -Metodě `restoreRequest()` předáme klíč uloženého požadavku a ona přesměruje (nebo přejde) na původní presenter. - -Pokud je ale klíč neplatný (například už v session neexistuje), metoda neudělá nic. Následuje tedy volání `$this->redirect('Admin:')`, které přesměruje na `AdminPresenter`. - -{{priority: -1}} -{{maintitle: Návrat na předchozí stránku v Nette: obnovení požadavku}} -{{description: Uživateli vyprší přihlášení při vyplňování formuláře? Uložte aktuální požadavek do session, přesměrujte na přihlášení a poté ho obnovte, aniž by přišel o data.}} diff --git a/best-practices/de/@home.texy b/best-practices/de/@home.texy deleted file mode 100644 index e61ec24211..0000000000 --- a/best-practices/de/@home.texy +++ /dev/null @@ -1,69 +0,0 @@ -Anleitungen und Best Practices -****************************** - -.[perex] -Anleitungen, Lösungen für häufige Aufgaben und *Best Practices* für Nette. - - -<div class=documentation> -<div> - - -Nette Application ------------------ -- [Inject-Methoden und -Attribute |inject-method-attribute] -- [Presenter aus Traits zusammensetzen |presenter-traits] -- [Einstellungen an Presenter übergeben |passing-settings-to-presenters] -- [Wie man einen Request wiederherstellt |restore-request] -- [Datenbankergebnisse paginieren |pagination] -- [Dynamische Snippets |dynamic-snippets] -- [Wie man das Attribut #Requires verwendet |attribute-requires] -- [Wie man POST-Links richtig verwendet |post-links] -- [Schöne URLs mit Slugs |pretty-urls] - -</div> -<div> - - -Formulare ---------- -- [Formulare wiederverwenden |form-reuse] -- [Formular zum Erstellen und Bearbeiten von Datensätzen |creating-editing-form] -- [Erstellen wir ein Kontaktformular |lets-create-contact-form] -- [Abhängige Selectboxen |https://blog.nette.org/de/dependent-selectboxes-elegantly-in-nette-and-pure-js] - -</div> -<div> - - -Allgemein ---------- -- [Wie man eine Konfigurationsdatei lädt |bootstrap:] -- [Wie man Microsites schreibt |microsites] -- [Warum verwendet Nette die PascalCase-Notation für Konstanten? |https://blog.nette.org/de/for-less-screaming-in-the-code] -- [Warum verwendet Nette das Suffix Interface nicht? |https://blog.nette.org/de/prefixes-and-suffixes-do-not-belong-in-interface-names] -- [Composer: Tipps zur Verwendung |composer] -- [Einführung in die objektorientierte Programmierung |nette:introduction-to-object-oriented-programming] - -</div> -<div> - - -Beispiellösungen ----------------- -- [Nette examples |https://github.com/nette-examples] -- [Doctrine & Nette |https://contributte.org/nettrine/] -- [Contributte examples |https://contributte.org/examples.html] -- [Doctrine ORM Website |https://github.com/MinecordNetwork/Website] -- [Quick start |quickstart:] - -</div> -<div> - - -Videos ------- -Hunderte Aufzeichnungen von den Last-Saturday-Treffen und Videos über Nette finden Sie gesammelt an einem Ort auf dem "YouTube-Kanal des Nette Frameworks":https://www.youtube.com/user/NetteFramework. - -</div> -</div> diff --git a/best-practices/de/@left-menu.texy b/best-practices/de/@left-menu.texy deleted file mode 100644 index cedf8742b7..0000000000 --- a/best-practices/de/@left-menu.texy +++ /dev/null @@ -1,34 +0,0 @@ -Anleitungen und Best Practices -****************************** -- [Übersicht |@home] - -Nette Application -***************** -- [Inject-Methoden und -Attribute |inject-method-attribute] -- [Presenter aus Traits zusammensetzen |presenter-traits] -- [Einstellungen an Presenter übergeben |passing-settings-to-presenters] -- [Wie man einen Request wiederherstellt |restore-request] -- [Datenbankergebnisse paginieren |pagination] -- [Dynamische Snippets |dynamic-snippets] -- [Wie man das Attribut #Requires verwendet |attribute-requires] -- [Wie man POST-Links richtig verwendet |post-links] -- [Schöne URLs mit Slugs |pretty-urls] - -Formulare -********* -- [Formulare wiederverwenden |form-reuse] -- [Formular zum Erstellen und Bearbeiten von Datensätzen |creating-editing-form] -- [Erstellen wir ein Kontaktformular |lets-create-contact-form] - -Allgemein -********* -- [Wie man Microsites schreibt |microsites] -- [Composer: Tipps zur Verwendung |composer] - - -Weiterführende Lektüre -********************** -- [Nette Dokumentation |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Fehlerbehebung |nette:troubleshooting] diff --git a/best-practices/de/@meta.texy b/best-practices/de/@meta.texy deleted file mode 100644 index 8aa9f9c5b2..0000000000 --- a/best-practices/de/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Anleitungen und Best Practices}} diff --git a/best-practices/de/attribute-requires.texy b/best-practices/de/attribute-requires.texy deleted file mode 100644 index d4f34945a5..0000000000 --- a/best-practices/de/attribute-requires.texy +++ /dev/null @@ -1,177 +0,0 @@ -Wie man das Attribut `#[Requires]` verwendet -******************************************** - -.[perex] -Wenn Sie eine Webanwendung schreiben, stoßen Sie oft auf die Notwendigkeit, den Zugriff auf bestimmte Teile Ihrer Anwendung einzuschränken. Vielleicht wollen Sie, dass manche Requests Daten nur über ein Formular senden können (also mit der Methode POST) oder nur für AJAX-Aufrufe zugänglich sind. Im Nette Framework 3.2 ist ein neues Werkzeug erschienen, mit dem Sie solche Einschränkungen sehr elegant und übersichtlich festlegen können: das Attribut `#[Requires]`. - -Ein Attribut ist eine spezielle Markierung in PHP, die Sie vor die Definition einer Klasse oder Methode setzen. Da es sich im Grunde um eine Klasse handelt, müssen Sie die `use`-Klausel angeben, damit die folgenden Beispiele funktionieren: - -```php -use Nette\Application\Attributes\Requires; -``` - -Das Attribut `#[Requires]` können Sie bei der Presenter-Klasse selbst und außerdem bei diesen Methoden verwenden: - -- `action<Action>()` -- `render<View>()` -- `handle<Signal>()` -- `createComponent<Name>()` - -Die letzten beiden Methoden betreffen auch Komponenten, Sie können das Attribut also auch bei ihnen einsetzen. - -Sind die Bedingungen, die das Attribut angibt, nicht erfüllt, wird ein HTTP-Fehler 4xx ausgelöst. - - -HTTP-Methoden -------------- - -Sie können angeben, welche HTTP-Methoden (wie GET, POST usw.) für den Zugriff erlaubt sind. Wenn Sie zum Beispiel den Zugriff nur über das Absenden eines Formulars erlauben wollen, setzen Sie: - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST')] - public function actionDelete(int $id): void - { - } -} -``` - -Warum Sie für zustandsändernde Aktionen POST statt GET verwenden sollten und wie das geht? [Lesen Sie die Anleitung |post-links]. - -Sie können eine Methode oder ein Array von Methoden angeben. Ein Sonderfall ist der Wert `'*'`, der alle Methoden erlaubt, was Presenter [aus Sicherheitsgründen standardmäßig nicht zulassen |application:presenters#Prüfung der HTTP-Methode]. - - -AJAX-Aufrufe ------------- - -Wenn Sie wollen, dass ein Presenter oder eine Methode nur für AJAX-Requests zugänglich ist, verwenden Sie: - -```php -#[Requires(ajax: true)] -class AjaxPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Gleicher Ursprung ------------------ - -Zur Erhöhung der Sicherheit können Sie verlangen, dass der Request von derselben Domain kommt. Damit verhindern Sie die [CSRF-Sicherheitslücke |nette:vulnerability-protection#Cross-Site Request Forgery (CSRF)]: - -```php -#[Requires(sameOrigin: true)] -class SecurePresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Bei `handle<Signal>()`-Methoden wird der Zugriff von derselben Domain automatisch verlangt. Wenn Sie umgekehrt den Zugriff von jeder beliebigen Domain erlauben wollen, geben Sie an: - -```php -#[Requires(sameOrigin: false)] -public function handleList(): void -{ -} -``` - - -Zugriff über forward --------------------- - -Manchmal ist es nützlich, den Zugriff auf einen Presenter so einzuschränken, dass er nur indirekt verfügbar ist, zum Beispiel über die Methoden `forward()` oder `switch()` aus einem anderen Presenter. So werden etwa Error-Presenter geschützt, damit sie sich nicht über eine URL aufrufen lassen: - -```php -#[Requires(forward: true)] -class ForwardedPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -In der Praxis ist es oft nötig, bestimmte Views zu kennzeichnen, zu denen man erst aufgrund der Logik im Presenter gelangt. Also wiederum so, dass sie sich nicht direkt öffnen lassen: - -```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 - { - } -} -``` - - -Konkrete Aktionen ------------------ - -Sie können außerdem einschränken, dass bestimmter Code, etwa das Erstellen einer Komponente, nur für bestimmte Aktionen im Presenter verfügbar ist: - -```php -class EditDeletePresenter extends Nette\Application\UI\Presenter -{ - #[Requires(actions: ['add', 'edit'])] - public function createComponentPostForm() - { - } -} -``` - -Bei einer einzelnen Aktion muss kein Array geschrieben werden: `#[Requires(actions: 'default')]` - - -Eigene Attribute ----------------- - -Wenn Sie das Attribut `#[Requires]` wiederholt mit denselben Einstellungen verwenden wollen, können Sie sich ein eigenes Attribut erstellen, das `#[Requires]` erbt und nach Ihren Bedürfnissen konfiguriert. - -Zum Beispiel erlaubt `#[SingleAction]` 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]` erlaubt den Zugriff über alle HTTP-Methoden, die für eine REST-API verwendet werden: - -```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 -{ -} -``` - - -Fazit ------ - -Das Attribut `#[Requires]` gibt Ihnen große Flexibilität und Kontrolle darüber, wie Ihre Webseiten zugänglich sind. Mit einfachen, aber mächtigen Regeln können Sie die Sicherheit und das korrekte Funktionieren Ihrer Anwendung verbessern. Wie Sie sehen, kann die Verwendung von Attributen in Nette Ihre Arbeit nicht nur erleichtern, sondern auch absichern. diff --git a/best-practices/de/composer.texy b/best-practices/de/composer.texy deleted file mode 100644 index 3c5fdb9006..0000000000 --- a/best-practices/de/composer.texy +++ /dev/null @@ -1,282 +0,0 @@ -Composer: Tipps zur Verwendung -****************************** - -<div class=perex> - -Composer ist ein Werkzeug zur Verwaltung von Abhängigkeiten in PHP. Es erlaubt Ihnen, die Bibliotheken aufzuzählen, von denen Ihr Projekt abhängt, und installiert und aktualisiert sie für Sie. Wir zeigen Ihnen: - -- wie Sie Composer installieren -- wie Sie ihn in einem neuen oder bestehenden Projekt verwenden - -</div> - - -Installation -============ - -Composer ist eine ausführbare `.phar`-Datei, die Sie folgendermaßen herunterladen und installieren: - - -Windows -------- - -Verwenden Sie den offiziellen Installer [Composer-Setup.exe |https://getcomposer.org/Composer-Setup.exe]. - - -Linux, macOS ------------- - -Es genügen 4 Befehle, die Sie sich von [dieser Seite |https://getcomposer.org/download/] kopieren können. - -Wenn Sie die Datei außerdem in ein Verzeichnis legen, das im System-`PATH` liegt, wird Composer global verfügbar: - -```shell -$ mv ./composer.phar ~/bin/composer # oder /usr/local/bin/composer -``` - - -Verwendung im Projekt -===================== - -Um Composer in Ihrem Projekt zu verwenden, brauchen Sie nur eine Datei `composer.json`. Sie beschreibt die Abhängigkeiten Ihres Projekts und kann außerdem weitere Metadaten enthalten. Die einfachste `composer.json` kann so aussehen: - -```js -{ - "require": { - "nette/database": "^3.0" - } -} -``` - -Wir sagen hier, dass unsere Anwendung (oder Bibliothek) das Paket `nette/database` benötigt (der Paketname besteht aus dem Namen der Organisation und dem Namen des Projekts) und eine Version verlangt, die der Bedingung `^3.0` entspricht (also die neueste Version 3). - -Wir haben also im Wurzelverzeichnis des Projekts die Datei `composer.json` und starten: - -```shell -composer update -``` - -Composer lädt Nette Database in das Verzeichnis `vendor/` herunter. Außerdem erzeugt er die Datei `composer.lock`, die Informationen darüber enthält, welche Bibliotheksversionen genau installiert wurden. - -Composer erzeugt die Datei `vendor/autoload.php`. Diese Datei können Sie einfach einbinden und die Klassen der Bibliotheken ohne jede weitere Arbeit verwenden: - -```php -require __DIR__ . '/vendor/autoload.php'; - -$db = new Nette\Database\Connection('sqlite::memory:'); -``` - - -Aktualisierung der Pakete auf die neuesten Versionen -==================================================== - -Für die Aktualisierung der verwendeten Bibliotheken auf die neuesten Versionen gemäß den in `composer.json` definierten Bedingungen ist der Befehl `composer update` zuständig. Bei der Abhängigkeit `"nette/database": "^3.0"` installiert er zum Beispiel die neueste Version 3.x.x, aber nicht mehr Version 4. - -Um die Bedingungen in der Datei `composer.json` zu aktualisieren, etwa auf `"nette/database": "^4.1"`, damit die neueste Version installiert werden kann, verwenden Sie den Befehl `composer require nette/database`. - -Um alle verwendeten Nette-Pakete zu aktualisieren, müssten Sie sie alle auf der Kommandozeile aufzählen, z. B.: - -```shell -composer require nette/application nette/forms latte/latte tracy/tracy ... -``` - -Das ist unpraktisch. Verwenden Sie deshalb das einfache Skript "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff, das das für Sie erledigt: - -```shell -php composer-frontline.php -``` - - -Erstellung eines neuen Projekts -=============================== - -Ein neues Nette-Projekt erstellen Sie mit einem einzigen Befehl: - -```shell -composer create-project nette/web-project projektname -``` - -Setzen Sie für `projektname` den Verzeichnisnamen Ihres Projekts ein und führen Sie den Befehl aus. Composer lädt das Repository `nette/web-project` von GitHub herunter, das bereits eine Datei `composer.json` enthält, und danach gleich das Nette Framework selbst. Es sollte nur noch nötig sein, die [Verzeichnisrechte zu setzen |nette:troubleshooting#Einstellung der Verzeichnisberechtigungen] für die Verzeichnisse `temp/` und `log/`, und das Projekt sollte laufen. - -Wenn Sie wissen, auf welcher PHP-Version Ihr Projekt gehostet wird, [stellen Sie sie unbedingt ein |#PHP-Version]. - - -PHP-Version -=========== - -Composer installiert immer diejenigen Paketversionen, die mit der PHP-Version kompatibel sind, die Sie gerade verwenden (genauer gesagt mit der PHP-Version, die beim Ausführen von Composer auf der Kommandozeile benutzt wird). Das ist aber vermutlich nicht dieselbe Version, die Ihr Hosting verwendet. Deshalb ist es sehr wichtig, in die Datei `composer.json` die Information über die PHP-Version auf dem Hosting einzutragen. Danach werden nur noch Paketversionen installiert, die mit dem Hosting kompatibel sind. - -Dass das Projekt zum Beispiel auf PHP 8.2.3 läuft, stellen wir mit diesem Befehl ein: - -```shell -composer config platform.php 8.2.3 -``` - -So wird die Version in die Datei `composer.json` geschrieben: - -```js -{ - "config": { - "platform": { - "php": "8.2.3" - } - } -} -``` - -Die PHP-Versionsnummer wird allerdings noch an einer anderen Stelle der Datei angegeben, nämlich im Abschnitt `require`. Während die erste Zahl bestimmt, für welche Version die Pakete installiert werden, sagt die zweite Zahl, für welche Version die Anwendung selbst geschrieben ist. Danach richtet zum Beispiel PhpStorm das *PHP language level* ein. (Natürlich ergibt es keinen Sinn, dass sich diese Versionen unterscheiden, die doppelte Angabe ist also ein Versäumnis.) Diese Version stellen Sie mit folgendem Befehl ein: - -```shell -composer require php 8.2.3 --no-update -``` - -Oder direkt in der Datei `composer.json`: - -```js -{ - "require": { - "php": "8.2.3" - } -} -``` - - -Ignorieren der PHP-Version -========================== - -Pakete geben in der Regel sowohl die niedrigste PHP-Version an, mit der sie kompatibel sind, als auch die höchste, mit der sie getestet wurden. Wenn Sie eine noch neuere PHP-Version verwenden wollen, etwa zu Testzwecken, wird Composer die Installation eines solchen Pakets verweigern. Die Lösung ist die Option `--ignore-platform-req=php+`, die dafür sorgt, dass Composer die oberen Grenzen der geforderten PHP-Version ignoriert. - - -Falsche Meldungen -================= - -Beim Upgrade von Paketen oder beim Ändern von Versionsnummern kommt es manchmal zu Konflikten. Ein Paket hat Anforderungen, die im Widerspruch zu einem anderen stehen, und so weiter. Composer gibt aber gelegentlich falsche Meldungen aus. Er meldet einen Konflikt, der in Wirklichkeit gar nicht existiert. In solchen Fällen hilft es, die Datei `composer.lock` zu löschen und es erneut zu versuchen. - -Wenn die Fehlermeldung bestehen bleibt, ist sie ernst gemeint, und Sie müssen ihr entnehmen, was und wie zu ändern ist. - - -Packagist.org - zentrales Repository -==================================== - -[Packagist |https://packagist.org] ist das Haupt-Repository, in dem Composer standardmäßig nach Paketen sucht. Sie können hier auch eigene Pakete veröffentlichen. - - -Was, wenn wir kein zentrales Repository verwenden möchten? ----------------------------------------------------------- - -Wenn wir firmeninterne Anwendungen oder Bibliotheken haben, die wir schlicht nicht öffentlich hosten können, legen wir uns für sie eigene Repositories an. - -Mehr zum Thema Repositories finden Sie in [der offiziellen Dokumentation |https://getcomposer.org/doc/05-repositories.md#repositories]. - - -Autoloading -=========== - -Eine wesentliche Eigenschaft von Composer ist, dass er Autoloading für alle von ihm installierten Klassen bereitstellt. Sie starten es, indem Sie die Datei `vendor/autoload.php` einbinden. - -Sie können Composer aber auch zum Laden weiterer Klassen außerhalb des Verzeichnisses `vendor/` verwenden. Die erste Möglichkeit besteht darin, Composer definierte Verzeichnisse und Unterverzeichnisse durchsuchen, alle Klassen finden und in den Autoloader aufnehmen zu lassen. Das erreichen Sie mit der Einstellung `autoload > classmap` in `composer.json`: - -```js -{ - "autoload": { - "classmap": [ - "src/", # schließt das Verzeichnis src/ und seine Unterverzeichnisse ein - ] - } -} -``` - -Anschließend müssen Sie nach jeder Änderung den Befehl `composer dumpautoload` ausführen und die Autoloading-Tabellen neu erzeugen lassen. Das ist außerordentlich unbequem. Weitaus besser ist es, diese Aufgabe dem [RobotLoader |robot-loader:] anzuvertrauen, der dieselbe Tätigkeit automatisch im Hintergrund und viel schneller erledigt. - -Die zweite Möglichkeit ist, sich an [PSR-4 |https://www.php-fig.org/psr/psr-4/] zu halten. Vereinfacht gesagt handelt es sich um ein System, bei dem Namespaces und Klassennamen der Verzeichnisstruktur und den Dateinamen entsprechen, also z. B. `App\Core\RouterFactory` liegt in der Datei `/path/to/App/Core/RouterFactory.php`. Beispielkonfiguration: - -```js -{ - "autoload": { - "psr-4": { - "App\\": "app/" # der Namespace App\ liegt im Verzeichnis app/ - } - } -} -``` - -Wie genau Sie dieses Verhalten konfigurieren, erfahren Sie in der [Composer-Dokumentation |https://getcomposer.org/doc/04-schema.md#psr-4]. - - -Testen neuer Versionen -====================== - -Sie wollen eine neue Entwicklungsversion eines Pakets testen? So geht es. Fügen Sie zuerst dieses Optionspaar in Ihre Datei `composer.json` ein. Es erlaubt die Installation von Entwicklungsversionen, Composer greift aber nur dann darauf zurück, wenn keine Kombination stabiler Versionen die Anforderungen erfüllt: - -```js -{ - "minimum-stability": "dev", - "prefer-stable": true, -} -``` - -Außerdem empfehlen wir, die Datei `composer.lock` zu löschen, denn manchmal verweigert Composer die Installation auf unerklärliche Weise, und das löst das Problem. - -Nehmen wir an, es handelt sich um das Paket `nette/utils` und die neue Version trägt die Nummer 4.0. Sie installieren sie mit dem Befehl: - -```shell -composer require nette/utils:4.0.x-dev -``` - -Oder Sie können eine konkrete Version installieren, zum Beispiel 4.0.0-RC2: - -```shell -composer require nette/utils:4.0.0-RC2 -``` - -Wenn aber ein anderes Paket von der Bibliothek abhängt und auf eine ältere Version festgelegt ist (z. B. `^3.1`), ist die ideale Lösung, dieses Paket zu aktualisieren, damit es mit der neuen Version funktioniert. Wenn Sie die Einschränkung jedoch nur umgehen und Composer zwingen wollen, die Entwicklungsversion zu installieren und vorzugeben, es handle sich um eine ältere Version (z. B. 3.1.6), können Sie das Schlüsselwort `as` verwenden: - -```shell -composer require nette/utils "4.0.x-dev as 3.1.6" -``` - - -Aufruf von Befehlen -=================== - -Über Composer lassen sich eigene, vorbereitete Befehle und Skripte aufrufen, als wären es native Composer-Befehle. Bei Skripten, die im Verzeichnis `vendor/bin` liegen, müssen Sie diesen Pfad nicht angeben. - -Als Beispiel definieren wir in der Datei `composer.json` ein Skript, das mit [Nette Tester |tester:] die Tests startet: - -```js -{ - "scripts": { - "tester": "tester tests -s" - } -} -``` - -Die Tests starten wir dann mit `composer tester`. Den Befehl können Sie auch dann aufrufen, wenn Sie sich nicht im Wurzelverzeichnis des Projekts befinden, sondern in einem seiner Unterverzeichnisse. - - -Senden Sie ein Dankeschön -========================= - -Wir zeigen Ihnen einen Trick, mit dem Sie Open-Source-Autoren eine Freude machen. Sie geben auf einfache Weise den Bibliotheken, die Ihr Projekt verwendet, einen Stern auf GitHub. Es genügt, die Bibliothek `symfony/thanks` zu installieren: - -```shell -composer global require symfony/thanks -``` - -Und dann auszuführen: - -```shell -composer thanks -``` - -Probieren Sie es aus! - - -Konfiguration -============= - -Composer ist eng mit dem Versionierungswerkzeug [Git |https://git-scm.com] verbunden. Wenn Sie es nicht installiert haben, müssen Sie Composer mitteilen, dass er es nicht verwenden soll: - -```shell -composer -g config preferred-install dist -``` diff --git a/best-practices/de/creating-editing-form.texy b/best-practices/de/creating-editing-form.texy deleted file mode 100644 index b91ee3eec1..0000000000 --- a/best-practices/de/creating-editing-form.texy +++ /dev/null @@ -1,204 +0,0 @@ -Formular zum Erstellen und Bearbeiten von Datensätzen -***************************************************** - -.[perex] -Wie setzt man in Nette das Hinzufügen und Bearbeiten eines Datensatzes richtig um, wenn man für beides dasselbe Formular verwenden möchte? - -In vielen Fällen sind die Formulare zum Hinzufügen und zum Bearbeiten eines Datensatzes identisch und unterscheiden sich vielleicht nur in der Beschriftung der Schaltfläche. Wir zeigen Beispiele einfacher Presenter, in denen wir das Formular zuerst zum Hinzufügen eines Datensatzes verwenden, dann zum Bearbeiten und schließlich beide Lösungen zusammenführen. - - -Hinzufügen eines Datensatzes ----------------------------- - -Beispiel eines Presenters zum Hinzufügen eines Datensatzes. Die eigentliche Arbeit mit der Datenbank überlassen wir der Klasse `Facade`, deren Code für dieses Beispiel nicht wesentlich ist. - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentRecordForm(): Form - { - $form = new Form; - - // ... wir fügen die Formularfelder hinzu ... - - $form->onSuccess[] = $this->recordFormSucceeded(...); - return $form; - } - - private function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // Datensatz zur Datenbank hinzufügen - $this->flashMessage('Erfolgreich hinzugefügt'); - $this->redirect('...'); - } - - public function renderAdd(): void - { - // ... - } -} -``` - - -Bearbeiten eines Datensatzes ----------------------------- - -Sehen wir uns nun an, wie ein Presenter zum Bearbeiten eines Datensatzes aussehen würde: - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - private $record; - - public function __construct( - private Facade $facade, - ) { - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // Existenz des Datensatzes prüfen - || !$this->facade->isEditAllowed(/*...*/) // Berechtigungen prüfen - ) { - $this->error(); // Fehler 404 - } - - $this->record = $record; - } - - protected function createComponentRecordForm(): Form - { - // wir prüfen, dass die Aktion 'edit' ist - if ($this->getAction() !== 'edit') { - $this->error(); - } - - $form = new Form; - - // ... wir fügen die Formularfelder hinzu ... - - $form->setDefaults($this->record); // Standardwerte setzen - $form->onSuccess[] = $this->recordFormSucceeded(...); - return $form; - } - - private function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->update($this->record->id, $data); // Datensatz aktualisieren - $this->flashMessage('Erfolgreich aktualisiert'); - $this->redirect('...'); - } -} -``` - -In der *action*-Methode, die gleich zu Beginn des [Lebenszyklus des Presenters |application:presenters#Lebenszyklus des Presenters] ausgeführt wird, prüfen wir die Existenz des Datensatzes und die Berechtigung des Benutzers, ihn zu bearbeiten. - -Den Datensatz speichern wir in der Property `$record`, damit er in der Methode `createComponentRecordForm()` zum Setzen der Standardwerte und in `recordFormSucceeded()` wegen der ID zur Verfügung steht. Eine alternative Lösung wäre, die Standardwerte direkt in `actionEdit()` zu setzen und den Wert der ID, der Teil der URL ist, mit `getParameter('id')` zu ermitteln: - - -```php - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - // Existenz prüfen und Berechtigungen kontrollieren - ) { - $this->error(); - } - - // Standardwerte des Formulars setzen - $this->getComponent('recordForm') - ->setDefaults($record); - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); - // ... - } -``` - -Allerdings müssen wir uns, und das sollte **die wichtigste Erkenntnis aus dem gesamten Code** sein, beim Erstellen des Formulars vergewissern, dass die Aktion tatsächlich `edit` ist. Denn sonst würde die Prüfung in der Methode `actionEdit()` überhaupt nicht stattfinden! - - -Dasselbe Formular zum Hinzufügen und Bearbeiten ------------------------------------------------ - -Und nun führen wir beide Presenter zu einem zusammen. Entweder könnten wir in der Methode `createComponentRecordForm()` unterscheiden, um welche Aktion es sich handelt, und das Formular entsprechend konfigurieren, oder wir überlassen das direkt den action-Methoden und werden die Bedingung los: - - -```php -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - public function actionAdd(): void - { - $form = $this->getComponent('recordForm'); - $form->onSuccess[] = $this->addingFormSucceeded(...); - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // Existenz des Datensatzes prüfen - || !$this->facade->isEditAllowed(/*...*/) // Berechtigungen prüfen - ) { - $this->error(); // Fehler 404 - } - - $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // Standardwerte setzen - $form->onSuccess[] = $this->editingFormSucceeded(...); - } - - protected function createComponentRecordForm(): Form - { - // wir prüfen, dass die Aktion 'add' oder 'edit' ist - if (!in_array($this->getAction(), ['add', 'edit'])) { - $this->error(); - } - - $form = new Form; - - // ... wir fügen die Formularfelder hinzu ... - - return $form; - } - - private function addingFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // Datensatz zur Datenbank hinzufügen - $this->flashMessage('Erfolgreich hinzugefügt'); - $this->redirect('...'); - } - - private function editingFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // Datensatz aktualisieren - $this->flashMessage('Erfolgreich aktualisiert'); - $this->redirect('...'); - } -} -``` - -{{priority: -1}} diff --git a/best-practices/de/dynamic-snippets.texy b/best-practices/de/dynamic-snippets.texy deleted file mode 100644 index ba9c59e815..0000000000 --- a/best-practices/de/dynamic-snippets.texy +++ /dev/null @@ -1,176 +0,0 @@ -Dynamische Snippets -******************* - -.[perex] -Wie Sie mit AJAX nur die Teile einer Seite aktualisieren, die sich wirklich ändern, etwa einzelne Einträge in einer Liste, und zwar mithilfe der dynamischen Snippets in Latte. - -Bei der Anwendungsentwicklung entsteht recht häufig der Bedarf, AJAX-Operationen zum Beispiel über einzelne Tabellenzeilen oder Listeneinträge auszuführen. Als Beispiel nehmen wir eine Artikelliste, bei der wir angemeldeten Benutzern erlauben, jeden Artikel mit "gefällt mir" oder "gefällt mir nicht" zu bewerten. Der Code des Presenters und das zugehörige Template ohne AJAX sehen ungefähr so aus (ich zeige die wichtigsten Ausschnitte; der Code setzt einen Service für das Speichern der Bewertungen und das Laden der Artikel voraus - die konkrete Implementierung ist für diese Anleitung nicht wichtig): - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - $this->redirect('this'); -} - -public function handleUnlike(int $articleId): void -{ - $this->ratingService->removeLike($articleId, $this->user->id); - $this->redirect('this'); -} -``` - -Template: - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {if !$article->liked} - <a n:href="like!, $article->id" class=ajax>gefällt mir</a> - {else} - <a n:href="unlike!, $article->id" class=ajax>gefällt mir nicht mehr</a> - {/if} -</article> -``` - - -Ajaxifizierung -============== - -Statten wir diese einfache Anwendung nun mit AJAX aus. Die Änderung der Bewertung eines Artikels ist nicht so wichtig, dass dafür eine Weiterleitung nötig wäre, deshalb sollte sie idealerweise per AJAX im Hintergrund ablaufen. Wir verwenden das [Handler-Skript aus den Addons |application:ajax#Naja] mit der üblichen Konvention, dass AJAX-Links die CSS-Klasse `ajax` tragen. - -Aber wie geht das konkret? Nette bietet zwei Wege: den Weg der dynamischen Snippets und den Weg der Komponenten. Beide haben ihr Für und Wider, deshalb zeigen wir sie einen nach dem anderen. - - -Der Weg der dynamischen Snippets -================================ - -Ein dynamisches Snippet bezeichnet in der Terminologie von Latte den speziellen Fall des Tags `{snippet}`, bei dem im Namen des Snippets eine Variable verwendet wird. Ein solches Snippet darf im Template nicht einfach irgendwo stehen - es muss von einem statischen, also gewöhnlichen Snippet umschlossen sein oder sich innerhalb eines `{snippetArea}` befinden. Unser Template könnten wir folgendermaßen anpassen: - - -```latte -{snippet articlesContainer} - <article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {snippet article-{$article->id}} - {if !$article->liked} - <a n:href="like!, $article->id" class=ajax>gefällt mir</a> - {else} - <a n:href="unlike!, $article->id" class=ajax>gefällt mir nicht mehr</a> - {/if} - {/snippet} - </article> -{/snippet} -``` - -Jeder Artikel definiert nun ein Snippet, das die ID des Artikels im Namen trägt. Alle diese dynamischen Snippets sind dann gemeinsam von einem statischen Snippet mit dem Namen `articlesContainer` umschlossen. Würden wir dieses umschließende Snippet weglassen, würde Latte eine Exception werfen. - -Es bleibt nur noch, im Presenter das Neuzeichnen zu ergänzen - es genügt, die statische Hülle neu zu zeichnen. - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - if ($this->isAjax()) { - $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- nicht nötig - } else { - $this->redirect('this'); - } -} -``` - -Passen Sie die zugehörige Methode `handleUnlike()` auf dieselbe Weise an, und AJAX funktioniert! - -Diese Lösung hat allerdings einen Schönheitsfehler. Wenn wir uns den AJAX-Request genauer ansehen, stellen wir fest, dass die Anwendung nach außen zwar sparsam wirkt (sie gibt nur ein einziges Snippet für den betreffenden Artikel zurück), auf dem Server aber in Wirklichkeit *alle* Snippets gerendert hat. Das gewünschte Snippet legt sie in den Payload, die übrigen wirft sie weg (sie hat sie also völlig unnötig auch aus der Datenbank geholt und gerendert). - -Um diesen Vorgang zu optimieren, müssen wir dort eingreifen, wo wir die Sammlung `$articles` an das Template übergeben (sagen wir in der Methode `renderDefault()`). Wir nutzen dabei die Tatsache, dass die Verarbeitung der Signale vor den `render<Something>`-Methoden abläuft: - -```php -public function handleLike(int $articleId): void -{ - // ... - if ($this->isAjax()) { - // ... - $this->template->articles = [ - $this->db->table('articles')->get($articleId), - ]; - } else { - // ... -} - -public function renderDefault(): void -{ - if (!isset($this->template->articles)) { - $this->template->articles = $this->db->table('articles'); - } -} -``` - -Bei der Verarbeitung des Signals wird nun statt der Sammlung aller Artikel nur noch ein Array mit dem einen relevanten Artikel an das Template übergeben - dem, den wir rendern und im Payload an den Browser senden wollen. Die `{foreach}`-Schleife läuft also nur einmal, und es werden keine überflüssigen Snippets gerendert. - - -Der Weg der Komponenten -======================= - -Ein völlig anderer Lösungsweg vermeidet dynamische Snippets ganz. Der Trick besteht darin, die gesamte Logik in eine eigene Komponente zu verlagern - um das Bewerten kümmert sich von nun an nicht mehr der Presenter, sondern ein dafür vorgesehenes `LikeControl`. Die Klasse sieht folgendermaßen aus (außerdem enthält sie noch Methoden wie `render`, `handleUnlike` usw.): - -```php -class LikeControl extends Nette\Application\UI\Control -{ - public function __construct( - private Article $article, - ) { - } - - public function handleLike(): void - { - $this->ratingService->saveLike($this->article->id, $this->presenter->user->id); - if ($this->presenter->isAjax()) { - $this->redrawControl(); - } else { - $this->presenter->redirect('this'); - } - } -} -``` - -Template der Komponente: - -```latte -{snippet} - {if !$article->liked} - <a n:href="like!" class=ajax>gefällt mir</a> - {else} - <a n:href="unlike!" class=ajax>gefällt mir nicht mehr</a> - {/if} -{/snippet} -``` - -Natürlich ändert sich das Template des Views, und im Presenter müssen wir eine Factory ergänzen. Da wir von dieser Komponente für jeden aus der Datenbank geladenen Artikel eine Instanz erzeugen, nutzen wir zu ihrer "Vervielfältigung" die Klasse [Multiplier |application:Multiplier]. - -```php -protected function createComponentLikeControl() -{ - $articles = $this->db->table('articles'); - return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { - return new LikeControl($articles[$articleId]); - }); -} -``` - -Das Template des Views schrumpft auf das nötige Minimum (und ist völlig frei von Snippets!): - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {control "likeControl-$article->id"} -</article> -``` - -Wir sind fast fertig: Die Anwendung funktioniert nun mit AJAX. Auch hier ist eine Optimierung nötig, denn wegen der Verwendung von Nette Database werden bei der Verarbeitung des Signals unnötig alle Artikel aus der Datenbank geladen statt nur des einen relevanten. Der Vorteil ist jedoch, dass nichts unnötig gerendert wird, weil wirklich nur unsere Komponenteninstanz gerendert wird. - -{{priority: -1}} diff --git a/best-practices/de/form-reuse.texy b/best-practices/de/form-reuse.texy deleted file mode 100644 index bbe84b7eff..0000000000 --- a/best-practices/de/form-reuse.texy +++ /dev/null @@ -1,348 +0,0 @@ -Wiederverwendung von Formularen an mehreren Stellen -*************************************************** - -.[perex] -Nette bietet Ihnen mehrere Möglichkeiten, dasselbe Formular an mehreren Stellen zu verwenden, ohne Code zu duplizieren. In diesem Artikel stellen wir verschiedene Lösungen vor, einschließlich derer, die Sie vermeiden sollten. - - -Formular-Factory -================ - -Ein grundlegender Ansatz zur Wiederverwendung einer Komponente an mehreren Stellen besteht darin, eine Methode oder Klasse zu erstellen, die diese Komponente erzeugt, und diese Methode dann an verschiedenen Stellen der Anwendung aufzurufen. Eine solche Methode oder Klasse nennt man *Factory*. Verwechseln Sie sie bitte nicht mit dem Entwurfsmuster *Factory Method*, das eine bestimmte Art der Verwendung von Factories beschreibt und mit diesem Thema nicht zusammenhängt. - -Als Beispiel erstellen wir eine Factory, die ein Bearbeitungsformular zusammensetzt: - -```php -use Nette\Application\UI\Form; - -class FormFactory -{ - public function createEditForm(): Form - { - $form = new Form; - $form->addText('title', 'Titel:'); - // hier werden weitere Formularfelder hinzugefügt - $form->addSubmit('send', 'Speichern'); - return $form; - } -} -``` - -Nun können Sie diese Factory an verschiedenen Stellen Ihrer Anwendung verwenden, zum Beispiel in Presentern oder Komponenten. Und zwar so, dass Sie sie [als Abhängigkeit anfordern |dependency-injection:passing-dependencies]. Zuerst tragen wir die Klasse also in die Konfigurationsdatei ein: - -```neon -services: - - FormFactory -``` - -Und dann verwenden wir sie im Presenter: - - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->createEditForm(); - $form->onSuccess[] = function () { - // Verarbeitung der übermittelten Daten - }; - return $form; - } -} -``` - -Sie können die Formular-Factory um weitere Methoden zum Erstellen anderer Formulartypen erweitern, ganz wie es Ihre Anwendung erfordert. Und natürlich können wir auch eine Methode hinzufügen, die ein einfaches Formular ohne Elemente erstellt und die dann die übrigen Methoden nutzen: - -```php -class FormFactory -{ - public function createForm(): Form - { - $form = new Form; - return $form; - } - - public function createEditForm(): Form - { - $form = $this->createForm(); - $form->addText('title', 'Titel:'); - // hier werden weitere Formularfelder hinzugefügt - $form->addSubmit('send', 'Speichern'); - return $form; - } -} -``` - -Die Methode `createForm()` tut noch nichts Nützliches, aber das ändert sich gleich. - - -Abhängigkeiten der Factory -========================== - -Mit der Zeit stellt sich heraus, dass die Formulare mehrsprachig sein müssen. Das bedeutet, dass wir für alle Formulare einen [Translator |forms:rendering#Übersetzen] setzen müssen. Zu diesem Zweck passen wir die Klasse `FormFactory` so an, dass sie das Objekt `Translator` als Abhängigkeit im Konstruktor entgegennimmt und an das erzeugte Formular übergibt: - -```php -use Nette\Localization\Translator; - -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function createForm(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } - - // ... -} -``` - -Da die Methode `createForm()` auch von den anderen Methoden aufgerufen wird, die spezifische Formulare erzeugen, genügt es, den Translator nur hier zu setzen. Und damit sind wir fertig. Es ist nicht nötig, den Code irgendeines Presenters oder einer Komponente zu ändern, was ausgezeichnet ist. - - -Mehrere Factory-Klassen -======================= - -Alternativ können Sie für jedes Formular, das Sie in Ihrer Anwendung verwenden möchten, eine eigene Factory-Klasse erstellen. Dieser Ansatz kann die Lesbarkeit des Codes erhöhen und die Verwaltung der Formulare erleichtern. Die ursprüngliche `FormFactory` lassen wir nur ein einfaches Formular mit der Grundkonfiguration erzeugen (zum Beispiel mit Übersetzungsunterstützung), und für das Bearbeitungsformular erstellen wir eine neue Factory `EditFormFactory`. - -```php -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function create(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } -} - - -// ✅ Verwendung von Komposition -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - // hier werden weitere Formularfelder hinzugefügt - $form->addSubmit('send', 'Speichern'); - return $form; - } -} -``` - -Sehr wichtig ist, dass die Beziehung zwischen den Klassen `FormFactory` und `EditFormFactory` durch [Komposition |nette:introduction-to-object-oriented-programming#Komposition] realisiert wird und nicht durch [objektorientierte Vererbung |nette:introduction-to-object-oriented-programming#Vererbung]: - -```php -// ⛔ NEIN! HIER GEHÖRT KEINE VERERBUNG HIN -class EditFormFactory extends FormFactory -{ - public function create(): Form - { - $form = parent::create(); - $form->addText('title', 'Titel:'); - // hier werden weitere Formularfelder hinzugefügt - $form->addSubmit('send', 'Speichern'); - return $form; - } -} -``` - -Vererbung wäre in diesem Fall völlig kontraproduktiv. Auf Probleme würden Sie sehr schnell stoßen. Etwa in dem Moment, in dem Sie der Methode `create()` Parameter hinzufügen wollten; PHP würde einen Fehler melden, weil sich ihre Signatur von der der Elternklasse unterscheidet. Oder beim Übergeben von Abhängigkeiten an die Klasse `EditFormFactory` über den Konstruktor. Es entstünde eine Situation, die wir [Constructor Hell |dependency-injection:passing-dependencies#Constructor Hell] nennen. - -Generell ist es besser, [Komposition der Vererbung vorzuziehen |dependency-injection:faq#Warum ist Komposition der Vererbung vorzuziehen?]. - - -Formularverarbeitung -==================== - -Auch die Formularverarbeitung, die nach dem erfolgreichen Absenden aufgerufen wird, kann Teil der Factory-Klasse sein. Sie funktioniert so, dass sie die übermittelten Daten an die Modellschicht zur Verarbeitung übergibt. Eventuelle Fehler gibt sie [an das Formular zurück |forms:validation#Fehler verarbeiten]. Das Modell wird im folgenden Beispiel durch die Klasse `Facade` repräsentiert: - -```php -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - private Facade $facade, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - $form->addText('title', 'Titel:'); - // hier werden weitere Formularfelder hinzugefügt - $form->addSubmit('send', 'Speichern'); - $form->onSuccess[] = $this->processForm(...); - return $form; - } - - private function processForm(Form $form, array $data): void - { - try { - // Verarbeitung der übermittelten Daten - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - } - } -} -``` - -Die eigentliche Weiterleitung überlassen wir jedoch dem Presenter. Er fügt dem Event `onSuccess` einen weiteren Handler hinzu, der die Weiterleitung durchführt. Dadurch lässt sich das Formular in verschiedenen Presentern verwenden und in jedem woandershin weiterleiten. - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditFormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->create(); - $form->onSuccess[] = function () { - $this->flashMessage('Der Datensatz wurde gespeichert'); - $this->redirect('Homepage:'); - }; - return $form; - } -} -``` - -Diese Lösung nutzt die Eigenschaft von Formularen aus, dass nach einem Aufruf von `addError()` auf dem Formular oder einem seiner Elemente die weiteren `onSuccess`-Handler nicht mehr aufgerufen werden. - - -Vererbung von der Klasse Form -============================= - -Ein zusammengesetztes Formular soll kein Nachkomme der Klasse `Form` sein. Mit anderen Worten: Verwenden Sie diese Lösung nicht: - -```php -// ⛔ NEIN! HIER GEHÖRT KEINE VERERBUNG HIN -class EditForm extends Form -{ - public function __construct(Translator $translator) - { - parent::__construct(); - $this->addText('title', 'Titel:'); - // hier werden weitere Formularfelder hinzugefügt - $this->addSubmit('send', 'Speichern'); - $this->setTranslator($translator); - } -} -``` - -Statt das Formular im Konstruktor zusammenzusetzen, verwenden Sie eine Factory. - -Man muss sich bewusst machen, dass die Klasse `Form` in erster Linie ein Werkzeug zum Zusammensetzen von Formularen ist, also ein *Form Builder*. Und das zusammengesetzte Formular lässt sich als ihr Produkt verstehen. Ein Produkt ist aber kein Spezialfall eines Builders; zwischen ihnen besteht keine *is a*-Beziehung, die die Grundlage der Vererbung bildet. - - -Komponente mit Formular -======================= - -Einen völlig anderen Ansatz stellt die Erstellung einer [Komponente |application:components] dar, die das Formular enthält. Das eröffnet neue Möglichkeiten, zum Beispiel das Formular auf eine bestimmte Art zu rendern, denn zur Komponente gehört auch ein eigenes Template. Oder man kann Signale für die AJAX-Kommunikation und das Nachladen von Informationen in das Formular nutzen, etwa für Vorschläge usw. - - -```php -use Nette\Application\UI\Form; - -class EditControl extends Nette\Application\UI\Control -{ - public array $onSave = []; - - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentForm(): Form - { - $form = new Form; - $form->addText('title', 'Titel:'); - // hier werden weitere Formularfelder hinzugefügt - $form->addSubmit('send', 'Speichern'); - $form->onSuccess[] = $this->processForm(...); - - return $form; - } - - private function processForm(Form $form, array $data): void - { - try { - // Verarbeitung der übermittelten Daten - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - return; - } - - // Auslösen des Events - $this->onSave($this, $data); - } -} -``` - -Nun erstellen wir noch eine Factory, die diese Komponente erzeugt. Es genügt, [ihr Interface zu definieren |application:components#Komponenten mit Abhängigkeiten]: - -```php -interface EditControlFactory -{ - function create(): EditControl; -} -``` - -Und es der Konfigurationsdatei hinzuzufügen: - -```neon -services: - - EditControlFactory -``` - -Und nun können wir die Factory anfordern und im Presenter verwenden: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditControlFactory $controlFactory, - ) { - } - - protected function createComponentEditForm(): EditControl - { - $control = $this->controlFactory->create(); - - $control->onSave[] = function (EditControl $control, $data) { - $this->redirect('this'); - // oder wir leiten zum Ergebnis der Bearbeitung weiter, z. B.: - // $this->redirect('detail', ['id' => $data->id]); - }; - - return $control; - } -} -``` diff --git a/best-practices/de/inject-method-attribute.texy b/best-practices/de/inject-method-attribute.texy deleted file mode 100644 index 7e60e09447..0000000000 --- a/best-practices/de/inject-method-attribute.texy +++ /dev/null @@ -1,61 +0,0 @@ -Inject-Methoden und -Attribute -****************************** - -.[perex] -Dieser Artikel widmet sich den verschiedenen Wegen, Abhängigkeiten an Presenter im Nette Framework zu übergeben. Wir vergleichen den bevorzugten Weg, den Konstruktor, mit weiteren Möglichkeiten wie den `inject`-Methoden und -Attributen. - -Auch für Presenter gilt, dass die Übergabe von Abhängigkeiten über den [Konstruktor |dependency-injection:passing-dependencies#Übergabe im Konstruktor] der bevorzugte Weg ist. Wenn Sie aber einen gemeinsamen Vorfahren erstellen, von dem die übrigen Presenter erben (z. B. `BasePresenter`), und dieser Vorfahre ebenfalls Abhängigkeiten hat, entsteht ein Problem, das wir [Constructor Hell |dependency-injection:passing-dependencies#Constructor Hell] nennen. Umgehen lässt es sich mit alternativen Wegen, nämlich den Inject-Methoden und -Attributen (früher Annotationen). - - -`inject*()`-Methoden -==================== - -Dabei handelt es sich um eine Form der Abhängigkeitsübergabe per [Setter |dependency-injection:passing-dependencies#Setter Injection]. Die Namen dieser Setter beginnen mit dem Präfix `inject`. So benannte Methoden ruft Nette DI automatisch unmittelbar nach dem Erzeugen der Presenter-Instanz auf und übergibt ihnen alle geforderten Abhängigkeiten. Sie müssen deshalb als public deklariert sein. - -Die `inject*()`-Methoden lassen sich als eine Art Erweiterung des Konstruktors auf mehrere Methoden verstehen. Dadurch kann der `BasePresenter` seine Abhängigkeiten über eine eigene Methode entgegennehmen und den Konstruktor für seine Nachkommen frei lassen: - -```php -abstract class BasePresenter extends Nette\Application\UI\Presenter -{ - private Foo $foo; - - public function injectBase(Foo $foo): void - { - $this->foo = $foo; - } -} - -class MyPresenter extends BasePresenter -{ - private Bar $bar; - - public function __construct(Bar $bar) - { - $this->bar = $bar; - } -} -``` - -Ein Presenter kann beliebig viele `inject*()`-Methoden enthalten, und jede kann beliebig viele Parameter haben. Hervorragend eignet sich dieser Ansatz auch für Fälle, in denen ein Presenter [aus Traits zusammengesetzt |presenter-traits] ist und jedes Trait eigene Abhängigkeiten verlangt. - - -`Inject`-Attribute -================== - -Dabei handelt es sich um eine Form der [Injektion in Properties |dependency-injection:passing-dependencies#Property Injection]. Es genügt, die Properties zu kennzeichnen, in die injiziert werden soll, und Nette DI übergibt die Abhängigkeiten automatisch unmittelbar nach dem Erzeugen der Presenter-Instanz. Damit sie eingesetzt werden können, müssen diese Properties als public deklariert sein. - -Die Properties kennzeichnen wir mit einem Attribut: (früher wurde die Annotation `/** @inject */` verwendet) - -```php -use Nette\DI\Attributes\Inject; // diese Zeile ist wichtig - -class MyPresenter extends Nette\Application\UI\Presenter -{ - #[Inject] - public Cache $cache; -} -``` - -Der Vorteil dieser Art der Abhängigkeitsübergabe war die sehr sparsame Schreibweise. Mit der Einführung von [Constructor Property Promotion |https://blog.nette.org/de/php-8-0-complete-overview-of-news#toc-constructor-property-promotion] erscheint es jedoch einfacher, den Konstruktor zu verwenden. - -Umgekehrt leidet dieser Weg an denselben Nachteilen wie die Übergabe von Abhängigkeiten in Properties allgemein: Wir haben keine Kontrolle über Änderungen der Variablen, und die Variable wird Teil der öffentlichen Schnittstelle der Klasse, was unerwünscht ist. diff --git a/best-practices/de/lets-create-contact-form.texy b/best-practices/de/lets-create-contact-form.texy deleted file mode 100644 index 02c59866d7..0000000000 --- a/best-practices/de/lets-create-contact-form.texy +++ /dev/null @@ -1,218 +0,0 @@ -Erstellen wir ein Kontaktformular -********************************* - -.[perex] -Sehen wir uns an, wie man in Nette ein Kontaktformular erstellt, einschließlich des Versands der übermittelten Daten per E-Mail. Also los! - -Zuerst müssen wir ein neues Projekt anlegen. Wie das geht, erklärt die Seite [Erste Schritte |nette:installation]. Und dann können wir mit der Erstellung des Formulars beginnen. - -Am einfachsten ist es, das [Formular direkt im Presenter |forms:in-presenter] zu erstellen. Wir können den bereits vorhandenen `HomePresenter` verwenden. Wir fügen ihm eine Komponente `contactForm` hinzu, die das Formular darstellt. Das erreichen wir, indem wir in den Code die Factory-Methode `createComponentContactForm()` schreiben, die die Komponente erzeugt: - -```php -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - protected function createComponentContactForm(): Form - { - $form = new Form; - $form->addText('name', 'Name:') - ->setRequired('Bitte geben Sie Ihren Namen ein'); - $form->addEmail('email', 'E-Mail:') - ->setRequired('Bitte geben Sie Ihre E-Mail-Adresse ein'); - $form->addTextArea('message', 'Nachricht:') - ->setRequired('Bitte geben Sie eine Nachricht ein'); - $form->addSubmit('send', 'Senden'); - $form->onSuccess[] = $this->contactFormSucceeded(...); - return $form; - } - - private function contactFormSucceeded(Form $form, $data): void - { - // Versand der E-Mail - } -} -``` - -Wie Sie sehen, haben wir zwei Methoden erstellt. Die erste Methode, `createComponentContactForm()`, erzeugt eine neue Formularinstanz. Sie enthält Felder für Name, E-Mail und Nachricht, die wir mit den Methoden `addText()`, `addEmail()` und `addTextArea()` hinzufügen. Außerdem haben wir eine Schaltfläche zum Absenden ergänzt. Aber was, wenn der Benutzer ein Feld leer lässt? In diesem Fall sollten wir ihm mitteilen, dass es sich um ein Pflichtfeld handelt. Das haben wir mit der Methode `setRequired()` erreicht. Schließlich haben wir noch einen Handler für das [Event |nette:glossary#Events] `onSuccess` angehängt, der beim erfolgreichen Absenden des Formulars ausgelöst wird. In unserem Fall ruft er die Methode `contactFormSucceeded` auf, die sich um die Verarbeitung der übermittelten Daten kümmert. Diese Methode ergänzen wir gleich. - -Die Komponente `contactForm` lassen wir im Template `Home/default.latte` rendern: - -```latte -{block content} -<h1>Kontaktformular</h1> -{control contactForm} -``` - -Für den eigentlichen Versand der E-Mail erstellen wir eine neue Klasse mit dem Namen `ContactFacade` und legen sie in der Datei `app/Model/ContactFacade.php` ab: - -```php -namespace App\Model; - -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $mail = new Message; - $mail->addTo('admin@example.com') // Ihre E-Mail-Adresse - ->setFrom($email, $name) - ->setSubject('Nachricht aus dem Kontaktformular') - ->setBody($message); - - $this->mailer->send($mail); - } -} -``` - -Die Methode `sendMessage()` erstellt und versendet die E-Mail. Sie nutzt dazu einen Mailer, den sie sich als Abhängigkeit über den Konstruktor übergeben lässt. Lesen Sie mehr über das [Versenden von E-Mails |mail:]. - -Nun kehren wir zum Presenter zurück und vervollständigen die Methode `contactFormSucceeded()`. Sie ruft die Methode `sendMessage()` der Klasse `ContactFacade` auf und übergibt ihr die über das Formular gesendeten Daten. Und wie bekommen wir das Objekt `ContactFacade`? Wir lassen es uns über den Konstruktor übergeben: - -```php -use App\Model\ContactFacade; -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - public function __construct( - private ContactFacade $facade, - ) { - } - - protected function createComponentContactForm(): Form - { - // ... - } - - public function contactFormSucceeded(stdClass $data): void - { - $this->facade->sendMessage($data->email, $data->name, $data->message); - $this->flashMessage('Die Nachricht wurde gesendet'); - $this->redirect('this'); - } -} -``` - -Nachdem die E-Mail versendet wurde, zeigen wir dem Benutzer noch eine [Flash-Meldung |application:components#Flash-Meldungen], die den Versand bestätigt. Danach leiten wir weiter, damit das Formular nicht durch ein *Refresh* im Browser erneut abgeschickt werden kann. - - -So, und wenn alles richtig eingerichtet ist, sollten Sie nun eine E-Mail aus Ihrem Kontaktformular versenden können. Herzlichen Glückwunsch! - - -HTML-Template für E-Mails -------------------------- - -Bisher wird eine einfache Text-E-Mail versendet, die nur die über das Formular gesendete Nachricht enthält. In der E-Mail können wir aber HTML verwenden und ihr Aussehen ansprechender gestalten. Wir erstellen dafür ein Template in Latte, das wir unter `app/Model/contactEmail.latte` speichern: - -```latte -<html> - <title>Nachricht aus dem Kontaktformular - - -

    Name: {$name}

    -

    E-Mail: {$email}

    -

    Nachricht: {$message}

    - - -``` - -Es bleibt, `ContactFacade` so anzupassen, dass diese Vorlage verwendet wird. Im Konstruktor fordern wir die Klasse `LatteFactory` an, die ein Objekt `Latte\Engine` erzeugen kann, also den [Renderer für Latte-Templates |latte:develop#Wie rendert man ein Template?]. Mit der Methode `renderToString()` rendern wir das Template in einen String; der erste Parameter ist der Pfad zur Template-Datei und der zweite ein Array der zu übergebenden Variablen. - -```php -namespace App\Model; - -use Nette\Bridges\ApplicationLatte\LatteFactory; -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $latte = $this->latteFactory->create(); - $body = $latte->renderToString(__DIR__ . '/contactEmail.latte', [ - 'email' => $email, - 'name' => $name, - 'message' => $message, - ]); - - $mail = new Message; - $mail->addTo('admin@example.com') // Ihre E-Mail-Adresse - ->setFrom($email, $name) - ->setHtmlBody($body); - - $this->mailer->send($mail); - } -} -``` - -Den erzeugten HTML-Inhalt der E-Mail übergeben wir dann der Methode `setHtmlBody()` statt der ursprünglichen `setBody()`. Ebenso müssen wir den Betreff der E-Mail nicht mehr mit `setSubject()` angeben, denn die Bibliothek entnimmt ihn automatisch dem Element `` des Templates. - - -Konfiguration -------------- - -Im Code der Klasse `ContactFacade` steht immer noch fest verdrahtet unsere Administrator-E-Mail `admin@example.com`. Besser wäre es, sie in die Konfigurationsdatei zu verschieben. Wie geht das? - -Zuerst passen wir die Klasse `ContactFacade` an und ersetzen den fest verdrahteten E-Mail-String durch eine über den Konstruktor übergebene Variable: - -```php -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - private string $adminEmail, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - // ... - $mail = new Message; - $mail->addTo($this->adminEmail) - ->setFrom($email, $name) - ->setHtmlBody($body); - // ... - } -} -``` - -Der zweite Schritt besteht darin, den Wert dieser Variablen in der Konfiguration anzugeben. In die Datei `app/config/services.neon` schreiben wir: - -```neon -services: - - App\Model\ContactFacade(adminEmail: admin@example.com) -``` - -Und das war's. Wenn der Abschnitt `services` viele Einträge enthält und Sie das Gefühl haben, dass die E-Mail-Adresse zwischen ihnen untergeht, können wir daraus einen Parameter machen. Wir ändern den Eintrag so: - -```neon -services: - - App\Model\ContactFacade(adminEmail: %adminEmail%) -``` - -Und in der Datei `app/config/common.neon` definieren wir diesen Parameter: - -```neon -parameters: - adminEmail: admin@example.com -``` - -Und fertig! diff --git a/best-practices/de/microsites.texy b/best-practices/de/microsites.texy deleted file mode 100644 index 33045463e9..0000000000 --- a/best-practices/de/microsites.texy +++ /dev/null @@ -1,63 +0,0 @@ -Wie man Microsites schreibt -*************************** - -Stellen Sie sich vor, Sie müssen schnell eine kleine Website für die bevorstehende Veranstaltung Ihrer Firma erstellen. Sie soll einfach und schnell sein, ohne unnötige Komplikationen. Vielleicht denken Sie, dass Sie für ein so kleines Projekt kein robustes Framework brauchen. Aber was, wenn der Einsatz des Nette Frameworks diesen Prozess gerade vereinfachen und beschleunigen kann? - -Auch beim Erstellen einfacher Websites wollen Sie schließlich nicht auf Komfort verzichten. Sie wollen nicht neu erfinden, was schon einmal gelöst wurde. Seien Sie ruhig faul und lassen Sie sich verwöhnen. Das Nette Framework lässt sich hervorragend auch als Micro-Framework einsetzen. - -Wie kann eine solche Microsite aussehen? Zum Beispiel so, dass wir den gesamten Code der Website in einer einzigen Datei `index.php` im öffentlichen Verzeichnis unterbringen: - -```php -<?php - -require __DIR__ . '/../vendor/autoload.php'; - -$configurator = new Nette\Bootstrap\Configurator; -$configurator->enableTracy(__DIR__ . '/../log'); -$configurator->setTempDirectory(__DIR__ . '/../temp'); - -// erzeuge den DI-Container auf Basis der Konfiguration in config.neon -$configurator->addConfig(__DIR__ . '/../app/config.neon'); -$container = $configurator->createContainer(); - -// wir richten das Routing ein -$router = new Nette\Application\Routers\RouteList; -$container->addService('router', $router); - -// Route für die URL https://example.com/ -$router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { - // wir erkennen die Sprache des Browsers und leiten auf die URL /en oder /de usw. weiter - $supportedLangs = ['en', 'de', 'cs']; - $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); - return $presenter->redirectUrl("/$lang"); -}); - -// Route für die URL https://example.com/cs oder https://example.com/en -$router->addRoute('<lang cs|en|de>', function ($presenter, string $lang) { - // wir zeigen das entsprechende Template an, zum Beispiel ../templates/en.latte - $template = $presenter->createTemplate() - ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); - return $template; -}); - -// starte die Anwendung! -$container->getByType(Nette\Application\Application::class)->run(); -``` - -Alles Übrige sind Templates, die im übergeordneten Verzeichnis `/templates` liegen. - -Der PHP-Code in `index.php` [bereitet zuerst die Umgebung vor |bootstrap:], definiert dann die [Routen |application:routing#Dynamisches Routing mit Callbacks] und startet schließlich die Anwendung. Der Vorteil ist, dass der zweite Parameter der Funktion `addRoute()` ein Callable sein kann, das beim Öffnen der entsprechenden Seite ausgeführt wird. - - -Warum Nette für Microsites verwenden? -------------------------------------- - -- Programmierer, die [Tracy |tracy:] einmal ausprobiert haben, können sich heute kaum vorstellen, ohne sie zu programmieren. -- Vor allem aber profitieren Sie vom Templating-System [Latte |latte:], denn schon ab zwei Seiten wollen Sie [Layout und Inhalt |latte:template-inheritance] getrennt haben. -- Und Sie wollen sich unbedingt auf das [automatische Escaping |latte:safety-first] verlassen, damit keine XSS-Sicherheitslücke entsteht. -- Nette sorgt außerdem dafür, dass bei einem Fehler niemals die technischen PHP-Fehlermeldungen angezeigt werden, sondern eine für den Benutzer verständliche Seite. -- Wenn Sie Rückmeldungen von Benutzern sammeln wollen, etwa über ein Kontaktformular, ergänzen Sie einfach noch [Formulare |forms:] und die [Datenbank |database:]. -- Ausgefüllte Formulare können Sie sich ebenso leicht [per E-Mail zusenden lassen |mail:]. -- Manchmal kann Ihnen auch das [Caching |caching:] gelegen kommen, zum Beispiel wenn Sie Feeds herunterladen und anzeigen. - -In der heutigen Zeit, in der Geschwindigkeit und Effizienz entscheidend sind, ist es wichtig, Werkzeuge zu haben, mit denen Sie ohne unnötige Verzögerungen zu Ergebnissen kommen. Das Nette Framework bietet Ihnen genau das - schnelle Entwicklung, Sicherheit und eine breite Palette an Werkzeugen wie Tracy und Latte, die den Prozess vereinfachen. Es genügt, ein paar Nette-Pakete zu installieren, und der Aufbau einer solchen Microsite wird zum Kinderspiel. Und Sie können sicher sein, dass sich nirgends eine Sicherheitslücke versteckt. diff --git a/best-practices/de/pagination.texy b/best-practices/de/pagination.texy deleted file mode 100644 index f7eb8d6d15..0000000000 --- a/best-practices/de/pagination.texy +++ /dev/null @@ -1,273 +0,0 @@ -Paginierung von Datenbankergebnissen -************************************ - -.[perex] -Bei der Entwicklung von Webanwendungen stoßen Sie sehr oft auf die Anforderung, die Anzahl der auf einer Seite aufgelisteten Einträge zu begrenzen. Diese Technik nennt man Paginierung. - -Gehen wir von einem Zustand aus, in dem wir alle Daten ohne Paginierung auflisten. Für die Auswahl der Daten aus der Datenbank haben wir eine Klasse `ArticleRepository`. Sie enthält neben dem Konstruktor eine Methode `findPublishedArticles`, die alle veröffentlichten Artikel absteigend nach dem Veröffentlichungsdatum sortiert zurückgibt. - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC', - new \DateTime, - ); - } -} -``` - -Im Presenter injizieren wir dann diese Modellklasse. In der Render-Methode fordern wir die veröffentlichten Artikel an und übergeben sie an das Template: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(): void - { - $this->template->articles = $this->articleRepository->findPublishedArticles(); - } -} -``` - -Im Template `default.latte` kümmern wir uns dann um die Ausgabe der Artikel: - -```latte -{block content} -<h1>Artikel</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> -``` - - -Auf diese Weise können wir alle Artikel auflisten, was jedoch in dem Moment problematisch wird, in dem die Anzahl der Artikel wächst. Dann kommt die Implementierung eines Paginierungsmechanismus gelegen. - -Dieser Mechanismus sorgt dafür, dass alle Artikel auf mehrere Seiten verteilt werden und wir nur die Artikel der gerade ausgewählten Seite anzeigen. Die Gesamtzahl der Seiten und die Aufteilung der Artikel berechnet der [Paginator |utils:Paginator] selbst, und zwar anhand der Gesamtzahl der Artikel und der gewünschten Anzahl von Artikeln pro Seite. - -Im ersten Schritt passen wir die Methode zum Abrufen der Artikel in der Repository-Klasse so an, dass sie nur die Artikel einer Seite zurückgeben kann. Außerdem fügen wir eine Methode hinzu, um die Gesamtzahl der Artikel in der Datenbank zu ermitteln, die wir für die Konfiguration des Paginators brauchen: - -```php -namespace App\Model; - -use Nette; - - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(int $limit, int $offset): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC - LIMIT ? - OFFSET ?', - new \DateTime, $limit, $offset, - ); - } - - /** - * Gibt die Gesamtzahl der veröffentlichten Artikel zurück - */ - public function getPublishedArticlesCount(): int - { - return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime); - } -} -``` - -Als Nächstes machen wir uns an die Anpassung des Presenters. An die Methode `renderDefault` übergeben wir die Nummer der aktuell angezeigten Seite. Für den Fall, dass diese Nummer nicht Teil der URL ist, setzen wir den Standardwert 1 (die erste Seite). - -Außerdem erweitern wir die Render-Methode, sodass sie eine Paginator-Instanz erzeugt und konfiguriert und die passenden Artikel für die Anzeige im Template auswählt. Der angepasste `HomePresenter` sieht dann so aus: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Wir ermitteln die Gesamtzahl der veröffentlichten Artikel - $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - - // Wir erzeugen eine Paginator-Instanz und konfigurieren sie - $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // Gesamtzahl der Einträge - $paginator->setItemsPerPage(10); // Einträge pro Seite - $paginator->setPage($page); // Nummer der aktuellen Seite - - // Aus der Datenbank holen wir eine begrenzte Menge von Artikeln nach der Berechnung des Paginators - $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - - // die wir an das Template übergeben - $this->template->articles = $articles; - // und auch den Paginator selbst für die Anzeige der Paginierung - $this->template->paginator = $paginator; - } -} -``` - -Das Template iteriert nun nur noch über die Artikel der aktuellen Seite. Wir müssen lediglich die Paginierungslinks hinzufügen: - -```latte -{block content} -<h1>Artikel</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if !$paginator->isFirst()} - <a n:href="default, 1">Erste</a> -  |  - <a n:href="default, $paginator->getPage() - 1">Vorherige</a> -  |  - {/if} - - Seite {$paginator->getPage()} von {$paginator->getPageCount()} - - {if !$paginator->isLast()} -  |  - <a n:href="default, $paginator->getPage() + 1">Nächste</a> -  |  - <a n:href="default, $paginator->getPageCount()">Letzte</a> - {/if} -</div> -``` - - -Damit ist die Paginierung mithilfe des Paginators fertig. Wenn Sie als Datenbankschicht statt [Nette Database Core |database:sql-way] den [Nette Database Explorer |database:explorer] verwenden, können Sie die Paginierung sogar ohne direkte Verwendung des Paginators umsetzen. Die Klasse `Nette\Database\Table\Selection` enthält nämlich die Methode [page() |api:Nette\Database\Table\Selection::page()], die die Paginierungslogik kapselt. - -Das Repository sieht bei dieser Art der Implementierung so aus: - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Explorer $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\Table\Selection - { - return $this->database->table('articles') - ->where('created_at < ', new \DateTime) - ->order('created_at DESC'); - } -} -``` - -Im Presenter müssen wir keine Paginator-Instanz erzeugen. Stattdessen verwenden wir die Methode `page()` des `Selection`-Objekts, das uns das Repository zurückgibt: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Wir holen die veröffentlichten Artikel - $articles = $this->articleRepository->findPublishedArticles(); - - // und senden an das Template nur den Teil, den die Berechnung der Methode page begrenzt - $lastPage = 0; - $this->template->articles = $articles->page($page, 10, $lastPage); - - // und außerdem die nötigen Daten für die Anzeige der Paginierung - $this->template->page = $page; - $this->template->lastPage = $lastPage; - } -} -``` - -Da wir dem Template jetzt kein Paginator-Objekt mehr übergeben, passen wir den Teil an, der die Paginierungslinks anzeigt: - -```latte -{block content} -<h1>Artikel</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if $page > 1} - <a n:href="default, 1">Erste</a> -  |  - <a n:href="default, $page - 1">Vorherige</a> -  |  - {/if} - - Seite {$page} von {$lastPage} - - {if $page < $lastPage} -  |  - <a n:href="default, $page + 1">Nächste</a> -  |  - <a n:href="default, $lastPage">Letzte</a> - {/if} -</div> -``` - -Auf diese Weise haben wir den Paginierungsmechanismus ohne explizite Verwendung des Paginators implementiert. - -{{priority: -1}} diff --git a/best-practices/de/passing-settings-to-presenters.texy b/best-practices/de/passing-settings-to-presenters.texy deleted file mode 100644 index 561fe9e506..0000000000 --- a/best-practices/de/passing-settings-to-presenters.texy +++ /dev/null @@ -1,49 +0,0 @@ -Einstellungen an Presenter übergeben -************************************ - -.[perex] -Müssen Sie an Presenter Argumente übergeben, die keine Objekte sind (z. B. die Information, ob die Anwendung im Debug-Modus läuft, Pfade zu Verzeichnissen usw.) und die deshalb nicht automatisch per Autowiring übergeben werden können? Die Lösung ist, sie in ein Objekt `Settings` zu kapseln. - -Der Service `Settings` stellt einen sehr einfachen und zugleich nützlichen Weg dar, Presentern Informationen über die laufende Anwendung bereitzustellen. Seine konkrete Gestalt hängt ganz von Ihren Bedürfnissen ab. Beispiel: - -```php -namespace App; - -class Settings -{ - public function __construct( - // seit PHP 8.1 kann readonly angegeben werden - public bool $debugMode, - public string $appDir, - // und so weiter - ) {} -} -``` - -Beispiel für die Registrierung in der Konfiguration: - -```neon -services: - - App\Settings( - %debugMode%, - %appDir%, - ) -``` - -Wenn ein Presenter die von diesem Service bereitgestellten Informationen braucht, fordert er ihn einfach im Konstruktor an: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private App\Settings $settings, - ) {} - - public function renderDefault() - { - if ($this->settings->debugMode) { - // ... - } - } -} -``` diff --git a/best-practices/de/post-links.texy b/best-practices/de/post-links.texy deleted file mode 100644 index a50a492a2b..0000000000 --- a/best-practices/de/post-links.texy +++ /dev/null @@ -1,56 +0,0 @@ -Wie man POST-Links richtig verwendet -************************************ - -.[perex] -In Webanwendungen, besonders in Administrationsoberflächen, sollte es eine Grundregel sein, dass Aktionen, die den Zustand des Servers ändern, nicht über die HTTP-Methode GET ausgeführt werden. Wie der Name der Methode nahelegt, sollte GET nur dem Abrufen von Daten dienen, nicht deren Änderung. Für Aktionen wie das Löschen von Datensätzen ist die Methode POST besser geeignet. Ideal wäre zwar die Methode DELETE, die lässt sich aber ohne JavaScript nicht auslösen, weshalb historisch POST verwendet wird. - -Wie geht das in der Praxis? Nutzen Sie diesen einfachen Trick. Legen Sie am Anfang des Layout-Templates ein Hilfsformular mit der ID `postForm` an, das Sie anschließend für Lösch-Schaltflächen verwenden: - -```latte .{file:@layout.latte} -<form method="post" id="postForm"></form> -``` - -Dank dieses Formulars können Sie statt eines klassischen Links `<a>` eine Schaltfläche `<button>` verwenden, die sich optisch so gestalten lässt, dass sie wie ein gewöhnlicher Link aussieht. Das CSS-Framework Bootstrap bietet zum Beispiel die Klassen `btn btn-link`, mit denen sich die Schaltfläche optisch nicht von den übrigen Links unterscheidet. Mit dem Attribut `form="postForm"` verbinden wir sie mit dem vorbereiteten Hilfsformular: - -```latte .{file:admin.latte} -<table> - <tr n:foreach="$posts as $post"> - <td>{$post->title}</td> - <td> - <button class="btn btn-link" form="postForm" formaction="{link delete, $post->id}">delete</button> - <!-- instead of <a n:href="delete, $post->id">delete</a> --> - </td> - </tr> -</table> -``` - -Beim Klick auf diese Schaltfläche wird nun die Aktion `delete` ausgelöst. Um sicherzustellen, dass Requests nur über die Methode POST und von derselben Domain angenommen werden (was eine wirksame Abwehr gegen CSRF-Angriffe ist), verwenden Sie das Attribut `#[Requires]`: - -```php .{file:AdminPresenter.php} -use Nette\Application\Attributes\Requires; - -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST', sameOrigin: true)] - public function actionDelete(int $id): void - { - $this->facade->deletePost($id); // hypothetischer Code zum Löschen eines Datensatzes - $this->redirect('default'); - } -} -``` - -Dieses Attribut gibt es seit Nette Application 3.2. Mehr über seine Möglichkeiten erfahren Sie auf der Seite [Wie man das Attribut #Requires verwendet |attribute-requires]. - -Wenn Sie statt der Aktion `actionDelete()` das Signal `handleDelete()` verwenden, ist die Angabe von `sameOrigin: true` nicht nötig, denn Signale haben diesen Schutz standardmäßig aktiviert: - -```php .{file:AdminPresenter.php} -#[Requires(methods: 'POST')] -public function handleDelete(int $id): void -{ - $this->facade->deletePost($id); - $this->redirect('this'); -} -``` - -Dieser Ansatz verbessert nicht nur die Sicherheit Ihrer Anwendung, sondern trägt auch zur Einhaltung korrekter Webstandards und Praktiken bei. Mit der Verwendung von POST für zustandsändernde Aktionen erreichen Sie eine robustere und sicherere Anwendung. diff --git a/best-practices/de/presenter-traits.texy b/best-practices/de/presenter-traits.texy deleted file mode 100644 index 442522895b..0000000000 --- a/best-practices/de/presenter-traits.texy +++ /dev/null @@ -1,47 +0,0 @@ -Presenter aus Traits zusammensetzen -*********************************** - -.[perex] -Wenn Sie in mehreren Presentern denselben Code implementieren müssen (z. B. die Prüfung, ob der Benutzer angemeldet ist), bietet es sich an, den Code in einen gemeinsamen Vorfahren zu legen. Die zweite Möglichkeit ist, [Traits |nette:introduction-to-object-oriented-programming#Traits] mit einem einzigen Zweck zu erstellen. - -Der Vorteil dieser Lösung ist, dass jeder Presenter genau die Traits verwenden kann, die er wirklich braucht, während Mehrfachvererbung in PHP nicht möglich ist. - -Diese Traits können sich zunutze machen, dass beim Erzeugen des Presenters nacheinander alle [Inject-Methoden |inject-method-attribute#inject*()-Methoden] aufgerufen werden. Sie müssen nur darauf achten, dass der Name jeder Inject-Methode über alle verwendeten Traits und den Presenter selbst hinweg eindeutig ist. - -Traits können Initialisierungscode an die Events [onStartup oder onRender |application:presenters#Events] hängen. - -Beispiele: - -```php -trait RequireLoggedUser -{ - public function injectRequireLoggedUser(): void - { - $this->onStartup[] = function () { - if (!$this->getUser()->isLoggedIn()) { - $this->redirect('Sign:in', $this->storeRequest()); - } - }; - } -} - -trait StandardTemplateFilters -{ - public function injectStandardTemplateFilters(TemplateBuilder $builder): void - { - $this->onRender[] = function () use ($builder) { - $builder->setupTemplate($this->template); - }; - } -} -``` - -Der Presenter verwendet diese Traits dann einfach: - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - use StandardTemplateFilters; - use RequireLoggedUser; -} -``` diff --git a/best-practices/de/pretty-urls.texy b/best-practices/de/pretty-urls.texy deleted file mode 100644 index 9cde1971e6..0000000000 --- a/best-practices/de/pretty-urls.texy +++ /dev/null @@ -1,204 +0,0 @@ -Schöne URLs mit Slugs -********************* - -.[perex] -URLs wie `/artikel/123-wie-man-brot-backt` sehen besser aus als `/artikel/123` und helfen sowohl Benutzern als auch Suchmaschinen zu verstehen, was auf der Seite wartet. Diese Anleitung zeigt, wie Sie sie rein im Router erzeugen - ohne Eingriff in ein einziges Template - und wie Sie dafür sorgen, dass jeder Besucher auf der kanonischen URL landet. - - -Warum ein Slug in der URL -========================= - -Vergleichen Sie diese beiden Adressen: - -``` -/artikel/123 -/artikel/123-wie-man-brot-backt -``` - -Die zweite verrät dem Benutzer (und Google), was ihn nach dem Klick erwartet. Das ist gut für SEO, macht Links in Chat oder E-Mail lesbar und gibt auch der Adresszeile einen Sinn. - -Der Slug ist aber kein echter Bezeichner. Die Seite wird durch die ID bestimmt. Der Slug ist nur Dekoration, die die Anwendung aus dem Titel erzeugt. Ändert sich der Titel, sollte sich auch der Slug ändern. Und wenn jemand die URL von Hand bearbeitet oder einem alten Link folgt, sollte die Anwendung trotzdem die richtige Seite finden. - - -Das Ziel -======== - -Wir wollen eine Route, die alle diese Fälle beherrscht: - -``` -/artikel/123 → öffnet Artikel 123, leitet auf die kanonische URL weiter -/artikel/123-wie-man-brot-backt → öffnet Artikel 123 direkt -/artikel/123-was-auch-immer-jemand-tippte → öffnet Artikel 123, leitet auf die kanonische URL weiter -/artikel/ → 404 (keine ID) -``` - -Und wir wollen, dass jedes `n:href` und jeder `link()`-Aufruf in der gesamten Anwendung automatisch `/artikel/123-wie-man-brot-backt` erzeugt - **ohne ein einziges Template umzuschreiben**. - - -Die Maske der Route -=================== - -Der Trick besteht darin, den Slug in der Maske mit eckigen Klammern als **optional** zu kennzeichnen: - -```php -$router->addRoute('artikel/<id [0-9]+>[-<slug>]', 'Article:detail'); -``` - -Die Maske `[-<slug>]` sagt: Nach der ID können (müssen aber nicht) ein Bindestrich und ein Slug folgen. Die Route akzeptiert sowohl `/artikel/123` als auch `/artikel/123-beliebig`. - -Eine Anmerkung zum Parameter `<slug>`: Standardmäßig matcht er beliebige Zeichen **außer dem Schrägstrich** - genau das, was wir wollen. Wenn Sie `<slug .+>` schreiben, matcht der Parameter auch Schrägstriche, sodass `/artikel/123-etwas/anderes` als ein einziger Slug mit `/` geparst würde. Bleiben Sie beim standardmäßigen `<slug>`, sofern Sie das nicht wirklich brauchen. - -Bis hierher wird die URL richtig geparst, aber die erzeugten Links enthalten den Slug noch nicht. Der nächste Schritt ist, der Route beizubringen, wie sie den Slug ergänzt. - - -Slug generieren ohne Eingriff in die Templates -============================================== - -Das ist die entscheidende Variante. Bestehende Aufrufe `n:href="Article:detail, $id"` funktionieren in der gesamten Anwendung unverändert weiter - der Router sucht den Titel selbst heraus. - -Wir verwenden dazu einen **allgemeinen Filter** unter dem Schlüssel des leeren Strings - er sieht alle Parameter auf einmal und kann den Slug ergänzen: - -```php -use Nette\Routing\Route; -use Nette\Utils\Strings; - -$router->addRoute('artikel/<id [0-9]+>[-<slug>]', [ - 'presenter' => 'Article', - 'action' => 'detail', - '' => [ - Route::FilterOut => function (array $params) use ($slugProvider): array { - if (isset($params['id']) && empty($params['slug'])) { - $params['slug'] = $slugProvider->getSlug((int) $params['id']); - } - return $params; - }, - ], -]); -``` - -`FilterOut` wird jedes Mal ausgeführt, wenn der Router eine URL **erzeugt**. Wurde der Slug nicht übergeben, sucht der Filter den Titel heraus und ergänzt ihn. - -Slugs können Sie in der gesamten Anwendung mit einer einzigen Änderung einführen - mit einer einzigen Routendefinition. Jeder Link in jedem Template erzeugt automatisch `/artikel/123-wie-man-brot-backt`. Kein Grep, keine Suche durch die Templates, kein übersehener Sonderfall. - - -Cache für die Suche -=================== - -Ein Link bedeutet eine DB-Abfrage, aber eine typische Seite hat viele davon - Auflistungen, Breadcrumbs, "zuletzt angesehen", verwandte Artikel. Dieselbe Artikel-ID taucht innerhalb eines Requests oft in mehreren Links auf, und Sie wollen nicht jedes Mal in die Datenbank gehen. - -Eine kleine Cache pro Request löst das. Kapseln Sie den DB-Aufruf in einen kleinen Service: - -```php -final class SlugProvider -{ - /** @var array<int, string> */ - private array $cache = []; - - public function __construct( - private Nette\Database\Explorer $db, - ) { - } - - public function getSlug(int $id): string - { - return $this->cache[$id] ??= Strings::webalize(Strings::truncate( - (string) $this->db->fetchField('SELECT title FROM article WHERE id = ?', $id), - 100, '' - )); - } -} -``` - -Das genügt - eine DB-Abfrage pro eindeutiger ID und Request. - - -Den Titel aus dem Template übergeben (optionaler schneller Weg) -=============================================================== - -Wenn Sie den Titel im Template ohnehin zur Hand haben, können Sie die DB-Abfrage ganz vermeiden. Übergeben Sie den Titel als benannten Parameter: - -```latte -<a n:href="Article:detail, $article->id, slug => $article->title">{$article->title}</a> -``` - -…und ergänzen Sie einen `FilterOut` pro Parameter, der den Titel in eine URL-sichere Form bringt: - -```php -$router->addRoute('artikel/<id [0-9]+>[-<slug>]', [ - 'presenter' => 'Article', - 'action' => 'detail', - 'slug' => [ - Route::FilterOut => fn($title) => Strings::webalize(Strings::truncate($title, 100, '')), - ], - '' => [/* der Fallback mit der Suche aus dem vorigen Beispiel */], -]); -``` - -Beide Filter arbeiten zusammen. Der allgemeine Filter läuft zuerst; er sieht, dass der Slug bereits mit dem übergebenen Titel gefüllt ist, und überspringt die Suche in der DB. Der `FilterOut` pro Parameter wandelt diesen Titel dann in den endgültigen Slug um. Templates, die den Titel nicht übergeben, funktionieren weiterhin - der allgemeine Filter findet den Slug leer vor und geht den Weg über die Suche. - -Verwenden Sie das nur dort, wo es wirklich eine Rolle spielt (große Auflistungen, die hundertfach pro Request gerendert werden). Für den größten Teil der Anwendung reicht die gecachte Suche. - - -Kanonisierung: Weiterleitung auf die richtige URL -================================================= - -Wir können nun `/artikel/123-wie-man-brot-backt` erzeugen, aber die Route akzeptiert weiterhin `/artikel/123` und `/artikel/123-was-auch-immer-jemand-schrieb`. Das ist Absicht - wir wollen kurze URLs (mehr dazu weiter unten) und wollen, dass alte oder von Hand getippte Links funktionieren. Aber wir wollen nicht, dass Suchmaschinen denselben Artikel unter mehreren Adressen indexieren. - -Die Lösung ist die [Kanonisierung |application:presenters#Kanonisierung]: Kommt der Benutzer über eine nicht kanonische URL, leitet ihn die Anwendung mit 301 auf die richtige weiter. Darum kümmert sich die Methode `canonicalize()`: - -```php -public function actionDetail(int $id, ?string $slug = null): void -{ - $article = $this->facade->getArticle($id); - if (!$article) { - $this->error(); - } - - // erzeugt die kanonische URL über denselben FilterOut - // und leitet mit HTTP 301 weiter, wenn sie sich von der aktuellen URL unterscheidet - $this->canonicalize('detail', ['id' => $id]); - - $this->template->article = $article; -} -``` - -`canonicalize()` erzeugt die kanonische URL auf dieselbe Weise wie `link()` (läuft also durch denselben `FilterOut`) und vergleicht sie mit der aktuellen URL. Unterscheiden sie sich, leitet es mit HTTP 301 weiter. Besucher landen auf der richtigen URL, Suchmaschinen sehen nur eine kanonische Version. - - -Ein Ort, der bestimmt, wie der Slug aussieht -============================================ - -Beachten Sie, dass der Aufruf `Strings::webalize(Strings::truncate(..., 100, ''))` an einer **einzigen Stelle** lebt - innerhalb von `SlugProvider` (oder im `FilterOut` pro Parameter). Dieselbe Logik erzeugt den Link im Template, die URL in `redirect()` und die kanonische Form in `canonicalize()`. - -Wenn Sie die Regeln später ändern wollen (anderes Längenlimit, andere Transliteration, weitere Zeichen entfernen), ändern Sie eine einzige Zeile. Ohne das würden Sie riskieren, dass `redirect()` `/artikel/123-wie-man-brot-backt` erzeugt, während `canonicalize()` `/artikel/123-wie-man-brot-back` erwartet (weil jemand anderswo eine andere `truncate`-Länge verwendet hat), und die Anwendung würde endlos weiterleiten. - - -Bonus: Kurze URLs funktionieren weiterhin -========================================= - -Weil der Slug optional ist, funktionieren auch Adressen ohne ihn: - -``` -/artikel/123 -``` - -Das ist nützlich für: -- **QR-Codes** - eine kürzere URL bedeutet einen weniger dichten, besser scanbaren Code -- **SMS und Chat** - passt in einen Tweet, sieht ordentlich aus -- **Gedruckte Materialien** - eine kurze URL tippt sich schneller - -Öffnet ein Benutzer eine solche URL, leitet ihn `canonicalize()` mit 301 auf die vollständige Version mit Slug weiter, sodass Suchmaschinen trotzdem nur die kanonische Form sehen. Sie können Kürze und SEO gleichzeitig haben. - - -Zusammenfassung -=============== - -- Die Maske `<id>[-<slug>]` macht den Slug optional. Das standardmäßige `<slug>` matcht kein `/`; verwenden Sie `<slug .+>` nur dann, wenn Sie wirklich Schrägstriche im Slug wollen. -- Ein allgemeiner `FilterOut` unter dem Schlüssel `''` sucht den Titel anhand der ID heraus - **ohne Eingriff in Templates irgendwo in der Anwendung**. -- Kapseln Sie die Suche in eine kleine Cache pro Request; eine DB-Abfrage pro eindeutiger ID genügt. -- Optional kann ein `FilterOut` pro Parameter den Templates erlauben, den Titel direkt zu übergeben und die Suche zu überspringen. -- `$this->canonicalize()` in der Action leitet nicht kanonische URLs mit HTTP 301 auf die richtige weiter. -- Die Formel für den Slug (`webalize` + `truncate`) lebt an einer Stelle - Sie ändern sie einmal, sie wirkt überall. -- Kurze URLs nur mit ID funktionieren weiterhin, was für QR-Codes und SMS praktisch ist. - -Mehr über Filter und Kanonisierung finden Sie in der Dokumentation zum [Routing |application:routing#Allgemeine Filter] und zu den [Presentern |application:presenters#Kanonisierung]. diff --git a/best-practices/de/restore-request.texy b/best-practices/de/restore-request.texy deleted file mode 100644 index 9816cb9cfc..0000000000 --- a/best-practices/de/restore-request.texy +++ /dev/null @@ -1,62 +0,0 @@ -Wie man zu einer früheren Seite zurückkehrt? -******************************************** - -.[perex] -Was passiert, wenn ein Benutzer gerade ein Formular ausfüllt und seine Anmeldung abläuft? Damit die Daten nicht verloren gehen, speichern wir den aktuellen Request (samt Formulardaten) vor der Weiterleitung zur Anmeldeseite in der Session. In Nette ist das erstaunlich einfach. - -Der aktuelle Request lässt sich mit der Methode `storeRequest()` in der Session speichern. Sie gibt einen eindeutigen Bezeichner in Form eines kurzen Strings zurück. Die Methode speichert den Namen des aktuellen Presenters, dessen View und dessen Parameter. Wurde als Teil des Requests auch ein Formular abgeschickt, werden zusätzlich die eingegebenen Feldwerte gespeichert (mit Ausnahme hochgeladener Dateien). - -Die Wiederherstellung des Requests übernimmt die Methode `restoreRequest($key)`, der Sie den zuvor erhaltenen Bezeichner übergeben. Sie leitet zurück zum ursprünglichen Presenter und View. Enthielt der gespeicherte Request jedoch das Absenden eines Formulars, verwendet `restoreRequest()` statt der Weiterleitung die Methode `forward()`. Sie übergibt dem Formular die zuvor eingegebenen Werte zurück und lässt es erneut rendern. Der Benutzer kann das Formular so noch einmal absenden, ohne dass eingegebene Daten verloren gehen. - -Wichtig ist, dass `restoreRequest()` prüft, ob der neu angemeldete Benutzer derselbe ist, der das Formular ursprünglich abgeschickt hat. Ist es ein anderer, wird der gespeicherte Request nicht wiederhergestellt und die Methode tut nichts, was die Sicherheit erhöht. - -Zeigen wir das an einem Beispiel. Nehmen wir einen `AdminPresenter`, in dem Daten bearbeitet werden. In seiner Methode `startup()` prüfen wir, ob der Benutzer angemeldet ist. Ist er es nicht, leiten wir ihn zum `SignPresenter` weiter. Gleichzeitig speichern wir den aktuellen Request mit `storeRequest()` und übergeben dessen Schlüssel (das `$backlink`) an den `SignPresenter`. - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - protected function startup() - { - parent::startup(); - - if (!$this->user->isLoggedIn()) { - $this->redirect('Sign:in', ['backlink' => $this->storeRequest()]); - } - } -} -``` - -Der `SignPresenter` enthält außer dem Anmeldeformular noch einen persistenten Parameter `$backlink`, in den der Schlüssel geschrieben wird. Weil der Parameter persistent ist, bleibt sein Wert auch nach dem Absenden des Anmeldeformulars erhalten. - - -```php -use Nette\Application\Attributes\Persistent; - -class SignPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $backlink = ''; - - protected function createComponentSignInForm() - { - $form = new Nette\Application\UI\Form; - // ... wir fügen die Formularfelder hinzu ... - $form->onSuccess[] = $this->signInFormSuceeded(...); - return $form; - } - - private function signInFormSuceeded($form) - { - // ... hier melden wir den Benutzer an ... - - $this->restoreRequest($this->backlink); - $this->redirect('Admin:'); - } -} -``` - -Der Methode `restoreRequest()` übergeben wir den Schlüssel (`$this->backlink`) des gespeicherten Requests. Sie leitet den Benutzer dann zum ursprünglichen Presenter und View weiter (oder springt dorthin). - -Ist der Schlüssel jedoch ungültig (weil er zum Beispiel nicht mehr in der Session existiert), tut die Methode nichts. Der anschließende Aufruf `$this->redirect('Admin:')` dient deshalb als Fallback und leitet auf eine Standardseite wie den `AdminPresenter` weiter. - -{{priority: -1}} diff --git a/best-practices/en/@home.texy b/best-practices/en/@home.texy deleted file mode 100644 index 2a8cb1b0d5..0000000000 --- a/best-practices/en/@home.texy +++ /dev/null @@ -1,72 +0,0 @@ -Tutorials and Best Practices -**************************** - -.[perex] -Tutorials, solutions to common tasks, and best practices for Nette. - - -<div class=documentation> -<div> - - -Nette Application ------------------ -- [Inject Methods and Attributes |inject-method-attribute] -- [Composing Presenters from Traits |presenter-traits] -- [Passing Settings to Presenters |passing-settings-to-presenters] -- [How to Restore a Request |restore-request] -- [Paginating Database Results |pagination] -- [Dynamic Snippets |dynamic-snippets] -- [How to Use the #Requires Attribute |attribute-requires] -- [How to Properly Use POST Links |post-links] -- [Pretty URLs with Slugs |pretty-urls] - -</div> -<div> - - -Forms ------ -- [Reusing Forms |form-reuse] -- [Form for Creating and Editing Records |creating-editing-form] -- [Let's Create a Contact Form |lets-create-contact-form] -- [Dependent Selectboxes |https://blog.nette.org/en/dependent-selectboxes-elegantly-in-nette-and-pure-js] - -</div> -<div> - - -General -------- -- [How to Load a Configuration File |bootstrap:] -- [How to Write Microsites |microsites] -- [Why Does Nette Use PascalCase Notation for Constants? |https://blog.nette.org/en/for-less-screaming-in-the-code] -- [Why Doesn't Nette Use the Interface Suffix? |https://blog.nette.org/en/prefixes-and-suffixes-do-not-belong-in-interface-names] -- [Composer: Usage Tips |composer] -- [Introduction to Object-Oriented Programming |nette:introduction-to-object-oriented-programming] - -</div> -<div> - - -Example Solutions ------------------ -- [Nette examples |https://github.com/nette-examples] -- [Doctrine & Nette |https://contributte.org/nettrine/] -- [Contributte examples |https://contributte.org/examples.html] -- [Doctrine ORM Website |https://github.com/MinecordNetwork/Website] -- [Quick start |quickstart:] - -</div> -<div> - - -Videos ------- -Hundreds of recordings from Last Saturday meetups and videos about Nette can all be found in one place on the "Nette Framework YouTube Channel":https://www.youtube.com/user/NetteFramework. - -</div> -</div> - -{{maintitle: Nette Tutorials and Best Practices: Common Tasks Solved}} -{{description: Tutorials, solutions to common tasks and best practices for Nette: forms, pagination, pretty URLs, presenters, Composer and more.}} diff --git a/best-practices/en/@left-menu.texy b/best-practices/en/@left-menu.texy deleted file mode 100644 index cb2b5244eb..0000000000 --- a/best-practices/en/@left-menu.texy +++ /dev/null @@ -1,34 +0,0 @@ -Tutorials and Best Practices -**************************** -- [Overview |@home] - -Nette Application -***************** -- [Inject Methods and Attributes |inject-method-attribute] -- [Composing Presenters from Traits |presenter-traits] -- [Passing Settings to Presenters |passing-settings-to-presenters] -- [How to Restore a Request |restore-request] -- [Paginating Database Results |pagination] -- [Dynamic Snippets |dynamic-snippets] -- [How to Use the #Requires Attribute |attribute-requires] -- [How to Properly Use POST Links |post-links] -- [Pretty URLs with Slugs |pretty-urls] - -Forms -***** -- [Reusing Forms |form-reuse] -- [Form for Creating and Editing Records |creating-editing-form] -- [Let's Create a Contact Form |lets-create-contact-form] - -General -******* -- [How to Write Microsites |microsites] -- [Composer: Usage Tips |composer] - - -Further Reading -*************** -- [Nette Documentation |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Troubleshooting |nette:troubleshooting] diff --git a/best-practices/en/@meta.texy b/best-practices/en/@meta.texy deleted file mode 100644 index cc2101524f..0000000000 --- a/best-practices/en/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Tutorials and Best Practices}} diff --git a/best-practices/en/attribute-requires.texy b/best-practices/en/attribute-requires.texy deleted file mode 100644 index f19a254de1..0000000000 --- a/best-practices/en/attribute-requires.texy +++ /dev/null @@ -1,180 +0,0 @@ -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. - -An attribute is a special marker in PHP that 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<Action>()` -- `render<View>()` -- `handle<Signal>()` -- `createComponent<Name>()` - -The last two methods also apply to 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 `'*'`, which allows all methods, something 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 the [CSRF vulnerability |nette:vulnerability-protection#Cross-Site Request Forgery CSRF]: - -```php -#[Requires(sameOrigin: true)] -class SecurePresenter extends Nette\Application\UI\Presenter -{ -} -``` - -For `handle<Signal>()` methods, access from the same domain is required automatically. 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, for instance, 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 certain code, like creating a component, to be accessible only for specific actions in the presenter: - -```php -class EditDeletePresenter extends Nette\Application\UI\Presenter -{ - #[Requires(actions: ['add', 'edit'])] - public function createComponentPostForm() - { - } -} -``` - -In the case of 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 inherits `#[Requires]` and configures 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. - -{{maintitle: Nette #[Requires] Attribute: Restrict Access to Presenters}} -{{description: Restrict a presenter or action to POST requests, AJAX calls, the same origin or forward-only access with a single #[Requires] attribute.}} diff --git a/best-practices/en/composer.texy b/best-practices/en/composer.texy deleted file mode 100644 index 0432b9c9bd..0000000000 --- a/best-practices/en/composer.texy +++ /dev/null @@ -1,285 +0,0 @@ -Composer Usage Tips -******************* - -<div class=perex> - -Composer is a tool for dependency management in PHP. It allows you to declare the libraries your project depends on and it will install and update them for you. We will learn: - -- how to install Composer -- how to use it in a new or existing project - -</div> - - -Installation -============ - -Composer is an executable `.phar` file that you download and install as follows. - - -Windows -------- - -Use the official installer [Composer-Setup.exe|https://getcomposer.org/Composer-Setup.exe]. - - -Linux, macOS ------------- - -All you need is 4 commands, which you can copy from [this page |https://getcomposer.org/download/]. - -Furthermore, by copying it into a folder that is in the system's `PATH`, Composer becomes globally accessible: - -```shell -$ mv ./composer.phar ~/bin/composer # or /usr/local/bin/composer -``` - - -Use in Project -============== - -To start using Composer in your project, all you need is a `composer.json` file. This file describes the dependencies of your project and may also contain other metadata. The simplest `composer.json` can look like this: - -```js -{ - "require": { - "nette/database": "^3.0" - } -} -``` - -We're saying here that our application (or library) requires the package `nette/database` (the package name consists of a vendor name and the project's name) and it wants a version that matches the `^3.0` version constraint (i.e., the latest version 3). - -So, with the `composer.json` file in the project root, run: - -```shell -composer update -``` - -Composer will download Nette Database into the `vendor/` directory. It also creates a `composer.lock` file, which contains information about exactly which library versions it installed. - -Composer generates a `vendor/autoload.php` file. You can simply include this file and start using the libraries' classes without any extra work: - -```php -require __DIR__ . '/vendor/autoload.php'; - -$db = new Nette\Database\Connection('sqlite::memory:'); -``` - - -Update Packages to the Latest Versions -====================================== - -To update the used libraries to the latest versions according to the constraints defined in `composer.json`, use the `composer update` command. For example, with the dependency `"nette/database": "^3.0"`, it will install the latest 3.x.x version, but not version 4. - -To update the constraints in the `composer.json` file, for example to `"nette/database": "^4.1"`, allowing the installation of the latest version, use the `composer require nette/database` command. - -To update all used Nette packages, you would need to list them all on the command line, e.g.: - -```shell -composer require nette/application nette/forms latte/latte tracy/tracy ... -``` - -This is impractical. Therefore, use the simple script "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff that will do it for you: - -```shell -php composer-frontline.php -``` - - -Creating a New Project -====================== - -You can create a new Nette project using a single command: - -```shell -composer create-project nette/web-project name-of-the-project -``` - -Replace `name-of-the-project` with the directory name for your project and execute the command. Composer will download the `nette/web-project` repository from GitHub, which already contains a `composer.json` file, and then install the Nette Framework itself. All that remains is to [set directory permissions |nette:troubleshooting#Setting Directory Permissions] for the `temp/` and `log/` directories, and the project should be live. - -If you know which PHP version your project will be hosted on, be sure to [set it |#PHP Version]. - - -PHP Version -=========== - -Composer always installs package versions compatible with the PHP version you are currently using (specifically, the PHP version used on the command line when running Composer). This might not be the same version your web host uses. Therefore, it's crucial to add information about the PHP version on your hosting to the `composer.json` file. Then, only package versions compatible with the host will be installed. - -For example, to specify that the project will run on PHP 8.2.3, use the command: - -```shell -composer config platform.php 8.2.3 -``` - -The version will be written to the `composer.json` file like this: - -```js -{ - "config": { - "platform": { - "php": "8.2.3" - } - } -} -``` - -However, the PHP version number is also specified elsewhere in the file, in the `require` section. While the first number determines the version for which packages are installed, the second number indicates the version the application itself is written for. For example, PhpStorm uses this to set the *PHP language level*. (Of course, it doesn't make sense for these versions to differ, so the double entry is an oversight.) Set this version using the command: - -```shell -composer require php 8.2.3 --no-update -``` - -Or directly in the `composer.json` file: - -```js -{ - "require": { - "php": "8.2.3" - } -} -``` - - -Ignoring PHP Version -==================== - -Packages typically specify both the lowest PHP version they are compatible with and the highest version they have been tested against. If you intend to use an even newer PHP version, perhaps for testing, Composer will refuse to install such a package. The solution is the `--ignore-platform-req=php+` option, which makes Composer ignore the upper limits of the required PHP version. - - -False Reports -============= - -When upgrading packages or changing version numbers, conflicts sometimes occur. One package has requirements that conflict with another, and so on. However, Composer sometimes outputs false reports. It reports a conflict that doesn't actually exist. In such cases, deleting the `composer.lock` file and trying again can help. - -If the error message persists, it is genuine, and you need to read it to understand what to modify and how. - - -Packagist.org - Global Repository -================================= - -[Packagist |https://packagist.org] is the main repository where Composer searches for packages by default. You can also publish your own packages here. - - -What If We Don't Want the Central Repository --------------------------------------------- - -If we have internal applications or libraries within our company that cannot be hosted publicly, we can create our own repositories for them. - -Read more about repositories in [the official documentation |https://getcomposer.org/doc/05-repositories.md#repositories]. - - -Autoloading -=========== - -A key feature of Composer is that it provides autoloading for all the classes it installs. You activate this by including the `vendor/autoload.php` file. - -However, you can also use Composer to load other classes from outside the `vendor/` directory. The first option is to let Composer scan defined directories and subdirectories, find all classes, and include them in the autoloader. To achieve this, set `autoload > classmap` in `composer.json`: - -```js -{ - "autoload": { - "classmap": [ - "src/", # includes the src/ directory and its subdirectories - ] - } -} -``` - -Subsequently, you need to run the `composer dumpautoload` command after each change to regenerate the autoloading tables. This is extremely inconvenient. It's much 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 adhere to [PSR-4 |https://www.php-fig.org/psr/psr-4/]. Simply put, it's a system where namespaces and class names correspond to the directory structure and file names, e.g., `App\Core\RouterFactory` will be located in the file `/path/to/App/Core/RouterFactory.php`. Configuration example: - -```js -{ - "autoload": { - "psr-4": { - "App\\": "app/" # the App\ namespace is in the app/ directory - } - } -} -``` - -See the [Composer documentation |https://getcomposer.org/doc/04-schema.md#psr-4] for details on how to configure this behavior. - - -Testing New Versions -==================== - -Want to test a new development version of a package? Here's how. First, add this pair of options to your `composer.json` file. This allows installing development versions, but Composer will only resort to them if no stable version combination satisfies the requirements: - -```js -{ - "minimum-stability": "dev", - "prefer-stable": true, -} -``` - -We also recommend deleting the `composer.lock` file, as Composer sometimes inexplicably refuses installation, and this can resolve the issue. - -Let's say the package is `nette/utils` and the new version is 4.0. Install it using the command: - -```shell -composer require nette/utils:4.0.x-dev -``` - -Or you can install a specific version, for example, 4.0.0-RC2: - -```shell -composer require nette/utils:4.0.0-RC2 -``` - -However, if another package depends on the library and is locked to an older version (e.g., `^3.1`), the ideal solution is to update that dependent package to work with the new version. But if you just want to bypass the restriction and force Composer to install the development version while pretending it's an older version (e.g., 3.1.6), you can use the `as` keyword: - -```shell -composer require nette/utils "4.0.x-dev as 3.1.6" -``` - - -Calling Commands -================ - -You can call your own predefined commands and scripts via Composer as if they were native Composer commands. For scripts located in the `vendor/bin` directory, you don't need to specify this path. - -As an example, let's define a script in `composer.json` that uses [Nette Tester |tester:] to run tests: - -```js -{ - "scripts": { - "tester": "tester tests -s" - } -} -``` - -We then run the tests using `composer tester`. You can call the command even if you are not in the project's root directory, but in one of its subdirectories. - - -Send Thanks -=========== - -We'll show you a trick to please open source authors. You can easily give stars on GitHub to the libraries your project uses. Simply install the `symfony/thanks` library: - -```shell -composer global require symfony/thanks -``` - -And then run: - -```shell -composer thanks -``` - -Try it! - - -Configuration -============= - -Composer is closely integrated with the version control tool [Git |https://git-scm.com]. If you don't have Git installed, you need to tell Composer not to use it: - -```shell -composer -g config preferred-install dist -``` - -{{maintitle: Composer for PHP: Install It and Manage Dependencies}} -{{description: Install Composer, add libraries to your PHP project, update them and use autoloading. Practical tips on PHP versions, Packagist and false reports.}} diff --git a/best-practices/en/creating-editing-form.texy b/best-practices/en/creating-editing-form.texy deleted file mode 100644 index 24a8f21f9a..0000000000 --- a/best-practices/en/creating-editing-form.texy +++ /dev/null @@ -1,206 +0,0 @@ -Form for Creating and Editing a Record -************************************** - -.[perex] -How to properly implement adding and editing a record in Nette, using the same form for both? - -In many cases, the forms for adding and editing records are identical, perhaps differing only in the button label. We will show examples of simple presenters where we use the form first to add a record, then to edit it, and finally combine the two solutions. - - -Adding a Record ---------------- - -Example of a presenter for adding a record. We'll leave the actual database interaction to a `Facade` class, whose code isn't essential for this example. - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentRecordForm(): Form - { - $form = new Form; - - // ... add form fields ... - - $form->onSuccess[] = $this->recordFormSucceeded(...); - return $form; - } - - private function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // add record to the database - $this->flashMessage('Successfully added'); - $this->redirect('...'); - } - - public function renderAdd(): void - { - // ... - } -} -``` - - -Editing a Record ----------------- - -Now let's look at what a presenter for editing a record would look like: - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - private $record; - - public function __construct( - private Facade $facade, - ) { - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // verify the existence of the record - || !$this->facade->isEditAllowed(/*...*/) // check permissions - ) { - $this->error(); // 404 error - } - - $this->record = $record; - } - - protected function createComponentRecordForm(): Form - { - // verify that the action is 'edit' - if ($this->getAction() !== 'edit') { - $this->error(); - } - - $form = new Form; - - // ... add form fields ... - - $form->setDefaults($this->record); // set default values - $form->onSuccess[] = $this->recordFormSucceeded(...); - return $form; - } - - private function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->update($this->record->id, $data); // update record - $this->flashMessage('Successfully updated'); - $this->redirect('...'); - } -} -``` - -In the *action* method, invoked at the beginning of the [presenter lifecycle |application:presenters#Presenter Life Cycle], we verify the record's existence and the user's permission to edit it. - -We store the record in the `$record` property, making it available in the `createComponentRecordForm()` method for setting default values and in `recordFormSucceeded()` for accessing the ID. An alternative solution is to set the default values directly in `actionEdit()` and retrieve the ID value (part of the URL) using `getParameter('id')`: - - -```php - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - // verify existence and check permissions - ) { - $this->error(); - } - - // set default form values - $this->getComponent('recordForm') - ->setDefaults($record); - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); - // ... - } -``` - -However, and this should be **the most important takeaway from the entire code**, we must ensure the action is indeed `edit` when creating the form. Otherwise, the verification in the `actionEdit()` method would not occur at all! - - -Same Form for Adding and Editing --------------------------------- - -Now, let's combine both presenters into one. We could either differentiate the action within the `createComponentRecordForm()` method and configure the form accordingly, or we can delegate this to the action methods directly and eliminate the conditional check: - - -```php -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - public function actionAdd(): void - { - $form = $this->getComponent('recordForm'); - $form->onSuccess[] = $this->addingFormSucceeded(...); - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // verify the existence of the record - || !$this->facade->isEditAllowed(/*...*/) // check permissions - ) { - $this->error(); // 404 error - } - - $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // set default values - $form->onSuccess[] = $this->editingFormSucceeded(...); - } - - protected function createComponentRecordForm(): Form - { - // verify that the action is 'add' or 'edit' - if (!in_array($this->getAction(), ['add', 'edit'])) { - $this->error(); - } - - $form = new Form; - - // ... add form fields ... - - return $form; - } - - private function addingFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // add record to the database - $this->flashMessage('Successfully added'); - $this->redirect('...'); - } - - private function editingFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // update record - $this->flashMessage('Successfully updated'); - $this->redirect('...'); - } -} -``` - -{{priority: -1}} -{{maintitle: One Form for Adding and Editing a Record in Nette}} -{{description: How to properly implement adding and editing a database record in Nette with one shared form, including default values and form processing.}} diff --git a/best-practices/en/dynamic-snippets.texy b/best-practices/en/dynamic-snippets.texy deleted file mode 100644 index 7eed68941c..0000000000 --- a/best-practices/en/dynamic-snippets.texy +++ /dev/null @@ -1,178 +0,0 @@ -Dynamic Snippets -**************** - -.[perex] -How to use AJAX to refresh only the parts of a page that actually change, such as individual items in a list, using Latte's dynamic snippets. - -Quite often during application development, the need arises to perform AJAX operations, for example, on individual rows of a table or list items. As an example, let's consider listing articles where logged-in users can rate each article with 'like' or 'dislike'. The presenter code and corresponding template without AJAX would look something like this (showing the most relevant parts; the code assumes a service exists for handling ratings and retrieving articles - the specific implementation isn't crucial for this guide): - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - $this->redirect('this'); -} - -public function handleUnlike(int $articleId): void -{ - $this->ratingService->removeLike($articleId, $this->user->id); - $this->redirect('this'); -} -``` - -Template: - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {if !$article->liked} - <a n:href="like!, $article->id" class=ajax>I like it</a> - {else} - <a n:href="unlike!, $article->id" class=ajax>I don't like it anymore</a> - {/if} -</article> -``` - - -Ajaxization -=========== - -Now, let's add AJAX functionality to this simple application. Changing an article's rating isn't critical enough to warrant a full page redirect, so it should ideally happen via AJAX in the background. We'll use the [handler script from add-ons |application:ajax#Naja] with the common convention that AJAX links have the CSS class `ajax`. - -But how exactly do we implement this? Nette offers two approaches: dynamic snippets and components. Both have their pros and cons, so we'll demonstrate each one. - - -The Dynamic Snippets Way -======================== - -In Latte terminology, a dynamic snippet refers to a specific use of the `{snippet}` tag where a variable is used in the snippet's name. Such a snippet cannot be placed just anywhere in the template; it must be wrapped by a static (regular) snippet or be inside a `{snippetArea}`. We could modify our template as follows: - - -```latte -{snippet articlesContainer} - <article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {snippet article-{$article->id}} - {if !$article->liked} - <a n:href="like!, $article->id" class=ajax>I like it</a> - {else} - <a n:href="unlike!, $article->id" class=ajax>I don't like it anymore</a> - {/if} - {/snippet} - </article> -{/snippet} -``` - -Each article now defines a snippet whose name includes the article's ID. All these dynamic snippets are then wrapped together by a static snippet named `articlesContainer`. If we were to omit this outer snippet, Latte would throw an exception. - -All that remains is to add the redrawing logic to the presenter - simply redraw the static wrapper. - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - if ($this->isAjax()) { - $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- not needed - } else { - $this->redirect('this'); - } -} -``` - -Modify the corresponding `handleUnlike()` method similarly, and AJAX is functional! - -However, this solution has a drawback. If we examine the AJAX request more closely, we'll find that while the application appears efficient externally (returning only a single snippet for the specific article), it actually renders *all* snippets on the server side. It places the required snippet into the payload and discards the others (meaning it also unnecessarily retrieved and rendered them). - -To optimize this, we need to intervene where the `$articles` collection is passed to the template (let's say in the `renderDefault()` method). We'll leverage the fact that signal handling occurs before the `render<Something>` methods: - -```php -public function handleLike(int $articleId): void -{ - // ... - if ($this->isAjax()) { - // ... - $this->template->articles = [ - $this->db->table('articles')->get($articleId), - ]; - } else { - // ... -} - -public function renderDefault(): void -{ - if (!isset($this->template->articles)) { - $this->template->articles = $this->db->table('articles'); - } -} -``` - -Now, during signal processing, instead of passing the entire collection of articles, only an array containing the single relevant article is passed to the template - the one we intend to render and send in the payload to the browser. Consequently, the `{foreach}` loop runs only once, and no unnecessary snippets are rendered. - - -Component Way -============= - -A completely different approach avoids dynamic snippets altogether. The trick involves encapsulating the entire logic within a separate component. Instead of the presenter handling the rating, a dedicated `LikeControl` will manage it. The class will look like this (it would also contain `render`, `handleUnlike`, etc. methods): - -```php -class LikeControl extends Nette\Application\UI\Control -{ - public function __construct( - private Article $article, - ) { - } - - public function handleLike(): void - { - $this->ratingService->saveLike($this->article->id, $this->presenter->user->id); - if ($this->presenter->isAjax()) { - $this->redrawControl(); - } else { - $this->presenter->redirect('this'); - } - } -} -``` - -The component's template: - -```latte -{snippet} - {if !$article->liked} - <a n:href="like!" class=ajax>I like it</a> - {else} - <a n:href="unlike!" class=ajax>I don't like it anymore</a> - {/if} -{/snippet} -``` - -Naturally, the view's template will change, and we'll need to add a factory to the presenter. Since we'll create an instance of this component for each article retrieved from the database, we'll use the [Multiplier |application:Multiplier] class to manage their creation. - -```php -protected function createComponentLikeControl() -{ - $articles = $this->db->table('articles'); - return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { - return new LikeControl($articles[$articleId]); - }); -} -``` - -The view's template shrinks to the bare minimum (and is completely free of snippets!): - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {control "likeControl-$article->id"} -</article> -``` - -We're almost finished: the application will now function with AJAX. Here too, optimization is needed because, due to the use of Nette Database, signal processing unnecessarily loads all articles from the database instead of just the relevant one. The advantage, however, is that no unnecessary rendering occurs, as only the specific component instance is rendered. - -{{priority: -1}} -{{maintitle: Nette Dynamic Snippets: Refresh List Items with AJAX}} -{{description: Use AJAX to refresh only what actually changes, such as a single item in a list. Two ways to do it in Nette: Latte dynamic snippets and components.}} diff --git a/best-practices/en/form-reuse.texy b/best-practices/en/form-reuse.texy deleted file mode 100644 index 9ad6e2bff9..0000000000 --- a/best-practices/en/form-reuse.texy +++ /dev/null @@ -1,351 +0,0 @@ -Reusing Forms in Multiple Places -******************************** - -.[perex] -Nette offers several ways to reuse the same form in multiple places without duplicating code. This article will cover various solutions, including those you should avoid. - - -Form Factory -============ - -A fundamental approach to reusing a component in multiple locations is to create a method or class that generates this component. This method is then called from various places in the application. Such a method or class is called a *factory*. Please don't confuse this with the *factory method* design pattern, which describes a specific way of using factories and isn't directly related to this topic. - -As an example, let's create a factory that builds an editing form: - -```php -use Nette\Application\UI\Form; - -class FormFactory -{ - public function createEditForm(): Form - { - $form = new Form; - $form->addText('title', 'Title:'); - // additional form fields are added here - $form->addSubmit('send', 'Save'); - return $form; - } -} -``` - -Now you can use this factory in various parts of your application, such as presenters or components. You do this by [requesting it as a dependency |dependency-injection:passing-dependencies]. First, register the class in the configuration file: - -```neon -services: - - FormFactory -``` - -Then, use it in a presenter: - - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->createEditForm(); - $form->onSuccess[] = function () { - // processing of submitted data - }; - return $form; - } -} -``` - -You can extend the form factory with more methods to create other types of forms as needed by your application. And naturally, we can add a method that creates a basic form without elements, which other methods can then utilize: - -```php -class FormFactory -{ - public function createForm(): Form - { - $form = new Form; - return $form; - } - - public function createEditForm(): Form - { - $form = $this->createForm(); - $form->addText('title', 'Title:'); - // additional form fields are added here - $form->addSubmit('send', 'Save'); - return $form; - } -} -``` - -The `createForm()` method doesn't do much useful yet, but that will change soon. - - -Factory Dependencies -==================== - -Over time, it may become necessary for forms to be multilingual. This means setting a [translator |forms:rendering#Translating] for all forms. To achieve this, modify the `FormFactory` class to accept the `Translator` object as a dependency in its constructor and pass it to the created form: - -```php -use Nette\Localization\Translator; - -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function createForm(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } - - // ... -} -``` - -Since the `createForm()` method is also called by other methods creating specific forms, setting the translator here is sufficient. And we're done. There's no need to modify any presenter or component code, which is excellent. - - -More Factory Classes -==================== - -Alternatively, you can create separate factory classes for each form you intend to use in your application. This approach can enhance code readability and simplify form management. Let the original `FormFactory` create only a basic form with fundamental configuration (like translation support), and create a new factory, `EditFormFactory`, specifically for the editing form. - -```php -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function create(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } -} - - -// ✅ using composition -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - // additional form fields are added here - $form->addSubmit('send', 'Save'); - return $form; - } -} -``` - -It's crucial that the relationship between the `FormFactory` and `EditFormFactory` classes is realized through [composition |nette:introduction-to-object-oriented-programming#Composition], not [object inheritance |nette:introduction-to-object-oriented-programming#Inheritance]: - -```php -// ⛔ NO! INHERITANCE DOESN'T BELONG HERE -class EditFormFactory extends FormFactory -{ - public function create(): Form - { - $form = parent::create(); - $form->addText('title', 'Title:'); - // additional form fields are added here - $form->addSubmit('send', 'Save'); - return $form; - } -} -``` - -Using inheritance here would be entirely counterproductive. You'd encounter problems very quickly. For instance, if you wanted to add parameters to the `create()` method, PHP would report an error because its signature would differ from the parent's. Or when passing dependencies to the `EditFormFactory` class via the constructor. This would lead to what's known as [constructor hell |dependency-injection:passing-dependencies#Constructor Hell]. - -Generally, it's better to prefer [composition over inheritance |dependency-injection:faq#Why is composition preferred over inheritance?]. - - -Form Handling -============= - -The form handler, invoked upon successful submission, can also be part of the factory class. It functions by passing the submitted data to the model layer for processing. Any processing errors are passed [back to |forms:validation#Processing Errors] the form. In the following example, the model is represented by the `Facade` class: - -```php -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - private Facade $facade, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - $form->addText('title', 'Title:'); - // additional form fields are added here - $form->addSubmit('send', 'Save'); - $form->onSuccess[] = $this->processForm(...); - return $form; - } - - private function processForm(Form $form, array $data): void - { - try { - // processing of submitted data - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - } - } -} -``` - -However, let the presenter handle the redirection itself. It adds another handler to the `onSuccess` event, which performs the redirection. This allows the form to be used in various presenters, each redirecting to a different location upon success. - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditFormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->create(); - $form->onSuccess[] = function () { - $this->flashMessage('Record was saved'); - $this->redirect('Homepage:'); - }; - return $form; - } -} -``` - -This solution leverages the characteristic of forms where if `addError()` is called on the form or one of its elements, subsequent `onSuccess` handlers are not invoked. - - -Inheriting from the Form Class -============================== - -An assembled form should not be a descendant of the `Form` class. In other words, avoid this approach: - -```php -// ⛔ NO! INHERITANCE DOESN'T BELONG HERE -class EditForm extends Form -{ - public function __construct(Translator $translator) - { - parent::__construct(); - $this->addText('title', 'Title:'); - // additional form fields are added here - $this->addSubmit('send', 'Save'); - $this->setTranslator($translator); - } -} -``` - -Instead of assembling the form within the constructor, use a factory. - -It's important to recognize that the `Form` class is primarily a tool for building forms, i.e., a *form builder*. The assembled form can be considered its product. However, a product is not a specific type of builder; there's no *is a* relationship between them, which is the foundation of inheritance. - - -Form Component -============== - -A completely different approach involves creating a [component |application:components] that encapsulates the form. This opens up new possibilities, such as rendering the form in a specific way, as the component includes its own template. Alternatively, signals can be used for AJAX communication and dynamically loading information into the form, for example, for suggestions, etc. - - -```php -use Nette\Application\UI\Form; - -class EditControl extends Nette\Application\UI\Control -{ - public array $onSave = []; - - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentForm(): Form - { - $form = new Form; - $form->addText('title', 'Title:'); - // additional form fields are added here - $form->addSubmit('send', 'Save'); - $form->onSuccess[] = $this->processForm(...); - - return $form; - } - - private function processForm(Form $form, array $data): void - { - try { - // processing of submitted data - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - return; - } - - // event invocation - $this->onSave($this, $data); - } -} -``` - -Next, let's create a factory that will produce this component. It's sufficient to [define its interface |application:components#Components with Dependencies]: - -```php -interface EditControlFactory -{ - function create(): EditControl; -} -``` - -And add it to the configuration file: - -```neon -services: - - EditControlFactory -``` - -Now, we can request the factory and use it in the presenter: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditControlFactory $controlFactory, - ) { - } - - protected function createComponentEditForm(): EditControl - { - $control = $this->controlFactory->create(); - - $control->onSave[] = function (EditControl $control, $data) { - $this->redirect('this'); - // or redirect to the edit result, e.g.: - // $this->redirect('detail', ['id' => $data->id]); - }; - - return $control; - } -} -``` - -{{maintitle: Reusing Forms in Nette: Factories and Form Components}} -{{description: Use the same form in multiple places without duplicating code: form factories and form components, plus solutions such as inheritance that you should avoid.}} diff --git a/best-practices/en/inject-method-attribute.texy b/best-practices/en/inject-method-attribute.texy deleted file mode 100644 index 6909d824d0..0000000000 --- a/best-practices/en/inject-method-attribute.texy +++ /dev/null @@ -1,64 +0,0 @@ -Inject Methods and Attributes -***************************** - -.[perex] -This article focuses on various ways to pass dependencies into presenters within the Nette framework. We'll compare the preferred method, constructor injection, with alternatives like `inject` methods and attributes. - -For presenters, as with other classes, passing dependencies via the [constructor |dependency-injection:passing-dependencies#Constructor Injection] is the preferred approach. However, if you create a common ancestor from which other presenters inherit (e.g., `BasePresenter`), and this ancestor also requires dependencies, a problem known as [constructor hell |dependency-injection:passing-dependencies#Constructor Hell] can arise. This can be circumvented using alternative methods, namely inject methods and attributes (formerly annotations). - - -`inject*()` Methods -=================== - -This is a form of dependency passing via [setters |dependency-injection:passing-dependencies#Setter Injection]. The names of these setters must start with the prefix `inject`. Nette DI automatically calls methods named this way immediately after creating the presenter instance, passing all required dependencies. Therefore, they must be declared as public. - -`inject*()` methods can be seen as extensions of the constructor, split into multiple methods. This allows `BasePresenter` to receive its dependencies via a separate method, leaving the constructor free for its descendants: - -```php -abstract class BasePresenter extends Nette\Application\UI\Presenter -{ - private Foo $foo; - - public function injectBase(Foo $foo): void - { - $this->foo = $foo; - } -} - -class MyPresenter extends BasePresenter -{ - private Bar $bar; - - public function __construct(Bar $bar) - { - $this->bar = $bar; - } -} -``` - -A presenter can have any number of `inject*()` methods, and each can accept any number of parameters. This approach is also well-suited for cases where a presenter is [composed of traits |presenter-traits], and each trait requires its own dependencies. - - -`Inject` Attributes -=================== - -This is a form of [injecting into properties |dependency-injection:passing-dependencies#Property Injection]. Simply mark the properties that should be injected, and Nette DI will automatically pass the dependencies immediately after creating the presenter instance. To allow injection, these properties must be declared as public. - -Properties are marked with an attribute (previously, the `/** @inject */` annotation was used): - -```php -use Nette\DI\Attributes\Inject; // this line is important - -class MyPresenter extends Nette\Application\UI\Presenter -{ - #[Inject] - public Cache $cache; -} -``` - -The advantage of this dependency passing method was its very concise syntax. However, with the introduction of [constructor property promotion |https://blog.nette.org/en/php-8-0-complete-overview-of-news#toc-constructor-property-promotion], using the constructor often appears simpler. - -Conversely, this method suffers from the same drawbacks as property injection in general: we lack control over changes to the variable, and the variable becomes part of the class's public interface, which is generally undesirable. - -{{maintitle: Inject Methods and Attributes in Nette Presenters}} -{{description: Ways to pass dependencies into Nette presenters: the preferred constructor injection compared with inject methods and the Inject attribute.}} diff --git a/best-practices/en/lets-create-contact-form.texy b/best-practices/en/lets-create-contact-form.texy deleted file mode 100644 index 11ce861d6e..0000000000 --- a/best-practices/en/lets-create-contact-form.texy +++ /dev/null @@ -1,221 +0,0 @@ -Let's Create a Contact Form -*************************** - -.[perex] -Let's explore how to create a contact form in Nette, including sending the submitted data via email. Let's get started! - -First, we need to create a new project. The [Getting Started |nette:installation] page explains how. Then, we can begin creating the form. - -The simplest approach is to create the [form directly within the Presenter |forms:in-presenter]. We can utilize the pre-existing `HomePresenter`. We'll add a component named `contactForm` to represent our form. We achieve this by adding a factory method `createComponentContactForm()` to the presenter's code, which will create the component: - -```php -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - protected function createComponentContactForm(): Form - { - $form = new Form; - $form->addText('name', 'Name:') - ->setRequired('Please enter your name'); - $form->addEmail('email', 'E-mail:') - ->setRequired('Please enter your email'); - $form->addTextArea('message', 'Message:') - ->setRequired('Please enter a message'); - $form->addSubmit('send', 'Send'); - $form->onSuccess[] = $this->contactFormSucceeded(...); - return $form; - } - - private function contactFormSucceeded(Form $form, $data): void - { - // sending an email - } -} -``` - -As you can see, we've created two methods. The first method, `createComponentContactForm()`, creates a new form instance. It includes fields for name, email, and message, added using the `addText()`, `addEmail()`, and `addTextArea()` methods respectively. We've also added a submit button. But what if the user leaves a field empty? In that case, we should inform them that the field is required. We achieved this using the `setRequired()` method. Finally, we attached an [event |nette:glossary#Events] handler to `onSuccess`, which is triggered upon successful form submission. In our case, it calls the `contactFormSucceeded` method, which will handle the processing of the submitted data. We'll implement this method shortly. - -Let's render the `contactForm` component in the `Home/default.latte` template: - -```latte -{block content} -<h1>Contact Form</h1> -{control contactForm} -``` - -For sending the email itself, we'll create a new class named `ContactFacade` and place it in the file `app/Model/ContactFacade.php`: - -```php -namespace App\Model; - -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $mail = new Message; - $mail->addTo('admin@example.com') // your email - ->setFrom($email, $name) - ->setSubject('Message from the contact form') - ->setBody($message); - - $this->mailer->send($mail); - } -} -``` - -The `sendMessage()` method creates and sends the email. It utilizes a mailer service for this, which it receives as a dependency via the constructor. Read more about [sending emails |mail:]. - -Now, let's return to the presenter and complete the `contactFormSucceeded()` method. It will call the `sendMessage()` method of the `ContactFacade` class, passing the data submitted through the form. And how do we obtain the `ContactFacade` object? We'll request it via the constructor using dependency injection: - -```php -use App\Model\ContactFacade; -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - public function __construct( - private ContactFacade $facade, - ) { - } - - protected function createComponentContactForm(): Form - { - // ... - } - - public function contactFormSucceeded(stdClass $data): void - { - $this->facade->sendMessage($data->email, $data->name, $data->message); - $this->flashMessage('Message has been sent'); - $this->redirect('this'); - } -} -``` - -After the email is sent, we display a [flash message |application:components#Flash Messages] to the user confirming the submission. Then, we redirect to prevent the form from being resubmitted via browser refresh. - - -So, if everything is set up correctly, you should now be able to send an email from your contact form. Congratulations! - - -HTML Email Template -------------------- - -Currently, a plain text email containing only the message submitted via the form is sent. However, we can use HTML in the email to make its appearance more appealing. We'll create a template for it in Latte and save it as `app/Model/contactEmail.latte`: - -```latte -<html> - <title>Message from the contact form - - -

    Name: {$name}

    -

    E-mail: {$email}

    -

    Message: {$message}

    - - -``` - -It remains to modify `ContactFacade` to use this template. In the constructor, we'll request the `LatteFactory` class, which can create a `Latte\Engine` object, the [Latte template renderer |latte:develop#How to Render a Template]. Using the `renderToString()` method, we render the template into a string. The first parameter is the path to the template file, and the second is an array of variables to pass to it. - -```php -namespace App\Model; - -use Nette\Bridges\ApplicationLatte\LatteFactory; -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $latte = $this->latteFactory->create(); - $body = $latte->renderToString(__DIR__ . '/contactEmail.latte', [ - 'email' => $email, - 'name' => $name, - 'message' => $message, - ]); - - $mail = new Message; - $mail->addTo('admin@example.com') // your email - ->setFrom($email, $name) - ->setHtmlBody($body); - - $this->mailer->send($mail); - } -} -``` - -We then pass the generated HTML email content to the `setHtmlBody()` method instead of the original `setBody()`. We also don't need to specify the email subject using `setSubject()`, as the library automatically extracts it from the `` element within the template. - - -Configuring ------------ - -In the `ContactFacade` class code, our administrator email `admin@example.com` is still hardcoded. It would be better to move this into the configuration file. How can we do that? - -First, modify the `ContactFacade` class, replacing the hardcoded email string with a variable passed through the constructor: - -```php -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - private string $adminEmail, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - // ... - $mail = new Message; - $mail->addTo($this->adminEmail) - ->setFrom($email, $name) - ->setHtmlBody($body); - // ... - } -} -``` - -The second step is to provide the value for this variable in the configuration. In the `app/config/services.neon` file, add: - -```neon -services: - - App\Model\ContactFacade(adminEmail: admin@example.com) -``` - -And that's it. If the `services` section contains many items and the email address gets lost among them, we can turn it into a parameter. Modify the entry like this: - -```neon -services: - - App\Model\ContactFacade(adminEmail: %adminEmail%) -``` - -And define this parameter in the `app/config/common.neon` file: - -```neon -parameters: - adminEmail: admin@example.com -``` - -And it's done! - -{{maintitle: Contact Form in PHP with Nette: Send Data by Email}} -{{description: Build a contact form in Nette step by step, from the form itself to sending the submitted data by email using a Latte template.}} diff --git a/best-practices/en/microsites.texy b/best-practices/en/microsites.texy deleted file mode 100644 index c80c4a2893..0000000000 --- a/best-practices/en/microsites.texy +++ /dev/null @@ -1,66 +0,0 @@ -How to Write Microsites -*********************** - -Imagine needing to quickly create a small website for your company's upcoming event. It needs to be simple, fast, and without unnecessary complications. You might think a robust framework isn't necessary for such a small project. But what if using the Nette Framework could actually simplify and accelerate this process? - -Even when creating simple websites, you don't want to sacrifice convenience. You don't want to reinvent what has already been solved. Feel free to be lazy and let yourself be pampered. The Nette Framework is also excellent for use as a micro-framework. - -What might such a microsite look like? For instance, the entire website code could reside in a single `index.php` file within the public directory: - -```php -<?php - -require __DIR__ . '/../vendor/autoload.php'; - -$configurator = new Nette\Bootstrap\Configurator; -$configurator->enableTracy(__DIR__ . '/../log'); -$configurator->setTempDirectory(__DIR__ . '/../temp'); - -// create DI container based on 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); - return $presenter->redirectUrl("/$lang"); -}); - -// route for URL https://example.com/cs or https://example.com/en -$router->addRoute('<lang cs|en|de>', 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` directory. - -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, which gets executed when the corresponding page is accessed. - - -Why use Nette for Microsites? ------------------------------ - -- Programmers who have tried [Tracy|tracy:] can hardly imagine coding without it today. -- Above all, you'll benefit from the [Latte|latte:] templating system, because even with just two pages, you'll want to separate the [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, raw PHP error messages are never displayed; instead, a user-friendly page is shown. -- If you want to gather user feedback, perhaps through a contact form, you can easily add [forms|forms:] and [database|database:] support. -- You can also easily have completed forms [sent via email|mail:]. -- Sometimes, [caching|caching:] might be useful, for example, when downloading and displaying feeds. - -In today's fast-paced world, where speed and efficiency are crucial, having tools that enable you to achieve results without unnecessary delays is vital. The Nette Framework offers precisely that - rapid development, security, and a wide array of tools like Tracy and Latte that streamline the process. Just install a few Nette packages, and building such a microsite becomes incredibly easy. And you can be confident there are no hidden security vulnerabilities. - -{{maintitle: Nette Microsites: A Small Website in One PHP File}} -{{description: Need a small website fast? Keep all its code in a single index.php with Nette routing and Latte, and still get Tracy, safe templates and other conveniences.}} diff --git a/best-practices/en/pagination.texy b/best-practices/en/pagination.texy deleted file mode 100644 index 82cab1ede2..0000000000 --- a/best-practices/en/pagination.texy +++ /dev/null @@ -1,275 +0,0 @@ -Paginating Database Results -*************************** - -.[perex] -When developing web applications, you often encounter the requirement to limit the number of listed items per page, a technique known as pagination. - -Let's start from a state where we list all data without pagination. For selecting data from the database, we have an `ArticleRepository` class. Besides the constructor, it contains a `findPublishedArticles` method that returns all published articles sorted descending by publication date. - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC', - new \DateTime, - ); - } -} -``` - -In the presenter, we then inject this model class. In the render method, we retrieve the published articles and pass them to the template: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(): void - { - $this->template->articles = $this->articleRepository->findPublishedArticles(); - } -} -``` - -The `default.latte` template will then take care of listing the articles: - -```latte -{block content} -<h1>Articles</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> -``` - - -This way, we can list all articles, but this becomes problematic as the number of articles increases. At that point, implementing a pagination mechanism becomes useful. - -This mechanism divides all articles into several pages, and we only display the articles belonging to the currently selected page. The total number of pages and the division of articles are calculated by the [Paginator |utils:Paginator] utility based on the total number of articles and the desired number of articles per page. - -In the first step, we'll modify the article retrieval method in the repository class so it can return articles for just one page. We'll also add a method to get the total count of articles in the database, which is needed to configure the Paginator: - -```php -namespace App\Model; - -use Nette; - - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(int $limit, int $offset): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC - LIMIT ? - OFFSET ?', - new \DateTime, $limit, $offset, - ); - } - - /** - * Returns the total number of published articles - */ - public function getPublishedArticlesCount(): int - { - return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime); - } -} -``` - -Next, let's modify the presenter. We'll pass the current page number to the `renderDefault` method. If this number isn't part of the URL, we'll set a default value of 1 (the first page). - -We'll also extend the render method to create and configure a Paginator instance and select the appropriate articles for display in the template. The modified `HomePresenter` will look like this: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Get the total number of published articles - $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - - // Create and configure the Paginator instance - $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // total item count - $paginator->setItemsPerPage(10); // items per page - $paginator->setPage($page); // current page number - - // Fetch a limited set of articles from the database based on Paginator's calculation - $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - - // pass them to the template - $this->template->articles = $articles; - // and also the Paginator itself for displaying pagination controls - $this->template->paginator = $paginator; - } -} -``` - -The template now iterates only over the articles for the current page. We just need to add the pagination links: - -```latte -{block content} -<h1>Articles</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if !$paginator->isFirst()} - <a n:href="default, 1">First</a> -  |  - <a n:href="default, $paginator->getPage() - 1">Previous</a> -  |  - {/if} - - Page {$paginator->getPage()} of {$paginator->getPageCount()} - - {if !$paginator->isLast()} -  |  - <a n:href="default, $paginator->getPage() + 1">Next</a> -  |  - <a n:href="default, $paginator->getPageCount()">Last</a> - {/if} -</div> -``` - - -This completes the pagination implementation using the Paginator. If you use [Nette Database Explorer |database:explorer] instead of [Nette Database Core |database:sql-way] as your database layer, you can implement pagination even without using the Paginator utility directly. The `Nette\Database\Table\Selection` class includes a [page() |api:Nette\Database\Table\Selection::page()] method that encapsulates the pagination logic. - -With this approach, the repository will look like this: - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Explorer $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\Table\Selection - { - return $this->database->table('articles') - ->where('created_at < ', new \DateTime) - ->order('created_at DESC'); - } -} -``` - -In the presenter, we don't need to create a Paginator instance. Instead, we'll use the `page()` method provided by the `Selection` object returned from the repository: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Fetch the published articles - $articles = $this->articleRepository->findPublishedArticles(); - - // and pass only their portion limited by the page method calculation to the template - $lastPage = 0; - $this->template->articles = $articles->page($page, 10, $lastPage); - - // and also the necessary data for displaying pagination options - $this->template->page = $page; - $this->template->lastPage = $lastPage; - } -} -``` - -Since we are no longer passing the Paginator object to the template, we need to adjust the part that displays the pagination links: - -```latte -{block content} -<h1>Articles</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if $page > 1} - <a n:href="default, 1">First</a> -  |  - <a n:href="default, $page - 1">Previous</a> -  |  - {/if} - - Page {$page} of {$lastPage} - - {if $page < $lastPage} -  |  - <a n:href="default, $page + 1">Next</a> -  |  - <a n:href="default, $lastPage">Last</a> - {/if} -</div> -``` - -This way, we've implemented the pagination mechanism without explicitly using the Paginator utility. - -{{priority: -1}} -{{maintitle: PHP Database Pagination with Nette: Paginator and Explorer}} -{{description: List database results page by page in Nette: a repository method, the Paginator class, links in the template and a simpler way with Nette Database Explorer.}} diff --git a/best-practices/en/passing-settings-to-presenters.texy b/best-practices/en/passing-settings-to-presenters.texy deleted file mode 100644 index 4432186ff7..0000000000 --- a/best-practices/en/passing-settings-to-presenters.texy +++ /dev/null @@ -1,52 +0,0 @@ -Passing Settings to Presenters -****************************** - -.[perex] -Do you need to pass non-object arguments to presenters (like a flag indicating debug mode, directory paths, etc.) which cannot be automatically passed via autowiring? The solution is to encapsulate them within a dedicated `Settings` object. - -The `Settings` service provides a very simple yet effective way to supply information about the running application to presenters. Its specific structure depends entirely on your particular needs. Example: - -```php -namespace App; - -class Settings -{ - public function __construct( - // since PHP 8.1, readonly can be used - public bool $debugMode, - public string $appDir, - // and so on - ) {} -} -``` - -Example of registering it in the configuration: - -```neon -services: - - App\Settings( - %debugMode%, - %appDir%, - ) -``` - -When a presenter requires the information provided by this service, it simply requests it in its constructor: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private App\Settings $settings, - ) {} - - public function renderDefault() - { - if ($this->settings->debugMode) { - // ... - } - } -} -``` - -{{maintitle: Passing Settings to Nette Presenters: Settings Object}} -{{description: Need to pass non-object values such as debug mode or directory paths to presenters? Autowiring can't do it, so wrap them in a dedicated Settings object.}} diff --git a/best-practices/en/post-links.texy b/best-practices/en/post-links.texy deleted file mode 100644 index 04d886f05a..0000000000 --- a/best-practices/en/post-links.texy +++ /dev/null @@ -1,59 +0,0 @@ -How to Properly Use POST Links -****************************** - -.[perex] -In web applications, particularly in administrative interfaces, a fundamental rule should be that actions modifying server state are not performed using the HTTP GET method. As the name implies, GET should only be used for retrieving data, not altering it. For actions like deleting records, using the POST method is more appropriate. While the DELETE method would be ideal, it cannot be invoked without JavaScript, which is why POST has historically been used for such actions. - -How to implement this in practice? Use this simple trick. At the beginning of your layout template, create a helper form with the ID `postForm`. You will then use this form for actions like delete buttons: - -```latte .{file:@layout.latte} -<form method="post" id="postForm"></form> -``` - -Thanks to this form, instead of a standard `<a>` link, you can use a `<button>`. This button can be styled to look like a regular link. For example, the Bootstrap CSS framework provides the `btn btn-link` classes, making the button visually indistinguishable from other links. Using the `form="postForm"` attribute, link the button to the prepared helper form: - -```latte .{file:admin.latte} -<table> - <tr n:foreach="$posts as $post"> - <td>{$post->title}</td> - <td> - <button class="btn btn-link" form="postForm" formaction="{link delete, $post->id}">delete</button> - <!-- instead of <a n:href="delete, $post->id">delete</a> --> - </td> - </tr> -</table> -``` - -Clicking this button now invokes the `delete` action. To ensure requests are accepted only via the POST method and originate from the same domain (an effective defense against CSRF attacks), use the `#[Requires]` attribute: - -```php .{file:AdminPresenter.php} -use Nette\Application\Attributes\Requires; - -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST', sameOrigin: true)] - public function actionDelete(int $id): void - { - $this->facade->deletePost($id); // hypothetical code for deleting a record - $this->redirect('default'); - } -} -``` - -This attribute is available since Nette Application 3.2. You can learn more about its capabilities on the [How to Use the #Requires Attribute |attribute-requires] page. - -If you were using the `handleDelete()` signal instead of the `actionDelete()` action, specifying `sameOrigin: true` is unnecessary, as signals have this protection enabled by default: - -```php .{file:AdminPresenter.php} -#[Requires(methods: 'POST')] -public function handleDelete(int $id): void -{ - $this->facade->deletePost($id); - $this->redirect('this'); -} -``` - -This approach not only enhances your application's security but also promotes adherence to proper web standards and practices. Using POST methods for state-changing actions results in a more robust and secure application. - -{{maintitle: POST Links in Nette: Never Change State with GET}} -{{description: Actions that change server state must not use GET. How to create POST links in Nette with Latte and secure them with the #[Requires] attribute.}} diff --git a/best-practices/en/presenter-traits.texy b/best-practices/en/presenter-traits.texy deleted file mode 100644 index fb79690558..0000000000 --- a/best-practices/en/presenter-traits.texy +++ /dev/null @@ -1,50 +0,0 @@ -Composing Presenters from Traits -******************************** - -.[perex] -If you need to implement the same functionality in multiple presenters (e.g., verifying user login), placing the code in a common ancestor is a common approach. Another option is to create single-purpose [traits |nette:introduction-to-object-oriented-programming#Traits]. - -The advantage of using traits is that each presenter can incorporate only the traits it actually needs, especially since multiple inheritance is not supported in PHP. - -These traits can leverage the fact that all [inject methods |inject-method-attribute#inject Methods] are called sequentially when the presenter instance is created. You just need to ensure that the name of each inject method is unique across all used traits and the presenter itself. - -Traits can attach initialization code to the [onStartup or onRender |application:presenters#Events] events. - -Examples: - -```php -trait RequireLoggedUser -{ - public function injectRequireLoggedUser(): void - { - $this->onStartup[] = function () { - if (!$this->getUser()->isLoggedIn()) { - $this->redirect('Sign:in', $this->storeRequest()); - } - }; - } -} - -trait StandardTemplateFilters -{ - public function injectStandardTemplateFilters(TemplateBuilder $builder): void - { - $this->onRender[] = function () use ($builder) { - $builder->setupTemplate($this->template); - }; - } -} -``` - -The presenter then simply uses these traits: - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - use StandardTemplateFilters; - use RequireLoggedUser; -} -``` - -{{maintitle: Composing Nette Presenters from Traits}} -{{description: Share code such as a login check across presenters with single-purpose PHP traits instead of a common parent class, including how traits hook into startup.}} diff --git a/best-practices/en/pretty-urls.texy b/best-practices/en/pretty-urls.texy deleted file mode 100644 index 7d37ef557e..0000000000 --- a/best-practices/en/pretty-urls.texy +++ /dev/null @@ -1,207 +0,0 @@ -Pretty URLs with Slugs -********************** - -.[perex] -URLs like `/article/123-how-to-bake-bread` look better than `/article/123` and help both users and search engines understand what's on the page. This guide shows how to generate them entirely in the router - without touching a single template - and how to make sure every visitor lands on the canonical URL. - - -Why Slugs in URLs -================= - -Compare these two addresses: - -``` -/article/123 -/article/123-how-to-bake-bread -``` - -The second one tells the user (and Google) what awaits after the click. It's good for SEO, makes links readable in chat or e-mail, and gives the URL bar some meaning. - -The slug isn't a real identifier, though. The page is determined by the ID. The slug is decoration that the application generates from the title. If the title changes, the slug should change too. And if someone hand-edits the URL or follows an old link, the application should still find the right page. - - -The Goal -======== - -We want a route that handles all of these: - -``` -/article/123 → opens article 123, redirects to canonical URL -/article/123-how-to-bake-bread → opens article 123 directly -/article/123-anything-someone-typed → opens article 123, redirects to canonical URL -/article/ → 404 (no ID) -``` - -And we want every `n:href` and `link()` call across the application to automatically produce `/article/123-how-to-bake-bread` - **without rewriting a single template**. - - -The Route Mask -============== - -The trick is to mark the slug as **optional** in the mask using square brackets: - -```php -$router->addRoute('article/<id [0-9]+>[-<slug>]', 'Article:detail'); -``` - -The mask `[-<slug>]` says: there may be a hyphen and a slug after the ID, but it's not required. The route accepts both `/article/123` and `/article/123-anything`. - -A note on the parameter `<slug>`: by default it matches any characters **except a slash** - exactly what we want. If you write `<slug .+>`, the parameter will match slashes too, so `/article/123-something/else` would parse as a single slug containing `/`. Stay with the default `<slug>` unless you really need that. - -So far the URL is parsed correctly, but generated links won't contain the slug. The next step is to teach the route how to fill the slug in. - - -Generating the Slug Without Touching Templates -============================================== - -This is the killer variant. Existing `n:href="Article:detail, $id"` calls keep working unchanged across the whole application - the router looks the title up by itself. - -We do this with a **general filter** under the empty-string key - it sees all parameters at once and can add the slug: - -```php -use Nette\Routing\Route; -use Nette\Utils\Strings; - -$router->addRoute('article/<id [0-9]+>[-<slug>]', [ - 'presenter' => 'Article', - 'action' => 'detail', - '' => [ - Route::FilterOut => function (array $params) use ($slugProvider): array { - if (isset($params['id']) && empty($params['slug'])) { - $params['slug'] = $slugProvider->getSlug((int) $params['id']); - } - return $params; - }, - ], -]); -``` - -`FilterOut` runs every time the router **generates** a URL. If the slug wasn't passed in, the filter looks the title up and adds it. - -You can deploy slugs across a whole application in a single change - just one route definition. Every link in every template starts producing `/article/123-how-to-bake-bread` automatically. No grep, no template hunt, no missed corner case. - - -Cache the Lookup -================ - -One link generates one DB query, but a typical page has many - listings, breadcrumbs, "last viewed", related articles. The same article ID often appears in several links during a single request, and you don't want to hit the database every time. - -A tiny per-request cache solves this. Wrap the DB call in a small service: - -```php -final class SlugProvider -{ - /** @var array<int, string> */ - private array $cache = []; - - public function __construct( - private Nette\Database\Explorer $db, - ) { - } - - public function getSlug(int $id): string - { - return $this->cache[$id] ??= Strings::webalize(Strings::truncate( - (string) $this->db->fetchField('SELECT title FROM article WHERE id = ?', $id), - 100, '' - )); - } -} -``` - -That's enough - one DB hit per unique ID per request. - - -Passing the Title from the Template (Optional Fast Path) -======================================================== - -When the title is already at hand in the template, you can skip the DB lookup entirely. Pass the title as a named parameter: - -```latte -<a n:href="Article:detail, $article->id, slug => $article->title">{$article->title}</a> -``` - -…and add a per-parameter `FilterOut` that turns the title into a URL-safe string: - -```php -$router->addRoute('article/<id [0-9]+>[-<slug>]', [ - 'presenter' => 'Article', - 'action' => 'detail', - 'slug' => [ - Route::FilterOut => fn($title) => Strings::webalize(Strings::truncate($title, 100, '')), - ], - '' => [/* the lookup-fallback from above */], -]); -``` - -The two filters cooperate. The general filter runs first; seeing the slug already filled with the supplied title, it skips the DB lookup. The per-parameter `FilterOut` then turns that title into a proper slug. Templates that don't pass the title still work - the general filter finds the slug empty and goes through the lookup path. - -Use this only where it matters (large listings rendered hundreds of times per request). For most of the application the cached lookup is fast enough. - - -Canonization: Redirect to the Right URL -======================================= - -We can now generate `/article/123-how-to-bake-bread`, but the route still accepts `/article/123` and `/article/123-anything-someone-wrote`. That's deliberate - we want short URLs (more on that below) and we want old or hand-typed links to keep working. But we don't want search engines to index the same article under multiple addresses. - -The solution is [canonization |application:presenters#canonization]: when the user arrives via a non-canonical URL, the application 301-redirects them to the correct one. The `canonicalize()` method handles this: - -```php -public function actionDetail(int $id, ?string $slug = null): void -{ - $article = $this->facade->getArticle($id); - if (!$article) { - $this->error(); - } - - // generates the canonical URL through the same FilterOut - // and redirects with HTTP 301 if it differs from the current URL - $this->canonicalize('detail', ['id' => $id]); - - $this->template->article = $article; -} -``` - -`canonicalize()` generates the canonical URL the same way `link()` would (so it runs through the same `FilterOut`) and compares it to the current URL. If they differ, it redirects with HTTP 301. Visitors land on the right URL, search engines see only one canonical version. - - -One Place That Decides What the Slug Looks Like -=============================================== - -Notice that the `Strings::webalize(Strings::truncate(..., 100, ''))` call lives in a single place - inside `SlugProvider` (or the per-parameter `FilterOut`). The same logic produces the link in the template, the URL in `redirect()`, and the canonical form in `canonicalize()`. - -If you want to change the rules later (different length limit, different transliteration, stripping extra characters), you change one line. Without this, you'd risk `redirect()` generating `/article/123-how-to-bake-bread` while `canonicalize()` expects `/article/123-how-to-bake-bre` (because someone applied a different `truncate` length elsewhere), and the application would redirect in a loop. - - -Bonus: Short URLs Still Work -============================ - -Because the slug is optional, addresses without it still work: - -``` -/article/123 -``` - -This is useful for: -- **QR codes** - shorter URL means a less dense, more scannable code -- **SMS and chat** - fits in a tweet, looks tidy -- **Printed materials** - a short URL is faster to type - -When a user opens such a URL, `canonicalize()` 301-redirects them to the full version with the slug, so search engines still see only the canonical form. You can have shortness and SEO at the same time. - - -Summary -======= - -- Mask `<id>[-<slug>]` makes the slug optional. The default `<slug>` doesn't match `/`; use `<slug .+>` only if you really want slashes in the slug. -- A general `FilterOut` under the `''` key looks the title up by ID - **no template changes anywhere in the application**. -- Wrap the lookup in a tiny per-request cache; one DB query per unique ID is plenty. -- Optionally, a per-parameter `FilterOut` lets templates pass the title directly and skip the lookup. -- `$this->canonicalize()` in the action redirects non-canonical URLs to the right one with HTTP 301. -- The slug formula (`webalize` + `truncate`) lives in one place - change it once and it takes effect everywhere. -- Short ID-only URLs keep working, which is handy for QR codes and SMS. - -You'll find more about filters and canonization in the [routing |application:routing#general-filters] and [presenters |application:presenters#canonization] documentation. - -{{maintitle: Pretty URLs with Slugs in Nette: SEO-Friendly PHP Routing}} -{{description: Turn /article/123 into /article/123-how-to-bake-bread right in the router, without touching a single template, and redirect old URLs to the correct one.}} diff --git a/best-practices/en/restore-request.texy b/best-practices/en/restore-request.texy deleted file mode 100644 index 4af0670e96..0000000000 --- a/best-practices/en/restore-request.texy +++ /dev/null @@ -1,64 +0,0 @@ -How to Return to an Earlier Page? -********************************* - -.[perex] -What happens if a user is filling out a form and their login session expires? To prevent data loss, we can save the current request (including form data) to the session before redirecting to the login page. In Nette, this is surprisingly easy. - -The current request can be stored in the session using the `storeRequest()` method. This method returns a unique identifier (a short string) for the stored request. The method saves the name of the current presenter, its view, and its parameters. If a form was submitted as part of the request, the values entered into the fields (excluding uploaded files) are also saved. - -The request is restored using the `restoreRequest($key)` method, to which you pass the previously obtained identifier. This method redirects the user back to the original presenter and view. However, if the stored request included a form submission, `restoreRequest()` uses the `forward()` method instead of redirecting. It passes the previously filled values back to the form and allows it to be rendered again. This allows the user to resubmit the form without losing any entered data. - -Crucially, `restoreRequest()` verifies that the newly logged-in user is the same user who originally submitted the form. If the user is different, the stored request is not restored, and the method does nothing, enhancing security. - -Let's illustrate this with an example. Consider an `AdminPresenter` where data is edited. Its `startup()` method verifies if the user is logged in. If not, the user is redirected to `SignPresenter`. Simultaneously, we store the current request using `storeRequest()` and pass its key (the `$backlink`) to `SignPresenter`. - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - protected function startup() - { - parent::startup(); - - if (!$this->user->isLoggedIn()) { - $this->redirect('Sign:in', ['backlink' => $this->storeRequest()]); - } - } -} -``` - -The `SignPresenter` will contain, in addition to the login form, a persistent parameter `$backlink` where the key is stored. Because the parameter is persistent, its value is retained even after the login form is submitted. - - -```php -use Nette\Application\Attributes\Persistent; - -class SignPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $backlink = ''; - - protected function createComponentSignInForm() - { - $form = new Nette\Application\UI\Form; - // ... add form fields ... - $form->onSuccess[] = $this->signInFormSuceeded(...); - return $form; - } - - private function signInFormSuceeded($form) - { - // ... log the user in here ... - - $this->restoreRequest($this->backlink); - $this->redirect('Admin:'); - } -} -``` - -We pass the key (`$this->backlink`) of the stored request to the `restoreRequest()` method. It then redirects (or forwards) the user back to the original presenter and view. - -However, if the key is invalid (e.g., it has expired from the session), the method does nothing. Therefore, the subsequent call `$this->redirect('Admin:')` acts as a fallback, redirecting to a default page like `AdminPresenter`. - -{{priority: -1}} -{{maintitle: Return to an Earlier Page in Nette: Restore the Request}} -{{description: A user's login expires while filling out a form? Save the current request to the session before redirecting to sign-in and restore it with no data lost.}} diff --git a/best-practices/es/@home.texy b/best-practices/es/@home.texy deleted file mode 100644 index 5300606b2c..0000000000 --- a/best-practices/es/@home.texy +++ /dev/null @@ -1,69 +0,0 @@ -Tutoriales y buenas prácticas -***************************** - -.[perex] -Tutoriales, soluciones a tareas habituales y buenas prácticas para Nette. - - -<div class=documentation> -<div> - - -Nette Application ------------------ -- [Métodos y atributos inject |inject-method-attribute] -- [Componer presenters a partir de traits |presenter-traits] -- [Pasar ajustes a los presenters |passing-settings-to-presenters] -- [Cómo volver a una página anterior |restore-request] -- [Paginación de resultados de la base de datos |pagination] -- [Snippets dinámicos |dynamic-snippets] -- [Cómo usar el atributo #Requires |attribute-requires] -- [Cómo usar correctamente los enlaces POST |post-links] -- [URLs amigables con slugs |pretty-urls] - -</div> -<div> - - -Formularios ------------ -- [Reutilización de formularios |form-reuse] -- [Formulario para crear y editar registros |creating-editing-form] -- [Creemos un formulario de contacto |lets-create-contact-form] -- [Selectboxes dependientes |https://blog.nette.org/es/dependent-selectboxes-elegantly-in-nette-and-pure-js] - -</div> -<div> - - -General -------- -- [Cómo cargar un archivo de configuración |bootstrap:] -- [Cómo escribir microsites |microsites] -- [¿Por qué Nette usa la notación PascalCase para las constantes? |https://blog.nette.org/es/for-less-screaming-in-the-code] -- [¿Por qué Nette no usa el sufijo Interface? |https://blog.nette.org/es/prefixes-and-suffixes-do-not-belong-in-interface-names] -- [Composer: consejos de uso |composer] -- [Introducción a la programación orientada a objetos |nette:introduction-to-object-oriented-programming] - -</div> -<div> - - -Ejemplos de soluciones ----------------------- -- [Nette examples |https://github.com/nette-examples] -- [Doctrine & Nette |https://contributte.org/nettrine/] -- [Contributte examples |https://contributte.org/examples.html] -- [Doctrine ORM Website |https://github.com/MinecordNetwork/Website] -- [Inicio rápido |quickstart:] - -</div> -<div> - - -Vídeos ------- -Cientos de grabaciones de los encuentros Last Saturday y vídeos sobre Nette los encontrará todos en un mismo sitio, en el "canal de YouTube de Nette Framework":https://www.youtube.com/user/NetteFramework. - -</div> -</div> diff --git a/best-practices/es/@left-menu.texy b/best-practices/es/@left-menu.texy deleted file mode 100644 index fdc33e5ade..0000000000 --- a/best-practices/es/@left-menu.texy +++ /dev/null @@ -1,34 +0,0 @@ -Tutoriales y buenas prácticas -***************************** -- [Introducción |@home] - -Nette Application -***************** -- [Métodos y atributos inject |inject-method-attribute] -- [Componer presenters a partir de traits |presenter-traits] -- [Pasar ajustes a los presenters |passing-settings-to-presenters] -- [Cómo volver a una página anterior |restore-request] -- [Paginación de resultados de la base de datos |pagination] -- [Snippets dinámicos |dynamic-snippets] -- [Cómo usar el atributo #Requires |attribute-requires] -- [Cómo usar correctamente los enlaces POST |post-links] -- [URLs amigables con slugs |pretty-urls] - -Formularios -*********** -- [Reutilización de formularios |form-reuse] -- [Formulario para crear y editar registros |creating-editing-form] -- [Creemos un formulario de contacto |lets-create-contact-form] - -General -******* -- [Cómo escribir microsites |microsites] -- [Composer: consejos de uso |composer] - - -Lecturas adicionales -******************** -- [Documentación de Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Solución de problemas |nette:troubleshooting] diff --git a/best-practices/es/@meta.texy b/best-practices/es/@meta.texy deleted file mode 100644 index 16230381cf..0000000000 --- a/best-practices/es/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Tutoriales y buenas prácticas}} diff --git a/best-practices/es/attribute-requires.texy b/best-practices/es/attribute-requires.texy deleted file mode 100644 index fed7bec56e..0000000000 --- a/best-practices/es/attribute-requires.texy +++ /dev/null @@ -1,177 +0,0 @@ -Cómo usar el atributo `#[Requires]` -*********************************** - -.[perex] -Al escribir una aplicación web se encuentra a menudo con la necesidad de restringir el acceso a ciertas partes de la aplicación. Quizá quiera que algunas peticiones solo puedan enviar datos mediante un formulario (es decir, usando el método POST) o que solo sean accesibles para llamadas AJAX. En Nette Framework 3.2 se ha introducido una nueva herramienta que le permite establecer esas restricciones de forma elegante y clara: el atributo `#[Requires]`. - -Un atributo es un marcador especial de PHP que se añade antes de la definición de una clase o de un método. Como en el fondo es una clase, hay que incluir la cláusula `use` para que los siguientes ejemplos funcionen: - -```php -use Nette\Application\Attributes\Requires; -``` - -El atributo `#[Requires]` se puede usar con la propia clase del presenter y con estos métodos: - -- `action<Action>()` -- `render<View>()` -- `handle<Signal>()` -- `createComponent<Name>()` - -Los dos últimos métodos afectan también a los componentes, así que puede usar el atributo con ellos igualmente. - -Si no se cumplen las condiciones indicadas por el atributo, se lanza un error HTTP 4xx. - - -Métodos HTTP ------------- - -Puede indicar qué métodos HTTP (como GET, POST, etc.) están permitidos para el acceso. Por ejemplo, si quiere permitir el acceso solo mediante el envío de un formulario, ponga: - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST')] - public function actionDelete(int $id): void - { - } -} -``` - -¿Por qué debería usar POST en lugar de GET para las acciones que cambian el estado y cómo hacerlo? [Lea la guía |post-links]. - -Puede indicar un método o un array de métodos. Un caso especial es el valor `'*'`, que permite todos los métodos, algo que los presenters [no permiten de forma predeterminada por motivos de seguridad |application:presenters#Comprobación del método HTTP]. - - -Llamadas AJAX -------------- - -Si quiere que un presenter o un método sea accesible solo para peticiones AJAX, use: - -```php -#[Requires(ajax: true)] -class AjaxPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Mismo origen ------------- - -Para aumentar la seguridad puede exigir que la petición se haga desde el mismo dominio. Eso evita la [vulnerabilidad 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<Signal>()`, el acceso desde el mismo dominio se exige automáticamente. Así que, si quiere permitir el acceso desde cualquier dominio, indique: - -```php -#[Requires(sameOrigin: false)] -public function handleList(): void -{ -} -``` - - -Acceso mediante forward ------------------------ - -A veces resulta útil restringir el acceso a un presenter de modo que solo esté disponible indirectamente, por ejemplo con los métodos `forward()` o `switch()` desde otro presenter. Así se protegen, por ejemplo, los presenters de error, para que no se puedan invocar desde una URL: - -```php -#[Requires(forward: true)] -class ForwardedPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -En la práctica es frecuente que haga falta marcar ciertas vistas a las que solo se puede llegar en función de la lógica del presenter. De nuevo, para que no se puedan abrir 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 concretas ------------------- - -También puede restringir cierto código, como la creación de un componente, para que solo sea accesible en acciones concretas del presenter: - -```php -class EditDeletePresenter extends Nette\Application\UI\Presenter -{ - #[Requires(actions: ['add', 'edit'])] - public function createComponentPostForm() - { - } -} -``` - -En el caso de una sola acción no hace falta escribir un array: `#[Requires(actions: 'default')]` - - -Atributos propios ------------------ - -Si quiere usar el atributo `#[Requires]` repetidamente con la misma configuración, puede crear su propio atributo que herede de `#[Requires]` y lo configure según sus necesidades. - -Por ejemplo, `#[SingleAction]` permite el acceso solo 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 mediante todos los métodos HTTP usados por 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 ----------- - -El atributo `#[Requires]` le da una gran flexibilidad y control sobre cómo se accede a sus páginas web. Con reglas sencillas pero potentes puede reforzar la seguridad y el correcto funcionamiento de su aplicación. Como ve, usar atributos en Nette no solo puede simplificarle el trabajo, sino también hacerlo más seguro. diff --git a/best-practices/es/composer.texy b/best-practices/es/composer.texy deleted file mode 100644 index 715d2c4ca3..0000000000 --- a/best-practices/es/composer.texy +++ /dev/null @@ -1,282 +0,0 @@ -Composer: consejos de uso -************************* - -<div class=perex> - -Composer es una herramienta para gestionar dependencias en PHP. Permite declarar las bibliotecas de las que depende su proyecto y se encarga de instalarlas y actualizarlas por usted. Aprenderemos: - -- cómo instalar Composer -- cómo usarlo en un proyecto nuevo o ya existente - -</div> - - -Instalación -=========== - -Composer es un archivo ejecutable `.phar` que se descarga e instala de la siguiente manera. - - -Windows -------- - -Use el instalador oficial [Composer-Setup.exe|https://getcomposer.org/Composer-Setup.exe]. - - -Linux, macOS ------------- - -Bastan 4 comandos, que puede copiar de [esta página |https://getcomposer.org/download/]. - -Además, copiándolo a una carpeta que esté en el `PATH` del sistema, Composer pasa a estar disponible globalmente: - -```shell -$ mv ./composer.phar ~/bin/composer # o /usr/local/bin/composer -``` - - -Uso en un proyecto -================== - -Para empezar a usar Composer en su proyecto solo necesita un archivo `composer.json`. Este archivo describe las dependencias de su proyecto y puede contener también otros metadatos. El `composer.json` más sencillo puede tener este aspecto: - -```js -{ - "require": { - "nette/database": "^3.0" - } -} -``` - -Aquí decimos que nuestra aplicación (o biblioteca) requiere el paquete `nette/database` (el nombre del paquete se compone del nombre del proveedor y del nombre del proyecto) y que quiere una versión que cumpla la restricción `^3.0` (es decir, la última versión 3). - -Así pues, con el archivo `composer.json` en la raíz del proyecto, ejecute: - -```shell -composer update -``` - -Composer descargará Nette Database en el directorio `vendor/`. También crea el archivo `composer.lock`, que contiene información sobre qué versiones exactas de las bibliotecas ha instalado. - -Composer genera el archivo `vendor/autoload.php`. Basta con incluir este archivo y podrá empezar a usar las clases de las bibliotecas sin ningún trabajo adicional: - -```php -require __DIR__ . '/vendor/autoload.php'; - -$db = new Nette\Database\Connection('sqlite::memory:'); -``` - - -Actualizar los paquetes a las últimas versiones -=============================================== - -Para actualizar las bibliotecas usadas a las últimas versiones según las restricciones definidas en `composer.json`, use el comando `composer update`. Por ejemplo, con la dependencia `"nette/database": "^3.0"` instalará la última versión 3.x.x, pero no la versión 4. - -Para actualizar las restricciones del archivo `composer.json`, por ejemplo a `"nette/database": "^4.1"`, permitiendo así instalar la última versión, use el comando `composer require nette/database`. - -Para actualizar todos los paquetes de Nette usados tendría que enumerarlos todos en la línea de comandos, p. ej.: - -```shell -composer require nette/application nette/forms latte/latte tracy/tracy ... -``` - -Esto es poco práctico. Por eso, use el sencillo script "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff, que lo hará por usted: - -```shell -php composer-frontline.php -``` - - -Crear un proyecto nuevo -======================= - -Puede crear un proyecto nuevo de Nette con un solo comando: - -```shell -composer create-project nette/web-project name-of-the-project -``` - -Sustituya `name-of-the-project` por el nombre del directorio de su proyecto y ejecute el comando. Composer descargará de GitHub el repositorio `nette/web-project`, que ya contiene el archivo `composer.json`, y después instalará el propio Nette Framework. Solo queda [establecer los permisos de los directorios |nette:troubleshooting#Establecer los permisos de los directorios] `temp/` y `log/` y el proyecto debería estar en marcha. - -Si sabe en qué versión de PHP se alojará su proyecto, no olvide [indicarla |#Versión de PHP]. - - -Versión de PHP -============== - -Composer instala siempre versiones de los paquetes compatibles con la versión de PHP que usted usa en ese momento (en concreto, la versión de PHP usada en la línea de comandos al ejecutar Composer). Puede que no sea la misma versión que usa su hosting. Por eso es crucial añadir al archivo `composer.json` la información sobre la versión de PHP de su hosting. Entonces solo se instalarán versiones de los paquetes compatibles con él. - -Por ejemplo, para indicar que el proyecto funcionará en PHP 8.2.3, use el comando: - -```shell -composer config platform.php 8.2.3 -``` - -La versión se escribirá en el archivo `composer.json` así: - -```js -{ - "config": { - "platform": { - "php": "8.2.3" - } - } -} -``` - -El número de la versión de PHP se indica, sin embargo, también en otro lugar del archivo, en la sección `require`. Mientras que el primer número determina la versión para la que se instalan los paquetes, el segundo indica la versión para la que está escrita la propia aplicación. PhpStorm, por ejemplo, lo usa para establecer el *PHP language level*. (Naturalmente, no tiene sentido que estas versiones difieran, así que la doble entrada es un descuido.) Establezca esta versión con el comando: - -```shell -composer require php 8.2.3 --no-update -``` - -O directamente en el archivo `composer.json`: - -```js -{ - "require": { - "php": "8.2.3" - } -} -``` - - -Ignorar la versión de PHP -========================= - -Los paquetes suelen indicar tanto la versión mínima de PHP con la que son compatibles como la versión máxima con la que se han probado. Si tiene pensado usar una versión de PHP aún más nueva, quizá para hacer pruebas, Composer se negará a instalar un paquete así. La solución es la opción `--ignore-platform-req=php+`, que hace que Composer ignore los límites superiores de la versión de PHP requerida. - - -Avisos falsos -============= - -Al actualizar paquetes o cambiar números de versión se producen a veces conflictos. Un paquete tiene requisitos que chocan con los de otro, y así sucesivamente. Pero Composer emite a veces avisos falsos. Informa de un conflicto que en realidad no existe. En esos casos puede ayudar borrar el archivo `composer.lock` y volver a intentarlo. - -Si el mensaje de error persiste, es real y hay que leerlo para entender qué hay que modificar y cómo. - - -Packagist.org: el repositorio global -==================================== - -[Packagist |https://packagist.org] es el repositorio principal en el que Composer busca los paquetes de forma predeterminada. También puede publicar aquí sus propios paquetes. - - -¿Y si no queremos el repositorio central? ------------------------------------------ - -Si dentro de nuestra empresa tenemos aplicaciones o bibliotecas internas que no se pueden alojar públicamente, podemos crear nuestros propios repositorios para ellas. - -Más sobre los repositorios en [la documentación oficial |https://getcomposer.org/doc/05-repositories.md#repositories]. - - -Autoloading -=========== - -Una característica clave de Composer es que proporciona autoloading para todas las clases que instala. Se activa incluyendo el archivo `vendor/autoload.php`. - -Pero también puede usar Composer para cargar otras clases de fuera del directorio `vendor/`. La primera opción es dejar que Composer recorra los directorios y subdirectorios definidos, encuentre todas las clases y las incluya en el autoloader. Para conseguirlo, configure `autoload > classmap` en `composer.json`: - -```js -{ - "autoload": { - "classmap": [ - "src/", # incluye el directorio src/ y sus subdirectorios - ] - } -} -``` - -Después hay que ejecutar el comando `composer dumpautoload` tras cada cambio para regenerar las tablas de autoloading. Esto es extremadamente incómodo. Es mucho mejor encomendar esta tarea a [RobotLoader|robot-loader:], que hace lo mismo automáticamente en segundo plano y mucho más rápido. - -La segunda opción es atenerse a [PSR-4 |https://www.php-fig.org/psr/psr-4/]. Dicho de forma simple, es 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, p. ej. `App\Core\RouterFactory` estará en el archivo `/path/to/App/Core/RouterFactory.php`. Ejemplo de configuración: - -```js -{ - "autoload": { - "psr-4": { - "App\\": "app/" # el espacio de nombres App\ está en el directorio app/ - } - } -} -``` - -Consulte la [documentación de Composer |https://getcomposer.org/doc/04-schema.md#psr-4] para saber cómo configurar este comportamiento. - - -Probar versiones nuevas -======================= - -¿Quiere probar una nueva versión de desarrollo de un paquete? Así se hace. Primero, añada este par de opciones a su archivo `composer.json`. Eso permite instalar versiones de desarrollo, pero Composer solo recurrirá a ellas si ninguna combinación de versiones estables cumple los requisitos: - -```js -{ - "minimum-stability": "dev", - "prefer-stable": true, -} -``` - -También recomendamos borrar el archivo `composer.lock`, porque Composer a veces se niega a instalar sin explicación y esto puede resolverlo. - -Digamos que el paquete es `nette/utils` y la nueva versión es la 4.0. Instálelo con el comando: - -```shell -composer require nette/utils:4.0.x-dev -``` - -O puede instalar una versión concreta, por ejemplo la 4.0.0-RC2: - -```shell -composer require nette/utils:4.0.0-RC2 -``` - -Sin embargo, si otro paquete depende de la biblioteca y está fijado a una versión más antigua (p. ej. `^3.1`), la solución ideal es actualizar ese paquete dependiente para que funcione con la nueva versión. Pero si solo quiere saltarse la restricción y forzar a Composer a instalar la versión de desarrollo haciéndola pasar por una versión más antigua (p. ej. la 3.1.6), puede usar la palabra clave `as`: - -```shell -composer require nette/utils "4.0.x-dev as 3.1.6" -``` - - -Llamar a comandos -================= - -Puede llamar a sus propios comandos y scripts predefinidos a través de Composer como si fueran comandos nativos de Composer. Para los scripts que están en el directorio `vendor/bin` no hace falta indicar esa ruta. - -Como ejemplo, definamos en `composer.json` un script que use [Nette Tester |tester:] para ejecutar los tests: - -```js -{ - "scripts": { - "tester": "tester tests -s" - } -} -``` - -Después ejecutamos los tests con `composer tester`. Puede llamar al comando aunque no esté en el directorio raíz del proyecto, sino en alguno de sus subdirectorios. - - -Dar las gracias -=============== - -Le enseñamos un truco para alegrar a los autores de código abierto. Puede dar fácilmente estrellas en GitHub a las bibliotecas que usa su proyecto. Basta con instalar la biblioteca `symfony/thanks`: - -```shell -composer global require symfony/thanks -``` - -Y después ejecutar: - -```shell -composer thanks -``` - -¡Pruébelo! - - -Configuración -============= - -Composer está estrechamente integrado con la herramienta de control de versiones [Git |https://git-scm.com]. Si no tiene Git instalado, hay que decirle a Composer que no lo use: - -```shell -composer -g config preferred-install dist -``` diff --git a/best-practices/es/creating-editing-form.texy b/best-practices/es/creating-editing-form.texy deleted file mode 100644 index 0d7af59ac5..0000000000 --- a/best-practices/es/creating-editing-form.texy +++ /dev/null @@ -1,204 +0,0 @@ -Formulario para crear y editar un registro -****************************************** - -.[perex] -¿Cómo implementar correctamente en Nette añadir y editar un registro usando el mismo formulario para ambas cosas? - -En muchos casos, los formularios para añadir y editar registros son idénticos y se diferencian, como mucho, en el texto del botón. Mostraremos ejemplos de presenters sencillos en los que usaremos el formulario primero para añadir un registro, después para editarlo y, por último, combinaremos ambas soluciones. - - -Añadir un registro ------------------- - -Ejemplo de un presenter para añadir un registro. El trabajo real con la base de datos se lo dejaremos a la clase `Facade`, cuyo código no es esencial para este ejemplo. - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentRecordForm(): Form - { - $form = new Form; - - // ... añadir los campos del formulario ... - - $form->onSuccess[] = $this->recordFormSucceeded(...); - return $form; - } - - private function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // añade el registro a la base de datos - $this->flashMessage('Successfully added'); - $this->redirect('...'); - } - - public function renderAdd(): void - { - // ... - } -} -``` - - -Editar un registro ------------------- - -Veamos ahora qué aspecto tendría un presenter para editar un registro: - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - private $record; - - public function __construct( - private Facade $facade, - ) { - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // comprueba que el registro existe - || !$this->facade->isEditAllowed(/*...*/) // comprueba los permisos - ) { - $this->error(); // error 404 - } - - $this->record = $record; - } - - protected function createComponentRecordForm(): Form - { - // comprueba que la acción es 'edit' - if ($this->getAction() !== 'edit') { - $this->error(); - } - - $form = new Form; - - // ... añadir los campos del formulario ... - - $form->setDefaults($this->record); // establece los valores predeterminados - $form->onSuccess[] = $this->recordFormSucceeded(...); - return $form; - } - - private function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->update($this->record->id, $data); // actualiza el registro - $this->flashMessage('Successfully updated'); - $this->redirect('...'); - } -} -``` - -En el método *action*, que se invoca al principio del [ciclo de vida del presenter |application:presenters#Ciclo de vida del presenter], comprobamos la existencia del registro y el permiso del usuario para editarlo. - -Guardamos el registro en la propiedad `$record`, con lo que queda disponible en el método `createComponentRecordForm()` para establecer los valores predeterminados y en `recordFormSucceeded()` para acceder al ID. Una solución alternativa es establecer los valores predeterminados directamente en `actionEdit()` y obtener el valor del ID (que forma parte de la URL) con `getParameter('id')`: - - -```php - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - // comprueba la existencia y los permisos - ) { - $this->error(); - } - - // establece los valores predeterminados del formulario - $this->getComponent('recordForm') - ->setDefaults($record); - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); - // ... - } -``` - -Ahora bien, y esto debería ser **lo más importante que se lleve de todo este código**, al crear el formulario tenemos que asegurarnos de que la acción es realmente `edit`. ¡De lo contrario, la comprobación del método `actionEdit()` no se realizaría en absoluto! - - -El mismo formulario para añadir y editar ----------------------------------------- - -Combinemos ahora ambos presenters en uno. Podríamos distinguir la acción dentro del método `createComponentRecordForm()` y configurar el formulario en consecuencia, o bien podemos delegarlo directamente en los métodos de acción y eliminar la condición: - - -```php -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - public function actionAdd(): void - { - $form = $this->getComponent('recordForm'); - $form->onSuccess[] = $this->addingFormSucceeded(...); - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // comprueba que el registro existe - || !$this->facade->isEditAllowed(/*...*/) // comprueba los permisos - ) { - $this->error(); // error 404 - } - - $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // establece los valores predeterminados - $form->onSuccess[] = $this->editingFormSucceeded(...); - } - - protected function createComponentRecordForm(): Form - { - // comprueba que la acción es 'add' o 'edit' - if (!in_array($this->getAction(), ['add', 'edit'])) { - $this->error(); - } - - $form = new Form; - - // ... añadir los campos del formulario ... - - return $form; - } - - private function addingFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // añade el registro a la base de datos - $this->flashMessage('Successfully added'); - $this->redirect('...'); - } - - private function editingFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // actualiza el registro - $this->flashMessage('Successfully updated'); - $this->redirect('...'); - } -} -``` - -{{priority: -1}} diff --git a/best-practices/es/dynamic-snippets.texy b/best-practices/es/dynamic-snippets.texy deleted file mode 100644 index dace88c1d9..0000000000 --- a/best-practices/es/dynamic-snippets.texy +++ /dev/null @@ -1,176 +0,0 @@ -Snippets dinámicos -****************** - -.[perex] -Cómo usar AJAX para refrescar solo las partes de la página que realmente cambian, como los elementos individuales de una lista, mediante los snippets dinámicos de Latte. - -Con bastante frecuencia surge durante el desarrollo la necesidad de realizar operaciones AJAX, por ejemplo sobre las filas de una tabla o los elementos de una lista. Como ejemplo, imaginemos un listado de artículos en el que los usuarios conectados pueden valorar cada artículo con "me gusta" o "no me gusta". El código del presenter y la plantilla correspondiente sin AJAX tendrían más o menos este aspecto (mostramos las partes más relevantes; el código da por hecho que existe un servicio que gestiona las valoraciones y obtiene los artículos, cuya implementación concreta no es esencial para esta guía): - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - $this->redirect('this'); -} - -public function handleUnlike(int $articleId): void -{ - $this->ratingService->removeLike($articleId, $this->user->id); - $this->redirect('this'); -} -``` - -Plantilla: - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {if !$article->liked} - <a n:href="like!, $article->id" class=ajax>I like it</a> - {else} - <a n:href="unlike!, $article->id" class=ajax>I don't like it anymore</a> - {/if} -</article> -``` - - -Ajaxización -=========== - -Añadamos ahora funcionalidad AJAX a esta sencilla aplicación. Cambiar la valoración de un artículo no es tan importante como para justificar una recarga completa de la página, así que lo ideal es que ocurra por AJAX en segundo plano. Usaremos el [script de gestión de los complementos |application:ajax#Naja] con la convención habitual de que los enlaces AJAX llevan la clase CSS `ajax`. - -¿Pero cómo lo implementamos exactamente? Nette ofrece dos enfoques: los snippets dinámicos y los componentes. Ambos tienen sus ventajas e inconvenientes, así que mostraremos cada uno de ellos. - - -La vía de los snippets dinámicos -================================ - -En la terminología de Latte, un snippet dinámico es un uso concreto de la etiqueta `{snippet}` en el que se usa una variable en el nombre del snippet. Un snippet así no se puede colocar en cualquier sitio de la plantilla: tiene que estar envuelto por un snippet estático (normal) o estar dentro de un `{snippetArea}`. Podríamos modificar nuestra plantilla así: - - -```latte -{snippet articlesContainer} - <article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {snippet article-{$article->id}} - {if !$article->liked} - <a n:href="like!, $article->id" class=ajax>I like it</a> - {else} - <a n:href="unlike!, $article->id" class=ajax>I don't like it anymore</a> - {/if} - {/snippet} - </article> -{/snippet} -``` - -Cada artículo define ahora un snippet cuyo nombre incluye el ID del artículo. Todos estos snippets dinámicos están envueltos a su vez por un snippet estático llamado `articlesContainer`. Si omitiéramos este snippet exterior, Latte lanzaría una excepción. - -Solo queda añadir al presenter la lógica del redibujado: basta con redibujar el envoltorio estático. - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - if ($this->isAjax()) { - $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- no hace falta - } else { - $this->redirect('this'); - } -} -``` - -Modifique de forma parecida el método `handleUnlike()` correspondiente y ¡AJAX ya funciona! - -Esta solución tiene, sin embargo, un inconveniente. Si examinamos la petición AJAX más de cerca, veremos que, aunque por fuera la aplicación parece eficiente (devuelve un único snippet del artículo concreto), en realidad renderiza en el servidor *todos* los snippets. Coloca en la carga útil el snippet requerido y descarta los demás (lo que significa que también los obtuvo y renderizó innecesariamente). - -Para optimizarlo tenemos que intervenir donde se pasa la colección `$articles` a la plantilla (digamos que en el método `renderDefault()`). Aprovecharemos que el procesamiento de la señal ocurre antes de los métodos `render<Something>`: - -```php -public function handleLike(int $articleId): void -{ - // ... - if ($this->isAjax()) { - // ... - $this->template->articles = [ - $this->db->table('articles')->get($articleId), - ]; - } else { - // ... -} - -public function renderDefault(): void -{ - if (!isset($this->template->articles)) { - $this->template->articles = $this->db->table('articles'); - } -} -``` - -Ahora, durante el procesamiento de la señal, en lugar de toda la colección de artículos se le pasa a la plantilla solo un array con el único artículo relevante, el que queremos renderizar y enviar en la carga útil al navegador. Como consecuencia, el bucle `{foreach}` se ejecuta una sola vez y no se renderiza ningún snippet innecesario. - - -La vía de los componentes -========================= - -Un enfoque completamente distinto evita del todo los snippets dinámicos. El truco consiste en encapsular toda la lógica en un componente separado. En lugar de que el presenter gestione la valoración, se ocupará de ella un `LikeControl` dedicado. La clase tendrá este aspecto (contendría también los métodos `render`, `handleUnlike`, etc.): - -```php -class LikeControl extends Nette\Application\UI\Control -{ - public function __construct( - private Article $article, - ) { - } - - public function handleLike(): void - { - $this->ratingService->saveLike($this->article->id, $this->presenter->user->id); - if ($this->presenter->isAjax()) { - $this->redrawControl(); - } else { - $this->presenter->redirect('this'); - } - } -} -``` - -Plantilla del componente: - -```latte -{snippet} - {if !$article->liked} - <a n:href="like!" class=ajax>I like it</a> - {else} - <a n:href="unlike!" class=ajax>I don't like it anymore</a> - {/if} -{/snippet} -``` - -Naturalmente, la plantilla de la vista cambiará y tendremos que añadir una factory al presenter. Como crearemos una instancia de este componente para cada artículo obtenido de la base de datos, usaremos la clase [Multiplier |application:Multiplier] para gestionar su creación. - -```php -protected function createComponentLikeControl() -{ - $articles = $this->db->table('articles'); - return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { - return new LikeControl($articles[$articleId]); - }); -} -``` - -La plantilla de la vista se reduce al mínimo (¡y queda completamente libre de snippets!): - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {control "likeControl-$article->id"} -</article> -``` - -Ya casi hemos terminado: la aplicación funcionará ahora con AJAX. También aquí hace falta optimizar, porque, debido al uso de Nette Database, el procesamiento de la señal carga innecesariamente de la base de datos todos los artículos en lugar de solo el relevante. La ventaja, sin embargo, es que no se produce ningún renderizado innecesario, porque solo se renderiza la instancia concreta del componente. - -{{priority: -1}} diff --git a/best-practices/es/form-reuse.texy b/best-practices/es/form-reuse.texy deleted file mode 100644 index b26c80c6d8..0000000000 --- a/best-practices/es/form-reuse.texy +++ /dev/null @@ -1,348 +0,0 @@ -Reutilización de formularios en múltiples lugares -************************************************* - -.[perex] -Nette ofrece varias formas de reutilizar el mismo formulario en varios sitios sin duplicar código. En este artículo veremos distintas soluciones, incluidas aquellas que debería evitar. - - -Factory de formularios -====================== - -Un enfoque básico para reutilizar un componente en varios lugares es crear un método o una clase que genere ese componente. Ese método se llama después desde distintos puntos de la aplicación. A un método o clase así se le llama *factory*. No lo confunda con el patrón de diseño *factory method*, que describe una forma concreta de usar las factories y no está directamente relacionado con este tema. - -Como ejemplo, creemos una factory que construya un formulario de edición: - -```php -use Nette\Application\UI\Form; - -class FormFactory -{ - public function createEditForm(): Form - { - $form = new Form; - $form->addText('title', 'Title:'); - // aquí se añaden más campos del formulario - $form->addSubmit('send', 'Save'); - return $form; - } -} -``` - -Ahora puede usar esta factory en distintas partes de su aplicación, por ejemplo en presenters o componentes. Lo hace [pidiéndola como dependencia |dependency-injection:passing-dependencies]. Primero registre la clase en el archivo de configuración: - -```neon -services: - - FormFactory -``` - -Y después úsela en el presenter: - - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->createEditForm(); - $form->onSuccess[] = function () { - // procesamiento de los datos enviados - }; - return $form; - } -} -``` - -Puede ampliar la factory de formularios con más métodos para crear otros tipos de formularios, según lo que necesite su aplicación. Y, naturalmente, podemos añadir un método que cree un formulario básico sin elementos, que los demás métodos podrán aprovechar: - -```php -class FormFactory -{ - public function createForm(): Form - { - $form = new Form; - return $form; - } - - public function createEditForm(): Form - { - $form = $this->createForm(); - $form->addText('title', 'Title:'); - // aquí se añaden más campos del formulario - $form->addSubmit('send', 'Save'); - return $form; - } -} -``` - -El método `createForm()` todavía no hace nada demasiado útil, pero eso cambiará enseguida. - - -Dependencias de la factory -========================== - -Con el tiempo puede surgir la necesidad de que los formularios sean multilingües. Eso significa establecer un [traductor |forms:rendering#Traducción] para todos los formularios. Para conseguirlo, modifique la clase `FormFactory` de modo que acepte el objeto `Translator` como dependencia en el constructor y se lo pase al formulario creado: - -```php -use Nette\Localization\Translator; - -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function createForm(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } - - // ... -} -``` - -Como el método `createForm()` lo llaman también los demás métodos que crean formularios concretos, basta con establecer aquí el traductor. Y listo. No hace falta modificar el código de ningún presenter ni componente, lo cual es estupendo. - - -Más clases factory -================== - -Otra opción es crear una clase factory separada para cada formulario que quiera usar en su aplicación. Este enfoque puede mejorar la legibilidad del código y simplificar la gestión de los formularios. Deje que la `FormFactory` original cree solo un formulario básico con la configuración fundamental (como el soporte de traducción) y cree una nueva factory, `EditFormFactory`, específicamente para el formulario de edición. - -```php -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function create(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } -} - - -// ✅ usando composición -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - // aquí se añaden más campos del formulario - $form->addSubmit('send', 'Save'); - return $form; - } -} -``` - -Es crucial que la relación entre las clases `FormFactory` y `EditFormFactory` se realice mediante [composición |nette:introduction-to-object-oriented-programming#Composición], no mediante [herencia de objetos |nette:introduction-to-object-oriented-programming#Herencia]: - -```php -// ⛔ ¡NO! LA HERENCIA NO PINTA NADA AQUÍ -class EditFormFactory extends FormFactory -{ - public function create(): Form - { - $form = parent::create(); - $form->addText('title', 'Title:'); - // aquí se añaden más campos del formulario - $form->addSubmit('send', 'Save'); - return $form; - } -} -``` - -Usar aquí la herencia sería del todo contraproducente. Se toparía con problemas muy pronto. Por ejemplo, si quisiera añadir parámetros al método `create()`, PHP daría un error porque su firma diferiría de la del antecesor. O al pasar dependencias a la clase `EditFormFactory` mediante el constructor. Eso llevaría a lo que se conoce como [infierno del constructor |dependency-injection:passing-dependencies#Infierno del constructor]. - -En general, es mejor preferir la [composición a la herencia |dependency-injection:faq#¿Por qué se prefiere la composición a la herencia?]. - - -Procesamiento del formulario -============================ - -El manejador del formulario, que se invoca tras un envío correcto, también puede formar parte de la clase factory. Funciona pasando los datos enviados a la capa del modelo para su procesamiento. Los posibles errores de procesamiento se devuelven [de vuelta |forms:validation#Errores de procesamiento] al formulario. En el siguiente ejemplo, el modelo está representado por la clase `Facade`: - -```php -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - private Facade $facade, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - $form->addText('title', 'Title:'); - // aquí se añaden más campos del formulario - $form->addSubmit('send', 'Save'); - $form->onSuccess[] = $this->processForm(...); - return $form; - } - - private function processForm(Form $form, array $data): void - { - try { - // procesamiento de los datos enviados - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - } - } -} -``` - -De la redirección, en cambio, deje que se ocupe el propio presenter. Añade al evento `onSuccess` otro manejador que realiza la redirección. Gracias a eso, el formulario se puede usar en distintos presenters, y cada uno redirigirá a un lugar distinto tras el éxito. - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditFormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->create(); - $form->onSuccess[] = function () { - $this->flashMessage('Record was saved'); - $this->redirect('Homepage:'); - }; - return $form; - } -} -``` - -Esta solución aprovecha la característica de los formularios según la cual, si se llama a `addError()` sobre el formulario o sobre alguno de sus elementos, los siguientes manejadores `onSuccess` ya no se invocan. - - -Heredar de la clase Form -======================== - -Un formulario ya montado no debería ser descendiente de la clase `Form`. Dicho de otro modo, evite este enfoque: - -```php -// ⛔ ¡NO! LA HERENCIA NO PINTA NADA AQUÍ -class EditForm extends Form -{ - public function __construct(Translator $translator) - { - parent::__construct(); - $this->addText('title', 'Title:'); - // aquí se añaden más campos del formulario - $this->addSubmit('send', 'Save'); - $this->setTranslator($translator); - } -} -``` - -En lugar de montar el formulario en el constructor, use una factory. - -Es importante darse cuenta de que la clase `Form` es ante todo una herramienta para construir formularios, es decir, un *form builder*. El formulario montado puede considerarse su producto. Pero un producto no es un tipo concreto de constructor; entre ellos no existe la relación *es un*, que es el fundamento de la herencia. - - -Componente de formulario -======================== - -Un enfoque completamente distinto consiste en crear un [componente |application:components] que encapsule el formulario. Eso abre nuevas posibilidades, como renderizar el formulario de una manera concreta, ya que el componente incluye su propia plantilla. O se pueden usar señales para la comunicación AJAX y cargar dinámicamente información en el formulario, por ejemplo para sugerencias, etc. - - -```php -use Nette\Application\UI\Form; - -class EditControl extends Nette\Application\UI\Control -{ - public array $onSave = []; - - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentForm(): Form - { - $form = new Form; - $form->addText('title', 'Title:'); - // aquí se añaden más campos del formulario - $form->addSubmit('send', 'Save'); - $form->onSuccess[] = $this->processForm(...); - - return $form; - } - - private function processForm(Form $form, array $data): void - { - try { - // procesamiento de los datos enviados - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - return; - } - - // invocación del evento - $this->onSave($this, $data); - } -} -``` - -A continuación, creemos una factory que produzca este componente. Basta con [definir su interfaz |application:components#Componentes con dependencias]: - -```php -interface EditControlFactory -{ - function create(): EditControl; -} -``` - -Y añadirla al archivo de configuración: - -```neon -services: - - EditControlFactory -``` - -Ahora ya podemos pedir la factory y usarla en el presenter: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditControlFactory $controlFactory, - ) { - } - - protected function createComponentEditForm(): EditControl - { - $control = $this->controlFactory->create(); - - $control->onSave[] = function (EditControl $control, $data) { - $this->redirect('this'); - // o redirigir al resultado de la edición, p. ej.: - // $this->redirect('detail', ['id' => $data->id]); - }; - - return $control; - } -} -``` diff --git a/best-practices/es/inject-method-attribute.texy b/best-practices/es/inject-method-attribute.texy deleted file mode 100644 index 3116c7f84e..0000000000 --- a/best-practices/es/inject-method-attribute.texy +++ /dev/null @@ -1,61 +0,0 @@ -Métodos y atributos inject -************************** - -.[perex] -Este artículo se centra en las distintas formas de pasar dependencias a los presenters dentro del framework Nette. Compararemos el método preferido, la inyección por constructor, con alternativas como los métodos y atributos `inject`. - -Para los presenters, igual que para las demás clases, el enfoque preferido es pasar las dependencias por el [constructor |dependency-injection:passing-dependencies#Inyección por constructor]. Ahora bien, si crea un antecesor común del que heredan los demás presenters (p. ej. `BasePresenter`) y ese antecesor también necesita dependencias, puede surgir el problema conocido como [infierno del constructor |dependency-injection:passing-dependencies#Infierno del constructor]. Se puede sortear con métodos alternativos, en concreto con los métodos y atributos inject (antes anotaciones). - - -Métodos `inject*()` -=================== - -Es una forma de pasar dependencias mediante [setters |dependency-injection:passing-dependencies#Inyección por setter]. Los nombres de estos setters deben empezar con el prefijo `inject`. Nette DI llama automáticamente a los métodos así llamados justo después de crear la instancia del presenter y les pasa todas las dependencias necesarias. Por eso deben declararse públicos. - -Los métodos `inject*()` se pueden ver como una ampliación del constructor repartida en varios métodos. Eso permite que `BasePresenter` reciba sus dependencias mediante un método aparte y deje el constructor libre para sus descendientes: - -```php -abstract class BasePresenter extends Nette\Application\UI\Presenter -{ - private Foo $foo; - - public function injectBase(Foo $foo): void - { - $this->foo = $foo; - } -} - -class MyPresenter extends BasePresenter -{ - private Bar $bar; - - public function __construct(Bar $bar) - { - $this->bar = $bar; - } -} -``` - -Un presenter puede tener cualquier número de métodos `inject*()` y cada uno puede aceptar cualquier número de parámetros. Este enfoque encaja bien también cuando un presenter está [compuesto por traits |presenter-traits] y cada trait necesita sus propias dependencias. - - -Atributos `Inject` -================== - -Es una forma de [inyectar en propiedades |dependency-injection:passing-dependencies#Inyección en propiedades]. Basta con marcar las propiedades que deben inyectarse y Nette DI pasará automáticamente las dependencias justo después de crear la instancia del presenter. Para permitir la inyección, estas propiedades deben declararse públicas. - -Las propiedades se marcan con un atributo: (antes se usaba la anotación `/** @inject */`) - -```php -use Nette\DI\Attributes\Inject; // esta línea es importante - -class MyPresenter extends Nette\Application\UI\Presenter -{ - #[Inject] - public Cache $cache; -} -``` - -La ventaja de esta forma de pasar dependencias era su sintaxis muy concisa. Pero con la llegada de la [promoción de propiedades en el constructor |https://blog.nette.org/es/php-8-0-complete-overview-of-news#toc-constructor-property-promotion], usar el constructor suele parecer más sencillo. - -Por el contrario, este método adolece de los mismos inconvenientes que la inyección en propiedades en general: perdemos el control sobre los cambios de la variable y la variable pasa a formar parte de la interfaz pública de la clase, lo cual no suele ser deseable. diff --git a/best-practices/es/lets-create-contact-form.texy b/best-practices/es/lets-create-contact-form.texy deleted file mode 100644 index e9ccbcf99c..0000000000 --- a/best-practices/es/lets-create-contact-form.texy +++ /dev/null @@ -1,218 +0,0 @@ -Creemos un formulario de contacto -********************************* - -.[perex] -Veamos cómo crear en Nette un formulario de contacto, incluido el envío por correo de los datos enviados. ¡Vamos allá! - -Primero tenemos que crear un proyecto nuevo. La página [Primeros pasos |nette:installation] explica cómo hacerlo. Después ya podemos empezar a crear el formulario. - -El enfoque más sencillo es crear el [formulario directamente en el presenter |forms:in-presenter]. Podemos aprovechar el `HomePresenter` ya existente. Le añadiremos un componente llamado `contactForm`, que representará nuestro formulario. Lo conseguimos añadiendo al código del presenter el método factory `createComponentContactForm()`, que creará el componente: - -```php -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - protected function createComponentContactForm(): Form - { - $form = new Form; - $form->addText('name', 'Name:') - ->setRequired('Please enter your name'); - $form->addEmail('email', 'E-mail:') - ->setRequired('Please enter your email'); - $form->addTextArea('message', 'Message:') - ->setRequired('Please enter a message'); - $form->addSubmit('send', 'Send'); - $form->onSuccess[] = $this->contactFormSucceeded(...); - return $form; - } - - private function contactFormSucceeded(Form $form, $data): void - { - // envío del correo - } -} -``` - -Como ve, hemos creado dos métodos. El primero, `createComponentContactForm()`, crea una nueva instancia del formulario. Incluye campos para el nombre, el correo y el mensaje, añadidos con los métodos `addText()`, `addEmail()` y `addTextArea()`, respectivamente. También hemos añadido un botón de envío. ¿Pero qué pasa si el usuario deja un campo vacío? En ese caso deberíamos avisarle de que el campo es obligatorio. Lo hemos conseguido con el método `setRequired()`. Por último hemos enganchado a `onSuccess` un manejador de [eventos |nette:glossary#Eventos] que se dispara tras el envío correcto del formulario. En nuestro caso llama al método `contactFormSucceeded`, que se ocupará de procesar los datos enviados. Lo implementaremos enseguida. - -Rendericemos el componente `contactForm` en la plantilla `Home/default.latte`: - -```latte -{block content} -<h1>Contact Form</h1> -{control contactForm} -``` - -Para el envío del correo propiamente dicho crearemos una nueva clase llamada `ContactFacade` y la colocaremos en el archivo `app/Model/ContactFacade.php`: - -```php -namespace App\Model; - -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $mail = new Message; - $mail->addTo('admin@example.com') // su correo - ->setFrom($email, $name) - ->setSubject('Message from the contact form') - ->setBody($message); - - $this->mailer->send($mail); - } -} -``` - -El método `sendMessage()` crea y envía el correo. Para ello usa un servicio mailer, que recibe como dependencia mediante el constructor. Lea más sobre el [envío de correos |mail:]. - -Ahora volvamos al presenter y completemos el método `contactFormSucceeded()`. Llamará al método `sendMessage()` de la clase `ContactFacade` y le pasará los datos enviados con el formulario. ¿Y cómo obtenemos el objeto `ContactFacade`? Se lo pediremos mediante el constructor usando la inyección de dependencias: - -```php -use App\Model\ContactFacade; -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - public function __construct( - private ContactFacade $facade, - ) { - } - - protected function createComponentContactForm(): Form - { - // ... - } - - public function contactFormSucceeded(stdClass $data): void - { - $this->facade->sendMessage($data->email, $data->name, $data->message); - $this->flashMessage('Message has been sent'); - $this->redirect('this'); - } -} -``` - -Tras enviar el correo mostramos al usuario un [mensaje flash |application:components#Mensajes flash] que confirma el envío. Después redirigimos para que el formulario no se vuelva a enviar al recargar la página en el navegador. - - -Así que, si todo está bien configurado, ya debería poder enviar un correo desde su formulario de contacto. ¡Enhorabuena! - - -Plantilla HTML del correo -------------------------- - -De momento se envía un correo en texto plano que contiene solo el mensaje enviado con el formulario. Pero podemos usar HTML en el correo para que su aspecto resulte más atractivo. Le crearemos una plantilla en Latte y la guardaremos como `app/Model/contactEmail.latte`: - -```latte -<html> - <title>Message from the contact form - - -

    Name: {$name}

    -

    E-mail: {$email}

    -

    Message: {$message}

    - - -``` - -Queda modificar `ContactFacade` para que use esta plantilla. En el constructor pediremos la clase `LatteFactory`, que sabe crear un objeto `Latte\Engine`, el [renderizador de plantillas Latte |latte:develop#Cómo renderizar una plantilla]. Con el método `renderToString()` renderizamos la plantilla en una cadena. El primer parámetro es la ruta al archivo de la plantilla y el segundo, un array de variables que se le pasan. - -```php -namespace App\Model; - -use Nette\Bridges\ApplicationLatte\LatteFactory; -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $latte = $this->latteFactory->create(); - $body = $latte->renderToString(__DIR__ . '/contactEmail.latte', [ - 'email' => $email, - 'name' => $name, - 'message' => $message, - ]); - - $mail = new Message; - $mail->addTo('admin@example.com') // su correo - ->setFrom($email, $name) - ->setHtmlBody($body); - - $this->mailer->send($mail); - } -} -``` - -El contenido HTML generado del correo se lo pasamos después al método `setHtmlBody()` en lugar del original `setBody()`. Tampoco tenemos que indicar el asunto del correo con `setSubject()`, porque la biblioteca lo extrae automáticamente del elemento `` de la plantilla. - - -Configuración -------------- - -En el código de la clase `ContactFacade` sigue estando escrito a fuego nuestro correo de administrador `admin@example.com`. Sería mejor trasladarlo al archivo de configuración. ¿Cómo lo hacemos? - -Primero modifique la clase `ContactFacade` y sustituya la cadena del correo escrita a fuego por una variable que se pasa por el constructor: - -```php -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - private string $adminEmail, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - // ... - $mail = new Message; - $mail->addTo($this->adminEmail) - ->setFrom($email, $name) - ->setHtmlBody($body); - // ... - } -} -``` - -El segundo paso es indicar el valor de esta variable en la configuración. En el archivo `app/config/services.neon` añada: - -```neon -services: - - App\Model\ContactFacade(adminEmail: admin@example.com) -``` - -Y ya está. Si la sección `services` contiene muchos elementos y le parece que la dirección de correo se pierde entre ellos, podemos convertirla en un parámetro. Modifique la entrada así: - -```neon -services: - - App\Model\ContactFacade(adminEmail: %adminEmail%) -``` - -Y defina este parámetro en el archivo `app/config/common.neon`: - -```neon -parameters: - adminEmail: admin@example.com -``` - -¡Y listo! diff --git a/best-practices/es/microsites.texy b/best-practices/es/microsites.texy deleted file mode 100644 index df58bf3bf2..0000000000 --- a/best-practices/es/microsites.texy +++ /dev/null @@ -1,63 +0,0 @@ -Cómo escribir microsites -************************ - -Imagine que necesita crear rápidamente un pequeño sitio web para un evento que se acerca en su empresa. Tiene que ser sencillo, rápido y sin complicaciones innecesarias. Quizá piense que para un proyecto tan pequeño no hace falta un framework robusto. ¿Pero y si usar Nette Framework pudiera simplificar y acelerar precisamente ese proceso? - -Incluso al crear webs sencillas no quiere renunciar a la comodidad. No quiere reinventar lo que ya está resuelto. Sea perezoso sin remordimientos y déjese mimar. Nette Framework es excelente también para usarlo como microframework. - -¿Qué aspecto puede tener un microsite así? Por ejemplo, todo el código de la web puede estar en un único archivo `index.php` dentro del directorio público: - -```php -<?php - -require __DIR__ . '/../vendor/autoload.php'; - -$configurator = new Nette\Bootstrap\Configurator; -$configurator->enableTracy(__DIR__ . '/../log'); -$configurator->setTempDirectory(__DIR__ . '/../temp'); - -// crea el contenedor DI a partir de la configuración de config.neon -$configurator->addConfig(__DIR__ . '/../app/config.neon'); -$container = $configurator->createContainer(); - -// configura 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) { - // detecta el idioma del navegador y redirige a la URL /en o /de, etc. - $supportedLangs = ['en', 'de', 'cs']; - $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); - return $presenter->redirectUrl("/$lang"); -}); - -// ruta para la URL https://example.com/cs o https://example.com/en -$router->addRoute('<lang cs|en|de>', function ($presenter, string $lang) { - // muestra la plantilla adecuada, por ejemplo ../templates/en.latte - $template = $presenter->createTemplate() - ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); - return $template; -}); - -// ¡ejecuta la aplicación! -$container->getByType(Nette\Application\Application::class)->run(); -``` - -Todo lo demás serán plantillas guardadas en el directorio superior `/templates`. - -El código PHP de `index.php` primero [prepara el entorno |bootstrap:], después define las [rutas |application:routing#Enrutamiento dinámico con callbacks] y por último ejecuta la aplicación. La ventaja está en que el segundo parámetro de la función `addRoute()` puede ser un callable, que se ejecuta al abrir la página correspondiente. - - -¿Por qué usar Nette para microsites? ------------------------------------- - -- Los programadores que han probado [Tracy|tracy:] hoy ya casi no se imaginan programar sin ella. -- Sobre todo, aprovechará el sistema de plantillas [Latte|latte:], porque incluso con dos páginas querrá separar el [layout y el contenido|latte:template-inheritance]. -- Y sin duda quiere contar con el [escapado automático |latte:safety-first] para evitar vulnerabilidades XSS. -- Nette también se asegura de que, en caso de error, nunca se muestren los mensajes de error de PHP en bruto, sino una página amable para el usuario. -- Si quiere recoger opiniones de los usuarios, por ejemplo con un formulario de contacto, puede añadir fácilmente soporte para [formularios|forms:] y [bases de datos|database:]. -- También puede hacer con facilidad que los formularios rellenados se [envíen por correo|mail:]. -- A veces le vendrá bien el [almacenamiento en caché|caching:], por ejemplo al descargar y mostrar feeds. - -En el mundo acelerado de hoy, donde la velocidad y la eficiencia son cruciales, es vital tener herramientas que le permitan lograr resultados sin retrasos innecesarios. Nette Framework ofrece justamente eso: desarrollo rápido, seguridad y un amplio abanico de herramientas como Tracy y Latte que agilizan el proceso. Basta con instalar unos pocos paquetes de Nette y construir un microsite así se vuelve increíblemente fácil. Y puede estar tranquilo de que no hay vulnerabilidades de seguridad ocultas. diff --git a/best-practices/es/pagination.texy b/best-practices/es/pagination.texy deleted file mode 100644 index b3184ebf22..0000000000 --- a/best-practices/es/pagination.texy +++ /dev/null @@ -1,273 +0,0 @@ -Paginación de resultados de la base de datos -******************************************** - -.[perex] -Al desarrollar aplicaciones web se encuentra a menudo con el requisito de limitar el número de elementos listados por página, una técnica conocida como paginación. - -Partamos de una situación en la que listamos todos los datos sin paginación. Para seleccionar los datos de la base de datos tenemos la clase `ArticleRepository`. Además del constructor, contiene el método `findPublishedArticles`, que devuelve todos los artículos publicados ordenados de forma descendente por la fecha de publicación. - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC', - new \DateTime, - ); - } -} -``` - -En el presenter inyectamos después esta clase del modelo. En el método render obtenemos los artículos publicados y se los pasamos a la plantilla: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(): void - { - $this->template->articles = $this->articleRepository->findPublishedArticles(); - } -} -``` - -De listar los artículos se encargará después la plantilla `default.latte`: - -```latte -{block content} -<h1>Articles</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> -``` - - -De esta manera podemos listar todos los artículos, pero eso empieza a ser problemático cuando el número de artículos crece. En ese momento resulta útil implementar un mecanismo de paginación. - -Este mecanismo divide todos los artículos en varias páginas y solo mostramos los artículos que pertenecen a la página seleccionada en ese momento. El número total de páginas y el reparto de los artículos los calcula la utilidad [Paginator |utils:Paginator] a partir del número total de artículos y del número deseado de artículos por página. - -En el primer paso modificaremos el método de obtención de artículos de la clase repositorio para que pueda devolver los artículos de una sola página. También añadiremos un método para obtener el número total de artículos de la base de datos, necesario para configurar el Paginator: - -```php -namespace App\Model; - -use Nette; - - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(int $limit, int $offset): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC - LIMIT ? - OFFSET ?', - new \DateTime, $limit, $offset, - ); - } - - /** - * Returns the total number of published articles - */ - public function getPublishedArticlesCount(): int - { - return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime); - } -} -``` - -A continuación modifiquemos el presenter. Le pasaremos al método `renderDefault` el número de la página actual. Si ese número no forma parte de la URL, estableceremos el valor predeterminado 1 (la primera página). - -Ampliaremos también el método render para crear y configurar una instancia del Paginator y seleccionar los artículos adecuados para mostrarlos en la plantilla. El `HomePresenter` modificado tendrá este aspecto: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Obtiene el número total de artículos publicados - $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - - // Crea y configura la instancia del Paginator - $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // número total de elementos - $paginator->setItemsPerPage(10); // elementos por página - $paginator->setPage($page); // número de la página actual - - // Obtiene de la base de datos un conjunto limitado de artículos según el cálculo del Paginator - $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - - // se los pasa a la plantilla - $this->template->articles = $articles; - // y también el propio Paginator para mostrar los controles de paginación - $this->template->paginator = $paginator; - } -} -``` - -La plantilla ahora recorre solo los artículos de la página actual. Solo nos queda añadir los enlaces de paginación: - -```latte -{block content} -<h1>Articles</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if !$paginator->isFirst()} - <a n:href="default, 1">First</a> -  |  - <a n:href="default, $paginator->getPage() - 1">Previous</a> -  |  - {/if} - - Page {$paginator->getPage()} of {$paginator->getPageCount()} - - {if !$paginator->isLast()} -  |  - <a n:href="default, $paginator->getPage() + 1">Next</a> -  |  - <a n:href="default, $paginator->getPageCount()">Last</a> - {/if} -</div> -``` - - -Con esto queda terminada la implementación de la paginación con el Paginator. Si usa como capa de base de datos [Nette Database Explorer |database:explorer] en lugar de [Nette Database Core |database:sql-way], puede implementar la paginación incluso sin usar directamente la utilidad Paginator. La clase `Nette\Database\Table\Selection` incluye el método [page() |api:Nette\Database\Table\Selection::page()], que encapsula la lógica de la paginación. - -Con este enfoque, el repositorio tendrá este aspecto: - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Explorer $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\Table\Selection - { - return $this->database->table('articles') - ->where('created_at < ', new \DateTime) - ->order('created_at DESC'); - } -} -``` - -En el presenter no necesitamos crear una instancia del Paginator. En su lugar usaremos el método `page()` que ofrece el objeto `Selection` devuelto por el repositorio: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Obtiene los artículos publicados - $articles = $this->articleRepository->findPublishedArticles(); - - // y pasa a la plantilla solo la parte limitada por el cálculo del método page - $lastPage = 0; - $this->template->articles = $articles->page($page, 10, $lastPage); - - // y también los datos necesarios para mostrar las opciones de paginación - $this->template->page = $page; - $this->template->lastPage = $lastPage; - } -} -``` - -Como ya no pasamos el objeto Paginator a la plantilla, tenemos que ajustar la parte que muestra los enlaces de paginación: - -```latte -{block content} -<h1>Articles</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if $page > 1} - <a n:href="default, 1">First</a> -  |  - <a n:href="default, $page - 1">Previous</a> -  |  - {/if} - - Page {$page} of {$lastPage} - - {if $page < $lastPage} -  |  - <a n:href="default, $page + 1">Next</a> -  |  - <a n:href="default, $lastPage">Last</a> - {/if} -</div> -``` - -De esta manera hemos implementado el mecanismo de paginación sin usar explícitamente la utilidad Paginator. - -{{priority: -1}} diff --git a/best-practices/es/passing-settings-to-presenters.texy b/best-practices/es/passing-settings-to-presenters.texy deleted file mode 100644 index c14195c5bd..0000000000 --- a/best-practices/es/passing-settings-to-presenters.texy +++ /dev/null @@ -1,49 +0,0 @@ -Pasar ajustes a los presenters -****************************** - -.[perex] -¿Necesita pasar a los presenters argumentos que no son objetos (como una bandera que indica el modo de depuración, rutas de directorios, etc.) y que no se pueden pasar automáticamente por autowiring? La solución es encapsularlos en un objeto `Settings` dedicado. - -El servicio `Settings` ofrece una forma muy sencilla pero eficaz de suministrar a los presenters información sobre la aplicación en marcha. Su estructura concreta depende por completo de sus necesidades. Ejemplo: - -```php -namespace App; - -class Settings -{ - public function __construct( - // desde PHP 8.1 se puede usar readonly - public bool $debugMode, - public string $appDir, - // y así sucesivamente - ) {} -} -``` - -Ejemplo de su registro en la configuración: - -```neon -services: - - App\Settings( - %debugMode%, - %appDir%, - ) -``` - -Cuando un presenter necesita la información que ofrece este servicio, simplemente se la pide en su constructor: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private App\Settings $settings, - ) {} - - public function renderDefault() - { - if ($this->settings->debugMode) { - // ... - } - } -} -``` diff --git a/best-practices/es/post-links.texy b/best-practices/es/post-links.texy deleted file mode 100644 index 53aa4d10ea..0000000000 --- a/best-practices/es/post-links.texy +++ /dev/null @@ -1,56 +0,0 @@ -Cómo usar correctamente los enlaces POST -**************************************** - -.[perex] -En las aplicaciones web, sobre todo en las interfaces de administración, debería regir una regla fundamental: las acciones que modifican el estado del servidor no se realizan con el método HTTP GET. Como su nombre indica, GET debería servir solo para obtener datos, no para modificarlos. Para acciones como borrar registros es más adecuado el método POST. El método ideal sería DELETE, pero no se puede invocar sin JavaScript, y por eso históricamente se ha usado POST para estas acciones. - -¿Cómo llevarlo a la práctica? Use este sencillo truco. Al principio de la plantilla del layout, cree un formulario auxiliar con el ID `postForm`. Después usará ese formulario para acciones como los botones de borrado: - -```latte .{file:@layout.latte} -<form method="post" id="postForm"></form> -``` - -Gracias a este formulario, en lugar de un enlace `<a>` corriente puede usar un `<button>`. Ese botón se puede estilizar para que parezca un enlace normal. Por ejemplo, el framework CSS Bootstrap ofrece las clases `btn btn-link`, que hacen que el botón sea visualmente indistinguible de los demás enlaces. Con el atributo `form="postForm"` enlace el botón con el formulario auxiliar preparado: - -```latte .{file:admin.latte} -<table> - <tr n:foreach="$posts as $post"> - <td>{$post->title}</td> - <td> - <button class="btn btn-link" form="postForm" formaction="{link delete, $post->id}">delete</button> - <!-- instead of <a n:href="delete, $post->id">delete</a> --> - </td> - </tr> -</table> -``` - -Al pulsar este botón se invoca ahora la acción `delete`. Para asegurarse de que las peticiones solo se aceptan por el método POST y proceden del mismo dominio (una defensa eficaz frente a los ataques CSRF), use el atributo `#[Requires]`: - -```php .{file:AdminPresenter.php} -use Nette\Application\Attributes\Requires; - -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST', sameOrigin: true)] - public function actionDelete(int $id): void - { - $this->facade->deletePost($id); // código hipotético para borrar un registro - $this->redirect('default'); - } -} -``` - -Este atributo está disponible desde Nette Application 3.2. Puede conocer mejor sus posibilidades en la página [Cómo usar el atributo #Requires |attribute-requires]. - -Si en lugar de la acción `actionDelete()` usara la señal `handleDelete()`, no hace falta indicar `sameOrigin: true`, porque las señales tienen esta protección activada de forma predeterminada: - -```php .{file:AdminPresenter.php} -#[Requires(methods: 'POST')] -public function handleDelete(int $id): void -{ - $this->facade->deletePost($id); - $this->redirect('this'); -} -``` - -Este enfoque no solo refuerza la seguridad de su aplicación, sino que además fomenta el respeto a los estándares y las prácticas correctas de la web. Usar el método POST para las acciones que cambian el estado da como resultado una aplicación más robusta y segura. diff --git a/best-practices/es/presenter-traits.texy b/best-practices/es/presenter-traits.texy deleted file mode 100644 index 72e84158db..0000000000 --- a/best-practices/es/presenter-traits.texy +++ /dev/null @@ -1,47 +0,0 @@ -Componer presenters a partir de traits -************************************** - -.[perex] -Si necesita implementar la misma funcionalidad en varios presenters (p. ej. comprobar que el usuario ha iniciado sesión), colocar el código en un antecesor común es un enfoque habitual. Otra opción es crear [traits |nette:introduction-to-object-oriented-programming#Traits] de un solo propósito. - -La ventaja de usar traits es que cada presenter puede incorporar solo los traits que realmente necesita, sobre todo teniendo en cuenta que PHP no admite la herencia múltiple. - -Estos traits pueden aprovechar que todos los [métodos inject |inject-method-attribute#Métodos inject*()] se llaman uno tras otro al crear la instancia del presenter. Solo hay que asegurarse de que el nombre de cada método inject sea único en todos los traits usados y en el propio presenter. - -Los traits pueden enganchar código de inicialización a los eventos [onStartup u onRender |application:presenters#Eventos]. - -Ejemplos: - -```php -trait RequireLoggedUser -{ - public function injectRequireLoggedUser(): void - { - $this->onStartup[] = function () { - if (!$this->getUser()->isLoggedIn()) { - $this->redirect('Sign:in', $this->storeRequest()); - } - }; - } -} - -trait StandardTemplateFilters -{ - public function injectStandardTemplateFilters(TemplateBuilder $builder): void - { - $this->onRender[] = function () use ($builder) { - $builder->setupTemplate($this->template); - }; - } -} -``` - -El presenter después simplemente usa estos traits: - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - use StandardTemplateFilters; - use RequireLoggedUser; -} -``` diff --git a/best-practices/es/pretty-urls.texy b/best-practices/es/pretty-urls.texy deleted file mode 100644 index 8616ddec6b..0000000000 --- a/best-practices/es/pretty-urls.texy +++ /dev/null @@ -1,204 +0,0 @@ -URLs amigables con slugs -************************ - -.[perex] -Las URL como `/article/123-how-to-bake-bread` quedan mejor que `/article/123` y ayudan tanto a los usuarios como a los buscadores a entender qué hay en la página. Esta guía muestra cómo generarlas enteramente en el router, sin tocar ni una sola plantilla, y cómo asegurarse de que todos los visitantes acaben en la URL canónica. - - -Por qué usar slugs en las URL -============================= - -Compare estas dos direcciones: - -``` -/article/123 -/article/123-how-to-bake-bread -``` - -La segunda le dice al usuario (y a Google) qué le espera tras el clic. Es buena para el SEO, hace que los enlaces sean legibles en un chat o un correo y da algún sentido a la barra de direcciones. - -El slug, sin embargo, no es un identificador real. La página la determina el ID. El slug es un adorno que la aplicación genera a partir del título. Si el título cambia, el slug debería cambiar también. Y si alguien edita la URL a mano o sigue un enlace antiguo, la aplicación debería encontrar igualmente la página correcta. - - -El objetivo -=========== - -Queremos una ruta que gestione todo esto: - -``` -/article/123 → opens article 123, redirects to canonical URL -/article/123-how-to-bake-bread → opens article 123 directly -/article/123-anything-someone-typed → opens article 123, redirects to canonical URL -/article/ → 404 (no ID) -``` - -Y queremos que cada `n:href` y cada llamada a `link()` de la aplicación produzcan automáticamente `/article/123-how-to-bake-bread`, **sin reescribir ni una sola plantilla**. - - -La máscara de la ruta -===================== - -El truco está en marcar el slug como **opcional** en la máscara mediante corchetes: - -```php -$router->addRoute('article/<id [0-9]+>[-<slug>]', 'Article:detail'); -``` - -La máscara `[-<slug>]` dice: tras el ID puede haber un guion y un slug, pero no es obligatorio. La ruta acepta tanto `/article/123` como `/article/123-anything`. - -Una nota sobre el parámetro `<slug>`: de forma predeterminada acepta cualquier carácter **salvo la barra**, que es justo lo que queremos. Si escribe `<slug .+>`, el parámetro aceptará también barras, así que `/article/123-something/else` se interpretaría como un único slug que contiene `/`. Quédese con el `<slug>` predeterminado a no ser que realmente necesite lo otro. - -Hasta aquí la URL se interpreta correctamente, pero los enlaces generados no contendrán el slug. El siguiente paso es enseñar a la ruta cómo rellenarlo. - - -Generar el slug sin tocar las plantillas -======================================== - -Esta es la variante estrella. Las llamadas `n:href="Article:detail, $id"` existentes siguen funcionando sin cambios en toda la aplicación: el router busca el título por su cuenta. - -Lo hacemos con un **filtro general** bajo la clave de cadena vacía: ve todos los parámetros a la vez y puede añadir el slug: - -```php -use Nette\Routing\Route; -use Nette\Utils\Strings; - -$router->addRoute('article/<id [0-9]+>[-<slug>]', [ - 'presenter' => 'Article', - 'action' => 'detail', - '' => [ - Route::FilterOut => function (array $params) use ($slugProvider): array { - if (isset($params['id']) && empty($params['slug'])) { - $params['slug'] = $slugProvider->getSlug((int) $params['id']); - } - return $params; - }, - ], -]); -``` - -`FilterOut` se ejecuta cada vez que el router **genera** una URL. Si no se le pasó el slug, el filtro busca el título y lo añade. - -Puede desplegar los slugs por toda una aplicación con un único cambio: una sola definición de ruta. Todos los enlaces de todas las plantillas empiezan a producir `/article/123-how-to-bake-bread` automáticamente. Sin greps, sin buscar por las plantillas, sin ningún caso olvidado. - - -Guardar la búsqueda en caché -============================ - -Un enlace genera una consulta a la base de datos, pero una página típica tiene muchos: listados, migas de pan, "vistos recientemente", artículos relacionados. El mismo ID de artículo aparece a menudo en varios enlaces durante una única petición, y no quiere ir a la base de datos cada vez. - -Una pequeña caché por petición lo resuelve. Envuelva la llamada a la base de datos en un pequeño servicio: - -```php -final class SlugProvider -{ - /** @var array<int, string> */ - private array $cache = []; - - public function __construct( - private Nette\Database\Explorer $db, - ) { - } - - public function getSlug(int $id): string - { - return $this->cache[$id] ??= Strings::webalize(Strings::truncate( - (string) $this->db->fetchField('SELECT title FROM article WHERE id = ?', $id), - 100, '' - )); - } -} -``` - -Con esto basta: una consulta a la base de datos por cada ID distinto y petición. - - -Pasar el título desde la plantilla (atajo opcional) -=================================================== - -Cuando el título ya está a mano en la plantilla, puede saltarse por completo la consulta a la base de datos. Pase el título como parámetro con nombre: - -```latte -<a n:href="Article:detail, $article->id, slug => $article->title">{$article->title}</a> -``` - -…y añada un `FilterOut` para ese parámetro que convierta el título en una cadena apta para la URL: - -```php -$router->addRoute('article/<id [0-9]+>[-<slug>]', [ - 'presenter' => 'Article', - 'action' => 'detail', - 'slug' => [ - Route::FilterOut => fn($title) => Strings::webalize(Strings::truncate($title, 100, '')), - ], - '' => [/* el plan B de búsqueda de arriba */], -]); -``` - -Los dos filtros colaboran. El filtro general se ejecuta primero; al ver que el slug ya está relleno con el título indicado, se salta la consulta. El `FilterOut` del parámetro convierte después ese título en un slug como es debido. Las plantillas que no pasan el título siguen funcionando: el filtro general encuentra el slug vacío y recurre a la búsqueda. - -Use esto solo donde importe (listados grandes que se renderizan cientos de veces por petición). Para la mayor parte de la aplicación, la búsqueda con caché es lo bastante rápida. - - -Canonización: redirigir a la URL correcta -========================================= - -Ya sabemos generar `/article/123-how-to-bake-bread`, pero la ruta sigue aceptando `/article/123` y `/article/123-anything-someone-wrote`. Es intencionado: queremos URL cortas (más sobre esto abajo) y queremos que los enlaces antiguos o escritos a mano sigan funcionando. Pero no queremos que los buscadores indexen el mismo artículo bajo varias direcciones. - -La solución es la [canonización |application:presenters#Canonización]: cuando el usuario llega por una URL no canónica, la aplicación lo redirige con un 301 a la correcta. De esto se encarga el método `canonicalize()`: - -```php -public function actionDetail(int $id, ?string $slug = null): void -{ - $article = $this->facade->getArticle($id); - if (!$article) { - $this->error(); - } - - // genera la URL canónica mediante el mismo FilterOut - // y redirige con un HTTP 301 si difiere de la URL actual - $this->canonicalize('detail', ['id' => $id]); - - $this->template->article = $article; -} -``` - -`canonicalize()` genera la URL canónica igual que lo haría `link()` (así que pasa por el mismo `FilterOut`) y la compara con la URL actual. Si difieren, redirige con un HTTP 301. Los visitantes acaban en la URL correcta y los buscadores ven una única versión canónica. - - -Un único lugar que decide cómo es el slug -========================================= - -Fíjese en que la llamada `Strings::webalize(Strings::truncate(..., 100, ''))` vive en un único lugar, dentro de `SlugProvider` (o del `FilterOut` del parámetro). La misma lógica produce el enlace de la plantilla, la URL de `redirect()` y la forma canónica de `canonicalize()`. - -Si más adelante quiere cambiar las reglas (otro límite de longitud, otra transliteración, eliminar caracteres adicionales), cambia una línea. Sin esto correría el riesgo de que `redirect()` generara `/article/123-how-to-bake-bread` mientras `canonicalize()` espera `/article/123-how-to-bake-bre` (porque alguien aplicó en otro sitio una longitud distinta en `truncate`), y la aplicación redirigiría en bucle. - - -Extra: las URL cortas siguen funcionando -======================================== - -Como el slug es opcional, las direcciones sin él siguen funcionando: - -``` -/article/123 -``` - -Esto es útil para: -- **códigos QR**: una URL más corta significa un código menos denso y más fácil de escanear -- **SMS y chats**: cabe en un tuit y queda ordenado -- **materiales impresos**: una URL corta se teclea antes - -Cuando un usuario abre una URL así, `canonicalize()` lo redirige con un 301 a la versión completa con el slug, de modo que los buscadores siguen viendo solo la forma canónica. Puede tener brevedad y SEO al mismo tiempo. - - -Resumen -======= - -- La máscara `<id>[-<slug>]` hace opcional el slug. El `<slug>` predeterminado no acepta `/`; use `<slug .+>` solo si de verdad quiere barras en el slug. -- Un `FilterOut` general bajo la clave `''` busca el título por el ID: **ningún cambio en las plantillas de toda la aplicación**. -- Envuelva la búsqueda en una pequeña caché por petición; una consulta a la base de datos por ID distinto es más que suficiente. -- Opcionalmente, un `FilterOut` para el parámetro permite que las plantillas pasen el título directamente y se salten la búsqueda. -- `$this->canonicalize()` en la acción redirige las URL no canónicas a la correcta con un HTTP 301. -- La fórmula del slug (`webalize` + `truncate`) vive en un único lugar: cámbiela una vez y surtirá efecto en todas partes. -- Las URL cortas solo con el ID siguen funcionando, lo que viene bien para códigos QR y SMS. - -Encontrará más sobre los filtros y la canonización en la documentación del [enrutamiento |application:routing#Filtros generales] y de los [presenters |application:presenters#Canonización]. diff --git a/best-practices/es/restore-request.texy b/best-practices/es/restore-request.texy deleted file mode 100644 index 3325951191..0000000000 --- a/best-practices/es/restore-request.texy +++ /dev/null @@ -1,62 +0,0 @@ -¿Cómo volver a una página anterior? -*********************************** - -.[perex] -¿Qué pasa si un usuario está rellenando un formulario y su sesión de acceso caduca? Para no perder los datos, podemos guardar la petición actual (incluidos los datos del formulario) en la sesión antes de redirigir a la página de acceso. En Nette esto es sorprendentemente fácil. - -La petición actual se puede guardar en la sesión con el método `storeRequest()`. Este método devuelve un identificador único (una cadena corta) de la petición guardada. El método guarda el nombre del presenter actual, su vista y sus parámetros. Si como parte de la petición se envió un formulario, se guardan también los valores introducidos en los campos (salvo los archivos subidos). - -La petición se restaura con el método `restoreRequest($key)`, al que se le pasa el identificador obtenido antes. Este método redirige al usuario de vuelta al presenter y la vista originales. Sin embargo, si la petición guardada incluía el envío de un formulario, `restoreRequest()` usa el método `forward()` en lugar de redirigir. Devuelve al formulario los valores rellenados anteriormente y permite renderizarlo de nuevo. Así, el usuario puede volver a enviar el formulario sin perder ninguno de los datos introducidos. - -Es crucial que `restoreRequest()` comprueba que el usuario recién conectado es el mismo que envió originalmente el formulario. Si es otro usuario, la petición guardada no se restaura y el método no hace nada, lo que refuerza la seguridad. - -Ilustrémoslo con un ejemplo. Imagine un `AdminPresenter` en el que se editan datos. Su método `startup()` comprueba si el usuario está conectado. Si no lo está, se le redirige al `SignPresenter`. Al mismo tiempo guardamos la petición actual con `storeRequest()` y le pasamos su clave (el `$backlink`) al `SignPresenter`. - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - protected function startup() - { - parent::startup(); - - if (!$this->user->isLoggedIn()) { - $this->redirect('Sign:in', ['backlink' => $this->storeRequest()]); - } - } -} -``` - -El `SignPresenter` contendrá, además del formulario de acceso, un parámetro persistente `$backlink` donde se guarda la clave. Como el parámetro es persistente, su valor se conserva incluso después de enviar el formulario de acceso. - - -```php -use Nette\Application\Attributes\Persistent; - -class SignPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $backlink = ''; - - protected function createComponentSignInForm() - { - $form = new Nette\Application\UI\Form; - // ... añadir los campos del formulario ... - $form->onSuccess[] = $this->signInFormSuceeded(...); - return $form; - } - - private function signInFormSuceeded($form) - { - // ... aquí se autentica al usuario ... - - $this->restoreRequest($this->backlink); - $this->redirect('Admin:'); - } -} -``` - -Al método `restoreRequest()` le pasamos la clave (`$this->backlink`) de la petición guardada. El método redirige (o hace forward) al usuario de vuelta al presenter y la vista originales. - -Si la clave no es válida (por ejemplo, porque ha caducado en la sesión), el método no hace nada. Por eso la llamada siguiente `$this->redirect('Admin:')` actúa como plan B y redirige a una página predeterminada, como el `AdminPresenter`. - -{{priority: -1}} diff --git a/best-practices/fr/@home.texy b/best-practices/fr/@home.texy deleted file mode 100644 index b18699884c..0000000000 --- a/best-practices/fr/@home.texy +++ /dev/null @@ -1,69 +0,0 @@ -Tutoriels et bonnes pratiques -***************************** - -.[perex] -Tutoriels, solutions aux tâches courantes et bonnes pratiques pour Nette. - - -<div class=documentation> -<div> - - -Nette Application ------------------ -- [Méthodes et attributs inject |inject-method-attribute] -- [Composer des presenters à partir de traits |presenter-traits] -- [Passer des réglages aux presenters |passing-settings-to-presenters] -- [Comment restaurer une requête |restore-request] -- [Paginer les résultats de base de données |pagination] -- [Snippets dynamiques |dynamic-snippets] -- [Comment utiliser l'attribut #Requires |attribute-requires] -- [Comment bien utiliser les liens POST |post-links] -- [URLs élégantes avec slugs |pretty-urls] - -</div> -<div> - - -Formulaires ------------ -- [Réutiliser les formulaires |form-reuse] -- [Formulaire de création et d'édition d'enregistrements |creating-editing-form] -- [Créons un formulaire de contact |lets-create-contact-form] -- [Listes déroulantes dépendantes |https://blog.nette.org/fr/dependent-selectboxes-elegantly-in-nette-and-pure-js] - -</div> -<div> - - -Général -------- -- [Comment charger un fichier de configuration |bootstrap:] -- [Comment écrire des microsites |microsites] -- [Pourquoi Nette utilise-t-il la notation PascalCase pour les constantes ? |https://blog.nette.org/fr/for-less-screaming-in-the-code] -- [Pourquoi Nette n'utilise-t-il pas le suffixe Interface ? |https://blog.nette.org/fr/prefixes-and-suffixes-do-not-belong-in-interface-names] -- [Composer : conseils d'utilisation |composer] -- [Introduction à la programmation orientée objet |nette:introduction-to-object-oriented-programming] - -</div> -<div> - - -Exemples de solutions ---------------------- -- [Exemples Nette |https://github.com/nette-examples] -- [Doctrine & Nette |https://contributte.org/nettrine/] -- [Exemples Contributte |https://contributte.org/examples.html] -- [Site web Doctrine ORM |https://github.com/MinecordNetwork/Website] -- [Démarrage rapide |quickstart:] - -</div> -<div> - - -Vidéos ------- -Des centaines d'enregistrements des rencontres Last Saturday et de vidéos sur Nette se trouvent au même endroit, sur la "chaîne YouTube Nette Framework":https://www.youtube.com/user/NetteFramework. - -</div> -</div> diff --git a/best-practices/fr/@left-menu.texy b/best-practices/fr/@left-menu.texy deleted file mode 100644 index 96d5e6f5ff..0000000000 --- a/best-practices/fr/@left-menu.texy +++ /dev/null @@ -1,34 +0,0 @@ -Tutoriels et bonnes pratiques -***************************** -- [Introduction |@home] - -Nette Application -***************** -- [Méthodes et attributs inject |inject-method-attribute] -- [Composer des presenters à partir de traits |presenter-traits] -- [Passer des réglages aux presenters |passing-settings-to-presenters] -- [Comment restaurer une requête |restore-request] -- [Paginer les résultats de base de données |pagination] -- [Snippets dynamiques |dynamic-snippets] -- [Comment utiliser l'attribut #Requires |attribute-requires] -- [Comment bien utiliser les liens POST |post-links] -- [URLs élégantes avec slugs |pretty-urls] - -Formulaires -*********** -- [Réutiliser les formulaires |form-reuse] -- [Formulaire de création et d'édition d'enregistrements |creating-editing-form] -- [Créons un formulaire de contact |lets-create-contact-form] - -Général -******* -- [Comment écrire des microsites |microsites] -- [Composer : conseils d'utilisation |composer] - - -Pour aller plus loin -******************** -- [Documentation Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Résolution de problèmes |nette:troubleshooting] diff --git a/best-practices/fr/@meta.texy b/best-practices/fr/@meta.texy deleted file mode 100644 index 12ceedd5d5..0000000000 --- a/best-practices/fr/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Tutoriels et bonnes pratiques}} diff --git a/best-practices/fr/attribute-requires.texy b/best-practices/fr/attribute-requires.texy deleted file mode 100644 index 803289ae62..0000000000 --- a/best-practices/fr/attribute-requires.texy +++ /dev/null @@ -1,177 +0,0 @@ -Comment utiliser l'attribut `#[Requires]` -***************************************** - -.[perex] -Quand vous écrivez une application web, vous rencontrez souvent le besoin de restreindre l'accès à certaines parties de votre application. Vous voulez peut-être que certaines requêtes ne puissent envoyer des données que par un formulaire (donc par la méthode POST), ou qu'elles ne soient accessibles qu'aux appels AJAX. Nette Framework 3.2 a introduit un nouvel outil qui vous permet de poser de telles restrictions de façon élégante et claire : l'attribut `#[Requires]`. - -Un attribut est un marqueur particulier en PHP que vous ajoutez avant la définition d'une classe ou d'une méthode. Comme il s'agit au fond d'une classe, vous devez ajouter la clause `use` pour que les exemples suivants fonctionnent : - -```php -use Nette\Application\Attributes\Requires; -``` - -Vous pouvez utiliser l'attribut `#[Requires]` sur la classe du presenter elle-même et sur ces méthodes : - -- `action<Action>()` -- `render<View>()` -- `handle<Signal>()` -- `createComponent<Name>()` - -Les deux dernières méthodes valent aussi pour les composants, vous pouvez donc utiliser l'attribut avec eux également. - -Si les conditions posées par l'attribut ne sont pas remplies, une erreur HTTP 4xx est déclenchée. - - -Méthodes HTTP -------------- - -Vous pouvez indiquer quelles méthodes HTTP (comme GET, POST, etc.) sont autorisées pour l'accès. Par exemple, si vous voulez n'autoriser l'accès que par l'envoi d'un formulaire, écrivez : - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST')] - public function actionDelete(int $id): void - { - } -} -``` - -Pourquoi utiliser POST plutôt que GET pour les actions qui changent l'état, et comment faire ? [Lisez le guide |post-links]. - -Vous pouvez indiquer une méthode ou un tableau de méthodes. Un cas particulier est la valeur `'*'`, qui autorise toutes les méthodes, ce que les presenters [n'autorisent pas par défaut pour des raisons de sécurité |application:presenters#Vérification de la méthode HTTP]. - - -Appels AJAX ------------ - -Si vous voulez qu'un presenter ou une méthode ne soit accessible qu'aux requêtes AJAX, utilisez : - -```php -#[Requires(ajax: true)] -class AjaxPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Même origine ------------- - -Pour renforcer la sécurité, vous pouvez exiger que la requête provienne du même domaine. Cela évite la [faille 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<Signal>()`, l'accès depuis le même domaine est exigé automatiquement. Si vous voulez donc autoriser l'accès depuis n'importe quel domaine, indiquez : - -```php -#[Requires(sameOrigin: false)] -public function handleList(): void -{ -} -``` - - -Accès par forward ------------------ - -Il est parfois utile de restreindre l'accès à un presenter de sorte qu'il ne soit accessible qu'indirectement, par exemple à l'aide des méthodes `forward()` ou `switch()` depuis un autre presenter. C'est ainsi que sont protégés, par exemple, les presenters d'erreur, afin qu'ils ne puissent pas être déclenchés depuis une URL : - -```php -#[Requires(forward: true)] -class ForwardedPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -En pratique, il est souvent nécessaire de marquer certaines vues qui ne sont accessibles que sur la base d'une logique dans le presenter. Là encore, pour qu'elles ne puissent 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 précises ----------------- - -Vous pouvez aussi restreindre certain code, comme la création d'un composant, aux seules actions précises du presenter : - -```php -class EditDeletePresenter extends Nette\Application\UI\Presenter -{ - #[Requires(actions: ['add', 'edit'])] - public function createComponentPostForm() - { - } -} -``` - -Dans le cas d'une seule action, il n'est pas nécessaire d'écrire un tableau : `#[Requires(actions: 'default')]` - - -Attributs personnalisés ------------------------ - -Si vous voulez utiliser l'attribut `#[Requires]` à plusieurs reprises avec les mêmes réglages, vous pouvez créer votre propre attribut qui hérite de `#[Requires]` et le configure selon vos besoins. - -Par exemple, `#[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]` autorisera l'accès par toutes les méthodes HTTP utilisées pour une 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 ----------- - -L'attribut `#[Requires]` vous donne une grande souplesse et un vrai contrôle sur la façon dont vos pages web sont accessibles. À l'aide de règles simples mais puissantes, vous pouvez renforcer la sécurité et le bon fonctionnement de votre application. Comme vous le voyez, l'usage des attributs dans Nette peut non seulement simplifier votre travail, mais aussi le sécuriser. diff --git a/best-practices/fr/composer.texy b/best-practices/fr/composer.texy deleted file mode 100644 index dc80f39788..0000000000 --- a/best-practices/fr/composer.texy +++ /dev/null @@ -1,282 +0,0 @@ -Composer : conseils d'utilisation -********************************* - -<div class=perex> - -Composer est un outil de gestion des dépendances en PHP. Il vous permet de déclarer les bibliothèques dont dépend votre projet et il les installera et les mettra à jour pour vous. Nous allons apprendre : - -- comment installer Composer -- comment l'utiliser dans un projet nouveau ou existant - -</div> - - -Installation -============ - -Composer est un fichier exécutable `.phar` que vous téléchargez et installez comme suit. - - -Windows -------- - -Utilisez l'installateur officiel [Composer-Setup.exe|https://getcomposer.org/Composer-Setup.exe]. - - -Linux, macOS ------------- - -Il vous suffit de 4 commandes, que vous pouvez copier depuis [cette page |https://getcomposer.org/download/]. - -De plus, en le copiant dans un dossier figurant dans le `PATH` du système, Composer devient accessible globalement : - -```shell -$ mv ./composer.phar ~/bin/composer # ou /usr/local/bin/composer -``` - - -Utilisation dans un projet -========================== - -Pour commencer à utiliser Composer dans votre projet, il vous suffit d'un fichier `composer.json`. Ce fichier décrit les dépendances de votre projet et peut aussi contenir d'autres métadonnées. Le `composer.json` le plus simple peut ressembler à ceci : - -```js -{ - "require": { - "nette/database": "^3.0" - } -} -``` - -Nous disons ici que notre application (ou bibliothèque) requiert le paquet `nette/database` (le nom du paquet se compose du nom du fournisseur et du nom du projet) et qu'elle veut une version correspondant à la contrainte `^3.0` (c'est-à-dire la dernière version 3). - -Avec le fichier `composer.json` à la racine du projet, exécutez donc : - -```shell -composer update -``` - -Composer téléchargera Nette Database dans le répertoire `vendor/`. Il crée aussi un fichier `composer.lock`, qui contient l'information sur les versions exactes des bibliothèques installées. - -Composer génère un fichier `vendor/autoload.php`. Il vous suffit d'inclure ce fichier pour commencer à utiliser les classes des bibliothèques sans travail supplémentaire : - -```php -require __DIR__ . '/vendor/autoload.php'; - -$db = new Nette\Database\Connection('sqlite::memory:'); -``` - - -Mettre les paquets à jour vers les dernières versions -===================================================== - -Pour mettre à jour les bibliothèques utilisées vers les dernières versions autorisées par les contraintes définies dans `composer.json`, utilisez la commande `composer update`. Par exemple, avec la dépendance `"nette/database": "^3.0"`, il installera la dernière version 3.x.x, mais pas la version 4. - -Pour mettre à jour les contraintes du fichier `composer.json`, par exemple vers `"nette/database": "^4.1"`, ce qui permet d'installer la dernière version, utilisez la commande `composer require nette/database`. - -Pour mettre à jour tous les paquets Nette utilisés, il faudrait tous les énumérer sur la ligne de commande, par exemple : - -```shell -composer require nette/application nette/forms latte/latte tracy/tracy ... -``` - -Ce n'est pas pratique. Utilisez donc le petit script "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff, qui le fera pour vous : - -```shell -php composer-frontline.php -``` - - -Créer un nouveau projet -======================= - -Vous pouvez créer un nouveau projet Nette en une seule commande : - -```shell -composer create-project nette/web-project nom-du-projet -``` - -Remplacez `nom-du-projet` par le nom du répertoire de votre projet et exécutez la commande. Composer téléchargera depuis GitHub le dépôt `nette/web-project`, qui contient déjà un fichier `composer.json`, puis installera Nette Framework lui-même. Il ne reste plus qu'à [régler les permissions des répertoires |nette:troubleshooting#Régler les permissions des répertoires] `temp/` et `log/`, et le projet devrait être opérationnel. - -Si vous savez sur quelle version de PHP votre projet sera hébergé, pensez à l'[indiquer |#Version de PHP]. - - -Version de PHP -============== - -Composer installe toujours les versions de paquets compatibles avec la version de PHP que vous utilisez actuellement (plus précisément la version de PHP employée en ligne de commande lors de l'exécution de Composer). Ce n'est peut-être pas la même version que celle de votre hébergement web. C'est pourquoi il est essentiel d'ajouter au fichier `composer.json` l'information sur la version de PHP de votre hébergement. Seules les versions de paquets compatibles avec l'hébergement seront alors installées. - -Par exemple, pour indiquer que le projet tournera sur PHP 8.2.3, utilisez la commande : - -```shell -composer config platform.php 8.2.3 -``` - -La version sera écrite dans le fichier `composer.json` de cette façon : - -```js -{ - "config": { - "platform": { - "php": "8.2.3" - } - } -} -``` - -Le numéro de version de PHP est cependant aussi indiqué ailleurs dans le fichier, dans la section `require`. Alors que le premier nombre détermine la version pour laquelle les paquets sont installés, le second indique la version pour laquelle l'application elle-même est écrite. PhpStorm s'en sert par exemple pour régler le *PHP language level*. (Bien sûr, il n'y a pas de sens à ce que ces versions diffèrent, cette double entrée est donc une maladresse.) Définissez cette version à l'aide de la commande : - -```shell -composer require php 8.2.3 --no-update -``` - -Ou directement dans le fichier `composer.json` : - -```js -{ - "require": { - "php": "8.2.3" - } -} -``` - - -Ignorer la version de PHP -========================= - -Les paquets indiquent généralement à la fois la version de PHP la plus basse avec laquelle ils sont compatibles et la version la plus élevée avec laquelle ils ont été testés. Si vous avez l'intention d'utiliser une version de PHP encore plus récente, par exemple pour tester, Composer refusera d'installer un tel paquet. La solution est l'option `--ignore-platform-req=php+`, qui fait ignorer à Composer les limites supérieures de la version de PHP requise. - - -Faux signalements -================= - -Lors de la mise à jour des paquets ou du changement des numéros de version, des conflits surviennent parfois. Un paquet a des exigences qui entrent en conflit avec un autre, et ainsi de suite. Composer produit cependant parfois de faux signalements. Il annonce un conflit qui n'existe pas en réalité. Dans ce cas, supprimer le fichier `composer.lock` et réessayer peut aider. - -Si le message d'erreur persiste, il est authentique et vous devez le lire pour comprendre quoi modifier et comment. - - -Packagist.org - le dépôt global -=============================== - -[Packagist |https://packagist.org] est le dépôt principal dans lequel Composer cherche les paquets par défaut. Vous pouvez aussi y publier vos propres paquets. - - -Et si nous ne voulons pas du dépôt central ------------------------------------------- - -Si nous avons dans notre entreprise des applications ou des bibliothèques internes qui ne peuvent pas être hébergées publiquement, nous pouvons créer nos propres dépôts pour elles. - -Pour en savoir plus sur les dépôts, voir [la documentation officielle |https://getcomposer.org/doc/05-repositories.md#repositories]. - - -Autoloading -=========== - -Une fonctionnalité clé de Composer est qu'il fournit l'autoloading de toutes les classes qu'il installe. Vous l'activez en incluant le fichier `vendor/autoload.php`. - -Vous pouvez cependant aussi utiliser Composer pour charger d'autres classes situées en dehors du répertoire `vendor/`. La première possibilité est de laisser Composer parcourir des répertoires et sous-répertoires définis, y trouver toutes les classes et les inclure dans l'autoloader. Pour cela, définissez `autoload > classmap` dans `composer.json` : - -```js -{ - "autoload": { - "classmap": [ - "src/", # inclut le répertoire src/ et ses sous-répertoires - ] - } -} -``` - -Il faut ensuite exécuter la commande `composer dumpautoload` après chaque changement pour régénérer les tables d'autoloading. C'est extrêmement peu pratique. Il vaut bien mieux confier cette tâche à [RobotLoader|robot-loader:], qui effectue la même activité automatiquement en arrière-plan et bien plus vite. - -La deuxième possibilité est de respecter [PSR-4 |https://www.php-fig.org/psr/psr-4/]. Dit simplement, c'est un système où les espaces de noms et les noms de classes correspondent à la structure des répertoires et aux noms de fichiers, par exemple `App\Core\RouterFactory` se trouvera dans le fichier `/path/to/App/Core/RouterFactory.php`. Exemple de configuration : - -```js -{ - "autoload": { - "psr-4": { - "App\\": "app/" # l'espace de noms App\ est dans le répertoire app/ - } - } -} -``` - -Voir la [documentation de Composer |https://getcomposer.org/doc/04-schema.md#psr-4] pour les détails de configuration de ce comportement. - - -Tester de nouvelles versions -============================ - -Vous voulez tester une nouvelle version de développement d'un paquet ? Voici comment. Ajoutez d'abord cette paire d'options à votre fichier `composer.json`. Elle permet d'installer des versions de développement, mais Composer n'y aura recours que si aucune combinaison de versions stables ne satisfait les exigences : - -```js -{ - "minimum-stability": "dev", - "prefer-stable": true, -} -``` - -Nous recommandons aussi de supprimer le fichier `composer.lock`, car Composer refuse parfois l'installation sans raison apparente et cela peut régler le problème. - -Disons que le paquet est `nette/utils` et que la nouvelle version est la 4.0. Installez-la à l'aide de la commande : - -```shell -composer require nette/utils:4.0.x-dev -``` - -Ou vous pouvez installer une version précise, par exemple la 4.0.0-RC2 : - -```shell -composer require nette/utils:4.0.0-RC2 -``` - -Si un autre paquet dépend cependant de la bibliothèque et est verrouillé sur une version plus ancienne (par exemple `^3.1`), la solution idéale est de mettre à jour ce paquet dépendant pour qu'il fonctionne avec la nouvelle version. Mais si vous voulez simplement contourner la restriction et forcer Composer à installer la version de développement en la faisant passer pour une version plus ancienne (par exemple 3.1.6), vous pouvez utiliser le mot-clé `as` : - -```shell -composer require nette/utils "4.0.x-dev as 3.1.6" -``` - - -Appeler des commandes -===================== - -Vous pouvez appeler vos propres commandes et scripts prédéfinis via Composer comme s'il s'agissait de commandes natives de Composer. Pour les scripts situés dans le répertoire `vendor/bin`, vous n'avez pas besoin d'indiquer ce chemin. - -Définissons par exemple dans `composer.json` un script qui utilise [Nette Tester |tester:] pour lancer les tests : - -```js -{ - "scripts": { - "tester": "tester tests -s" - } -} -``` - -Nous lançons ensuite les tests avec `composer tester`. Vous pouvez appeler la commande même si vous n'êtes pas dans le répertoire racine du projet, mais dans l'un de ses sous-répertoires. - - -Dire merci -========== - -Voici une astuce pour faire plaisir aux auteurs open source. Vous pouvez facilement donner des étoiles sur GitHub aux bibliothèques que votre projet utilise. Il suffit d'installer la bibliothèque `symfony/thanks` : - -```shell -composer global require symfony/thanks -``` - -Puis d'exécuter : - -```shell -composer thanks -``` - -Essayez ! - - -Configuration -============= - -Composer est étroitement intégré à l'outil de gestion de versions [Git |https://git-scm.com]. Si Git n'est pas installé, vous devez indiquer à Composer de ne pas l'utiliser : - -```shell -composer -g config preferred-install dist -``` diff --git a/best-practices/fr/creating-editing-form.texy b/best-practices/fr/creating-editing-form.texy deleted file mode 100644 index b9904f4a77..0000000000 --- a/best-practices/fr/creating-editing-form.texy +++ /dev/null @@ -1,204 +0,0 @@ -Formulaire de création et d'édition d'enregistrements -***************************************************** - -.[perex] -Comment implémenter correctement dans Nette l'ajout et l'édition d'un enregistrement, en utilisant le même formulaire pour les deux ? - -Dans bien des cas, les formulaires d'ajout et d'édition d'un enregistrement sont identiques et ne diffèrent peut-être que par le libellé du bouton. Nous montrerons des exemples de presenters simples où nous utiliserons le formulaire d'abord pour ajouter un enregistrement, puis pour le modifier, et enfin nous combinerons les deux solutions. - - -Ajouter un enregistrement -------------------------- - -Exemple d'un presenter d'ajout d'enregistrement. Nous laisserons le travail réel avec la base de données à une classe `Facade` dont le code n'est pas essentiel pour cet exemple. - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentRecordForm(): Form - { - $form = new Form; - - // ... ajout des champs du formulaire ... - - $form->onSuccess[] = $this->recordFormSucceeded(...); - return $form; - } - - private function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // ajoute l'enregistrement dans la base de données - $this->flashMessage('Ajouté avec succès'); - $this->redirect('...'); - } - - public function renderAdd(): void - { - // ... - } -} -``` - - -Éditer un enregistrement ------------------------- - -Voyons maintenant à quoi ressemblerait un presenter d'édition d'enregistrement : - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - private $record; - - public function __construct( - private Facade $facade, - ) { - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // vérifie l'existence de l'enregistrement - || !$this->facade->isEditAllowed(/*...*/) // vérifie les permissions - ) { - $this->error(); // erreur 404 - } - - $this->record = $record; - } - - protected function createComponentRecordForm(): Form - { - // vérifie que l'action est bien 'edit' - if ($this->getAction() !== 'edit') { - $this->error(); - } - - $form = new Form; - - // ... ajout des champs du formulaire ... - - $form->setDefaults($this->record); // définit les valeurs par défaut - $form->onSuccess[] = $this->recordFormSucceeded(...); - return $form; - } - - private function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->update($this->record->id, $data); // met à jour l'enregistrement - $this->flashMessage('Mis à jour avec succès'); - $this->redirect('...'); - } -} -``` - -Dans la méthode *action*, appelée au début du [cycle de vie du presenter |application:presenters#Cycle de vie du presenter], nous vérifions l'existence de l'enregistrement et le droit de l'utilisateur à le modifier. - -Nous stockons l'enregistrement dans la propriété `$record`, ce qui le rend disponible dans la méthode `createComponentRecordForm()` pour définir les valeurs par défaut, et dans `recordFormSucceeded()` pour accéder à l'ID. Une solution alternative consiste à définir les valeurs par défaut directement dans `actionEdit()` et à récupérer la valeur de l'ID (qui fait partie de l'URL) à l'aide de `getParameter('id')` : - - -```php - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - // vérifie l'existence et les permissions - ) { - $this->error(); - } - - // définit les valeurs par défaut du formulaire - $this->getComponent('recordForm') - ->setDefaults($record); - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); - // ... - } -``` - -Cependant, et ce devrait être **l'enseignement le plus important de tout ce code**, nous devons nous assurer que l'action est bien `edit` lors de la création du formulaire. Sinon, la vérification effectuée dans la méthode `actionEdit()` n'aurait pas lieu du tout ! - - -Même formulaire pour l'ajout et l'édition ------------------------------------------ - -Combinons maintenant les deux presenters en un seul. Nous pourrions soit distinguer l'action dans la méthode `createComponentRecordForm()` et configurer le formulaire en conséquence, soit déléguer cela directement aux méthodes d'action et supprimer la condition : - - -```php -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - public function actionAdd(): void - { - $form = $this->getComponent('recordForm'); - $form->onSuccess[] = $this->addingFormSucceeded(...); - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // vérifie l'existence de l'enregistrement - || !$this->facade->isEditAllowed(/*...*/) // vérifie les permissions - ) { - $this->error(); // erreur 404 - } - - $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // définit les valeurs par défaut - $form->onSuccess[] = $this->editingFormSucceeded(...); - } - - protected function createComponentRecordForm(): Form - { - // vérifie que l'action est 'add' ou 'edit' - if (!in_array($this->getAction(), ['add', 'edit'])) { - $this->error(); - } - - $form = new Form; - - // ... ajout des champs du formulaire ... - - return $form; - } - - private function addingFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // ajoute l'enregistrement dans la base de données - $this->flashMessage('Ajouté avec succès'); - $this->redirect('...'); - } - - private function editingFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // met à jour l'enregistrement - $this->flashMessage('Mis à jour avec succès'); - $this->redirect('...'); - } -} -``` - -{{priority: -1}} diff --git a/best-practices/fr/dynamic-snippets.texy b/best-practices/fr/dynamic-snippets.texy deleted file mode 100644 index ed6f1cdd8e..0000000000 --- a/best-practices/fr/dynamic-snippets.texy +++ /dev/null @@ -1,176 +0,0 @@ -Snippets dynamiques -******************* - -.[perex] -Comment utiliser AJAX pour ne rafraîchir que les parties d'une page qui changent réellement, par exemple les différents éléments d'une liste, à l'aide des snippets dynamiques de Latte. - -Au cours du développement d'une application, le besoin se fait assez souvent sentir d'effectuer des opérations AJAX, par exemple sur les différentes lignes d'un tableau ou les éléments d'une liste. Prenons comme exemple la liste d'articles où les utilisateurs connectés peuvent noter chaque article par un "j'aime" ou un "je n'aime pas". Le code du presenter et le template correspondant, sans AJAX, ressembleraient à ceci (nous montrons les parties les plus pertinentes ; le code suppose l'existence d'un service qui gère les notes et récupère les articles, son implémentation exacte n'ayant pas d'importance pour ce guide) : - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - $this->redirect('this'); -} - -public function handleUnlike(int $articleId): void -{ - $this->ratingService->removeLike($articleId, $this->user->id); - $this->redirect('this'); -} -``` - -Template : - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {if !$article->liked} - <a n:href="like!, $article->id" class=ajax>J'aime</a> - {else} - <a n:href="unlike!, $article->id" class=ajax>Je n'aime plus</a> - {/if} -</article> -``` - - -Ajaxisation -=========== - -Ajoutons maintenant l'AJAX à cette application toute simple. Changer la note d'un article n'est pas assez critique pour justifier un rechargement complet de la page, cela devrait donc idéalement se faire en AJAX, en arrière-plan. Nous utiliserons le [script de gestion des add-ons |application:ajax#Naja] avec la convention habituelle selon laquelle les liens AJAX portent la classe CSS `ajax`. - -Mais comment le mettre en œuvre concrètement ? Nette propose deux approches : les snippets dynamiques et les composants. Toutes deux ont leurs avantages et leurs inconvénients, nous allons donc montrer chacune d'elles. - - -La voie des snippets dynamiques -=============================== - -Dans la terminologie de Latte, un snippet dynamique désigne un usage particulier de la balise `{snippet}` où une variable est utilisée dans le nom du snippet. Un tel snippet ne peut pas être placé n'importe où dans le template ; il doit être enveloppé par un snippet statique (ordinaire) ou se trouver dans un `{snippetArea}`. Nous pourrions modifier notre template ainsi : - - -```latte -{snippet articlesContainer} - <article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {snippet article-{$article->id}} - {if !$article->liked} - <a n:href="like!, $article->id" class=ajax>J'aime</a> - {else} - <a n:href="unlike!, $article->id" class=ajax>Je n'aime plus</a> - {/if} - {/snippet} - </article> -{/snippet} -``` - -Chaque article définit désormais un snippet dont le nom contient l'ID de l'article. Tous ces snippets dynamiques sont ensuite enveloppés ensemble par un snippet statique nommé `articlesContainer`. Si nous omettions ce snippet extérieur, Latte lèverait une exception. - -Il ne reste plus qu'à ajouter la logique de redessin au presenter : il suffit de redessiner l'enveloppe statique. - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - if ($this->isAjax()) { - $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- inutile - } else { - $this->redirect('this'); - } -} -``` - -Modifiez de la même façon la méthode `handleUnlike()` correspondante, et l'AJAX fonctionne ! - -Cette solution a cependant un inconvénient. Si nous examinons la requête AJAX de plus près, nous découvrirons que, même si l'application paraît efficace vue de l'extérieur (elle ne renvoie qu'un seul snippet pour l'article concerné), elle rend en réalité *tous* les snippets côté serveur. Elle place le snippet requis dans le payload et jette les autres (ce qui veut dire qu'elle les a aussi récupérés et rendus inutilement). - -Pour optimiser cela, nous devons intervenir là où la collection `$articles` est passée au template (disons dans la méthode `renderDefault()`). Nous tirerons parti du fait que le traitement du signal a lieu avant les méthodes `render<Quelquechose>` : - -```php -public function handleLike(int $articleId): void -{ - // ... - if ($this->isAjax()) { - // ... - $this->template->articles = [ - $this->db->table('articles')->get($articleId), - ]; - } else { - // ... -} - -public function renderDefault(): void -{ - if (!isset($this->template->articles)) { - $this->template->articles = $this->db->table('articles'); - } -} -``` - -Désormais, pendant le traitement du signal, ce n'est plus toute la collection d'articles qui est passée au template, mais un tableau contenant le seul article concerné, celui que nous voulons rendre et envoyer dans le payload au navigateur. Par conséquent, la boucle `{foreach}` ne s'exécute qu'une fois et aucun snippet inutile n'est rendu. - - -La voie des composants -====================== - -Une approche complètement différente évite totalement les snippets dynamiques. L'astuce consiste à encapsuler toute la logique dans un composant distinct. Au lieu que le presenter gère la note, c'est un `LikeControl` dédié qui s'en chargera. La classe ressemblera à ceci (elle contiendrait aussi les méthodes `render`, `handleUnlike`, etc.) : - -```php -class LikeControl extends Nette\Application\UI\Control -{ - public function __construct( - private Article $article, - ) { - } - - public function handleLike(): void - { - $this->ratingService->saveLike($this->article->id, $this->presenter->user->id); - if ($this->presenter->isAjax()) { - $this->redrawControl(); - } else { - $this->presenter->redirect('this'); - } - } -} -``` - -Template du composant : - -```latte -{snippet} - {if !$article->liked} - <a n:href="like!" class=ajax>J'aime</a> - {else} - <a n:href="unlike!" class=ajax>Je n'aime plus</a> - {/if} -{/snippet} -``` - -Naturellement, le template de la vue changera et nous devrons ajouter une factory au presenter. Comme nous créerons une instance de ce composant pour chaque article récupéré de la base de données, nous utiliserons la classe [Multiplier |application:Multiplier] pour gérer leur création. - -```php -protected function createComponentLikeControl() -{ - $articles = $this->db->table('articles'); - return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { - return new LikeControl($articles[$articleId]); - }); -} -``` - -Le template de la vue se réduit au strict minimum (et ne contient plus aucun snippet !) : - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {control "likeControl-$article->id"} -</article> -``` - -Nous avons presque terminé : l'application fonctionnera désormais en AJAX. Là aussi, une optimisation s'impose car, du fait de l'utilisation de Nette Database, le traitement du signal charge inutilement tous les articles depuis la base au lieu du seul article concerné. L'avantage, en revanche, est qu'aucun rendu inutile n'a lieu, puisque seule l'instance de composant concernée est rendue. - -{{priority: -1}} diff --git a/best-practices/fr/form-reuse.texy b/best-practices/fr/form-reuse.texy deleted file mode 100644 index 439a6cc1ef..0000000000 --- a/best-practices/fr/form-reuse.texy +++ /dev/null @@ -1,348 +0,0 @@ -Réutiliser les formulaires à plusieurs endroits -*********************************************** - -.[perex] -Nette propose plusieurs façons de réutiliser le même formulaire à plusieurs endroits sans dupliquer de code. Cet article passera en revue différentes solutions, y compris celles que vous devriez éviter. - - -Factory de formulaire -===================== - -Une approche fondamentale pour réutiliser un composant à plusieurs endroits consiste à créer une méthode ou une classe qui génère ce composant. Cette méthode est ensuite appelée depuis différents endroits de l'application. Une telle méthode ou classe s'appelle une *factory*. Ne la confondez pas avec le patron de conception *factory method*, qui décrit une façon particulière d'utiliser les factories et n'a pas de rapport direct avec ce sujet. - -Créons par exemple une factory qui construit un formulaire d'édition : - -```php -use Nette\Application\UI\Form; - -class FormFactory -{ - public function createEditForm(): Form - { - $form = new Form; - $form->addText('title', 'Titre :'); - // d'autres champs du formulaire sont ajoutés ici - $form->addSubmit('send', 'Enregistrer'); - return $form; - } -} -``` - -Vous pouvez maintenant utiliser cette factory dans différentes parties de votre application, comme les presenters ou les composants. Pour cela, vous la [demandez comme dépendance |dependency-injection:passing-dependencies]. Enregistrez d'abord la classe dans le fichier de configuration : - -```neon -services: - - FormFactory -``` - -Puis utilisez-la dans un presenter : - - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->createEditForm(); - $form->onSuccess[] = function () { - // traitement des données soumises - }; - return $form; - } -} -``` - -Vous pouvez étoffer la factory de formulaire avec d'autres méthodes créant les types de formulaires dont votre application a besoin. Et naturellement, nous pouvons ajouter une méthode qui crée un formulaire de base sans éléments, que les autres méthodes pourront ensuite utiliser : - -```php -class FormFactory -{ - public function createForm(): Form - { - $form = new Form; - return $form; - } - - public function createEditForm(): Form - { - $form = $this->createForm(); - $form->addText('title', 'Titre :'); - // d'autres champs du formulaire sont ajoutés ici - $form->addSubmit('send', 'Enregistrer'); - return $form; - } -} -``` - -La méthode `createForm()` ne fait encore rien de bien utile, mais cela va changer très vite. - - -Dépendances de la factory -========================= - -Avec le temps, il peut devenir nécessaire que les formulaires soient multilingues. Cela signifie définir un [traducteur |forms:rendering#Traduction] pour tous les formulaires. Pour y parvenir, modifiez la classe `FormFactory` pour qu'elle reçoive l'objet `Translator` comme dépendance dans son constructeur et le passe au formulaire créé : - -```php -use Nette\Localization\Translator; - -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function createForm(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } - - // ... -} -``` - -Comme la méthode `createForm()` est aussi appelée par les autres méthodes qui créent des formulaires précis, définir le traducteur ici suffit. Et c'est terminé. Il n'y a besoin de modifier le code d'aucun presenter ni composant, ce qui est excellent. - - -Plusieurs classes factory -========================= - -Vous pouvez aussi créer des classes factory distinctes pour chaque formulaire que vous comptez utiliser dans votre application. Cette approche peut améliorer la lisibilité du code et simplifier la gestion des formulaires. Laissez la `FormFactory` d'origine ne créer qu'un formulaire de base avec la configuration fondamentale (comme la prise en charge de la traduction) et créez une nouvelle factory, `EditFormFactory`, spécialement pour le formulaire d'édition. - -```php -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function create(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } -} - - -// ✅ utilisation de la composition -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - // d'autres champs du formulaire sont ajoutés ici - $form->addSubmit('send', 'Enregistrer'); - return $form; - } -} -``` - -Il est essentiel que la relation entre les classes `FormFactory` et `EditFormFactory` soit réalisée par [composition |nette:introduction-to-object-oriented-programming#Composition], et non par [héritage d'objets |nette:introduction-to-object-oriented-programming#Héritage] : - -```php -// ⛔ NON ! L'HÉRITAGE N'A PAS SA PLACE ICI -class EditFormFactory extends FormFactory -{ - public function create(): Form - { - $form = parent::create(); - $form->addText('title', 'Titre :'); - // d'autres champs du formulaire sont ajoutés ici - $form->addSubmit('send', 'Enregistrer'); - return $form; - } -} -``` - -Utiliser ici l'héritage serait tout à fait contre-productif. Vous rencontreriez des problèmes très vite. Par exemple, si vous vouliez ajouter des paramètres à la méthode `create()`, PHP signalerait une erreur, car sa signature différerait de celle du parent. Ou encore lors du passage de dépendances à la classe `EditFormFactory` par le constructeur. Cela mènerait à ce qu'on appelle le [constructor hell |dependency-injection:passing-dependencies#Constructor hell]. - -D'une manière générale, il vaut mieux préférer la [composition à l'héritage |dependency-injection:faq#Pourquoi la composition est-elle préférée à l'héritage ?]. - - -Traitement du formulaire -======================== - -Le gestionnaire du formulaire, invoqué après une soumission réussie, peut lui aussi faire partie de la classe factory. Il fonctionne en passant les données soumises à la couche modèle pour traitement. Les éventuelles erreurs de traitement sont renvoyées [au |forms:validation#Erreurs lors du traitement] formulaire. Dans l'exemple suivant, le modèle est représenté par la classe `Facade` : - -```php -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - private Facade $facade, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - $form->addText('title', 'Titre :'); - // d'autres champs du formulaire sont ajoutés ici - $form->addSubmit('send', 'Enregistrer'); - $form->onSuccess[] = $this->processForm(...); - return $form; - } - - private function processForm(Form $form, array $data): void - { - try { - // traitement des données soumises - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - } - } -} -``` - -Laissez cependant le presenter s'occuper lui-même de la redirection. Il ajoute à l'événement `onSuccess` un gestionnaire supplémentaire qui effectue la redirection. Le formulaire peut ainsi être utilisé dans différents presenters, chacun redirigeant en cas de succès vers un endroit différent. - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditFormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->create(); - $form->onSuccess[] = function () { - $this->flashMessage('L\'enregistrement a été sauvegardé'); - $this->redirect('Homepage:'); - }; - return $form; - } -} -``` - -Cette solution tire parti d'une caractéristique des formulaires : si `addError()` est appelée sur le formulaire ou sur l'un de ses éléments, les gestionnaires `onSuccess` suivants ne sont pas invoqués. - - -Hériter de la classe Form -========================= - -Un formulaire assemblé ne devrait pas être un descendant de la classe `Form`. Autrement dit, évitez cette approche : - -```php -// ⛔ NON ! L'HÉRITAGE N'A PAS SA PLACE ICI -class EditForm extends Form -{ - public function __construct(Translator $translator) - { - parent::__construct(); - $this->addText('title', 'Titre :'); - // d'autres champs du formulaire sont ajoutés ici - $this->addSubmit('send', 'Enregistrer'); - $this->setTranslator($translator); - } -} -``` - -Au lieu d'assembler le formulaire dans le constructeur, utilisez une factory. - -Il est important de comprendre que la classe `Form` est avant tout un outil de construction de formulaires, autrement dit un *form builder*. Le formulaire assemblé peut être considéré comme son produit. Or un produit n'est pas un type particulier de constructeur ; il n'y a pas de relation *est un* entre eux, qui est le fondement de l'héritage. - - -Composant formulaire -==================== - -Une approche complètement différente consiste à créer un [composant |application:components] qui encapsule le formulaire. Cela ouvre de nouvelles possibilités, comme rendre le formulaire d'une manière particulière, puisque le composant possède son propre template. On peut aussi utiliser les signaux pour la communication AJAX et charger dynamiquement des informations dans le formulaire, par exemple pour des suggestions, etc. - - -```php -use Nette\Application\UI\Form; - -class EditControl extends Nette\Application\UI\Control -{ - public array $onSave = []; - - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentForm(): Form - { - $form = new Form; - $form->addText('title', 'Titre :'); - // d'autres champs du formulaire sont ajoutés ici - $form->addSubmit('send', 'Enregistrer'); - $form->onSuccess[] = $this->processForm(...); - - return $form; - } - - private function processForm(Form $form, array $data): void - { - try { - // traitement des données soumises - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - return; - } - - // déclenchement de l'événement - $this->onSave($this, $data); - } -} -``` - -Créons ensuite une factory qui produira ce composant. Il suffit d'[en définir l'interface |application:components#Composants avec dépendances] : - -```php -interface EditControlFactory -{ - function create(): EditControl; -} -``` - -Et de l'ajouter au fichier de configuration : - -```neon -services: - - EditControlFactory -``` - -Nous pouvons maintenant demander la factory et l'utiliser dans le presenter : - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditControlFactory $controlFactory, - ) { - } - - protected function createComponentEditForm(): EditControl - { - $control = $this->controlFactory->create(); - - $control->onSave[] = function (EditControl $control, $data) { - $this->redirect('this'); - // ou redirection vers le résultat de l'édition, par exemple : - // $this->redirect('detail', ['id' => $data->id]); - }; - - return $control; - } -} -``` diff --git a/best-practices/fr/inject-method-attribute.texy b/best-practices/fr/inject-method-attribute.texy deleted file mode 100644 index d8629951a4..0000000000 --- a/best-practices/fr/inject-method-attribute.texy +++ /dev/null @@ -1,61 +0,0 @@ -Méthodes et attributs inject -**************************** - -.[perex] -Cet article se concentre sur les différentes façons de passer des dépendances aux presenters dans le framework Nette. Nous comparerons la méthode préférée, l'injection par le constructeur, avec des alternatives comme les méthodes et les attributs `inject`. - -Pour les presenters, comme pour les autres classes, passer les dépendances par le [constructeur |dependency-injection:passing-dependencies#Injection par le constructeur] est l'approche préférée. Cependant, si vous créez un ancêtre commun dont héritent les autres presenters (par exemple `BasePresenter`) et que cet ancêtre a lui aussi besoin de dépendances, un problème appelé [constructor hell |dependency-injection:passing-dependencies#Constructor hell] peut apparaître. On peut le contourner par des méthodes alternatives, à savoir les méthodes et les attributs inject (autrefois des annotations). - - -Méthodes `inject*()` -==================== - -C'est une forme de passage des dépendances par [setters |dependency-injection:passing-dependencies#Injection par setter]. Les noms de ces setters doivent commencer par le préfixe `inject`. Nette DI appelle automatiquement les méthodes ainsi nommées juste après la création de l'instance du presenter, en leur passant toutes les dépendances requises. Elles doivent donc être déclarées publiques. - -Les méthodes `inject*()` peuvent être vues comme des extensions du constructeur, réparties en plusieurs méthodes. Cela permet à `BasePresenter` de recevoir ses dépendances par une méthode distincte, en laissant le constructeur libre pour ses descendants : - -```php -abstract class BasePresenter extends Nette\Application\UI\Presenter -{ - private Foo $foo; - - public function injectBase(Foo $foo): void - { - $this->foo = $foo; - } -} - -class MyPresenter extends BasePresenter -{ - private Bar $bar; - - public function __construct(Bar $bar) - { - $this->bar = $bar; - } -} -``` - -Un presenter peut avoir autant de méthodes `inject*()` que nécessaire, et chacune peut accepter autant de paramètres que nécessaire. Cette approche convient aussi très bien aux cas où un presenter est [composé de traits |presenter-traits] et où chaque trait a besoin de ses propres dépendances. - - -Attributs `Inject` -================== - -C'est une forme d'[injection dans les propriétés |dependency-injection:passing-dependencies#Injection dans une propriété]. Il suffit de marquer les propriétés à injecter et Nette DI passera automatiquement les dépendances juste après la création de l'instance du presenter. Pour permettre l'injection, ces propriétés doivent être déclarées publiques. - -Les propriétés se marquent par un attribut : (auparavant, on utilisait l'annotation `/** @inject */`) - -```php -use Nette\DI\Attributes\Inject; // cette ligne est importante - -class MyPresenter extends Nette\Application\UI\Presenter -{ - #[Inject] - public Cache $cache; -} -``` - -L'avantage de cette façon de passer les dépendances était sa forme d'écriture très concise. Cependant, avec l'arrivée de la [promotion des propriétés du constructeur |https://blog.nette.org/fr/php-8-0-complete-overview-of-news#toc-constructor-property-promotion], utiliser le constructeur paraît souvent plus simple. - -En revanche, cette méthode souffre des mêmes défauts que l'injection dans les propriétés en général : nous perdons le contrôle sur les changements de la variable, et celle-ci devient partie de l'interface publique de la classe, ce qui est généralement indésirable. diff --git a/best-practices/fr/lets-create-contact-form.texy b/best-practices/fr/lets-create-contact-form.texy deleted file mode 100644 index e888d3c771..0000000000 --- a/best-practices/fr/lets-create-contact-form.texy +++ /dev/null @@ -1,218 +0,0 @@ -Créons un formulaire de contact -******************************* - -.[perex] -Voyons comment créer dans Nette un formulaire de contact, envoi des données saisies par e-mail compris. Allons-y ! - -Nous devons d'abord créer un nouveau projet. La page [Premiers pas |nette:installation] explique comment faire. Nous pouvons ensuite commencer à créer le formulaire. - -L'approche la plus simple est de créer le [formulaire directement dans le presenter |forms:in-presenter]. Nous pouvons utiliser le `HomePresenter` déjà existant. Nous y ajouterons un composant nommé `contactForm` qui représentera notre formulaire. Pour cela, nous ajoutons au code du presenter une méthode fabrique `createComponentContactForm()`, qui créera le composant : - -```php -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - protected function createComponentContactForm(): Form - { - $form = new Form; - $form->addText('name', 'Nom :') - ->setRequired('Veuillez saisir votre nom'); - $form->addEmail('email', 'E-mail :') - ->setRequired('Veuillez saisir votre e-mail'); - $form->addTextArea('message', 'Message :') - ->setRequired('Veuillez saisir un message'); - $form->addSubmit('send', 'Envoyer'); - $form->onSuccess[] = $this->contactFormSucceeded(...); - return $form; - } - - private function contactFormSucceeded(Form $form, $data): void - { - // envoi d'un e-mail - } -} -``` - -Comme vous le voyez, nous avons créé deux méthodes. La première, `createComponentContactForm()`, crée une nouvelle instance du formulaire. Elle contient les champs pour le nom, l'e-mail et le message, ajoutés respectivement par les méthodes `addText()`, `addEmail()` et `addTextArea()`. Nous avons aussi ajouté un bouton d'envoi. Mais que se passe-t-il si l'utilisateur laisse un champ vide ? Dans ce cas, nous devrions lui signaler que le champ est obligatoire. Nous y sommes parvenus à l'aide de la méthode `setRequired()`. Enfin, nous avons accroché un gestionnaire d'[événement |nette:glossary#Événements] à `onSuccess`, qui est déclenché après l'envoi réussi du formulaire. Dans notre cas, il appelle la méthode `contactFormSucceeded`, qui se chargera du traitement des données soumises. Nous l'implémenterons dans un instant. - -Rendons le composant `contactForm` dans le template `Home/default.latte` : - -```latte -{block content} -<h1>Formulaire de contact</h1> -{control contactForm} -``` - -Pour l'envoi de l'e-mail lui-même, nous créerons une nouvelle classe nommée `ContactFacade` et la placerons dans le fichier `app/Model/ContactFacade.php` : - -```php -namespace App\Model; - -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $mail = new Message; - $mail->addTo('admin@example.com') // votre e-mail - ->setFrom($email, $name) - ->setSubject('Message du formulaire de contact') - ->setBody($message); - - $this->mailer->send($mail); - } -} -``` - -La méthode `sendMessage()` crée et envoie l'e-mail. Elle utilise pour cela un service mailer, qu'elle reçoit comme dépendance par le constructeur. Pour en savoir plus, voir l'[envoi d'e-mails |mail:]. - -Revenons maintenant au presenter et complétons la méthode `contactFormSucceeded()`. Elle appellera la méthode `sendMessage()` de la classe `ContactFacade` en lui passant les données soumises par le formulaire. Et comment obtenir l'objet `ContactFacade` ? Nous le demanderons par le constructeur, grâce à l'injection de dépendances : - -```php -use App\Model\ContactFacade; -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - public function __construct( - private ContactFacade $facade, - ) { - } - - protected function createComponentContactForm(): Form - { - // ... - } - - public function contactFormSucceeded(stdClass $data): void - { - $this->facade->sendMessage($data->email, $data->name, $data->message); - $this->flashMessage('Le message a été envoyé'); - $this->redirect('this'); - } -} -``` - -Après l'envoi de l'e-mail, nous affichons à l'utilisateur un [message flash |application:components#Messages Flash] qui confirme l'envoi. Puis nous redirigeons pour éviter que le formulaire ne soit renvoyé par un rafraîchissement du navigateur. - - -Si tout est bien en place, vous devriez donc maintenant pouvoir envoyer un e-mail depuis votre formulaire de contact. Félicitations ! - - -Template HTML de l'e-mail -------------------------- - -Pour l'instant, c'est un e-mail en texte brut, contenant seulement le message envoyé par le formulaire, qui est expédié. Nous pouvons cependant utiliser du HTML dans l'e-mail pour rendre son apparence plus attrayante. Nous en créerons un template en Latte et l'enregistrerons sous `app/Model/contactEmail.latte` : - -```latte -<html> - <title>Message du formulaire de contact - - -

    Nom : {$name}

    -

    E-mail : {$email}

    -

    Message : {$message}

    - - -``` - -Il reste à modifier `ContactFacade` pour qu'elle utilise ce template. Dans le constructeur, nous demanderons la classe `LatteFactory`, qui sait créer un objet `Latte\Engine`, le [moteur de rendu des templates Latte |latte:develop#Comment rendre un template]. À l'aide de la méthode `renderToString()`, nous rendons le template dans une chaîne. Le premier paramètre est le chemin du fichier de template, le second un tableau de variables à lui passer. - -```php -namespace App\Model; - -use Nette\Bridges\ApplicationLatte\LatteFactory; -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $latte = $this->latteFactory->create(); - $body = $latte->renderToString(__DIR__ . '/contactEmail.latte', [ - 'email' => $email, - 'name' => $name, - 'message' => $message, - ]); - - $mail = new Message; - $mail->addTo('admin@example.com') // votre e-mail - ->setFrom($email, $name) - ->setHtmlBody($body); - - $this->mailer->send($mail); - } -} -``` - -Nous passons ensuite le contenu HTML généré de l'e-mail à la méthode `setHtmlBody()` au lieu du `setBody()` d'origine. Nous n'avons pas non plus besoin d'indiquer le sujet de l'e-mail avec `setSubject()`, car la bibliothèque l'extrait automatiquement de l'élément `` du template. - - -Configuration -------------- - -Dans le code de la classe `ContactFacade`, notre e-mail d'administrateur `admin@example.com` est toujours écrit en dur. Il vaudrait mieux le déplacer dans le fichier de configuration. Comment faire ? - -Modifions d'abord la classe `ContactFacade` en remplaçant la chaîne d'e-mail écrite en dur par une variable passée au constructeur : - -```php -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - private string $adminEmail, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - // ... - $mail = new Message; - $mail->addTo($this->adminEmail) - ->setFrom($email, $name) - ->setHtmlBody($body); - // ... - } -} -``` - -La deuxième étape consiste à fournir la valeur de cette variable dans la configuration. Dans le fichier `app/config/services.neon`, ajoutez : - -```neon -services: - - App\Model\ContactFacade(adminEmail: admin@example.com) -``` - -Et voilà. Si la section `services` contient beaucoup d'entrées et que vous avez l'impression que l'adresse e-mail s'y perd, nous pouvons en faire un paramètre. Modifiez l'entrée ainsi : - -```neon -services: - - App\Model\ContactFacade(adminEmail: %adminEmail%) -``` - -Et définissez ce paramètre dans le fichier `app/config/common.neon` : - -```neon -parameters: - adminEmail: admin@example.com -``` - -Et c'est terminé ! diff --git a/best-practices/fr/microsites.texy b/best-practices/fr/microsites.texy deleted file mode 100644 index 1f70d1363f..0000000000 --- a/best-practices/fr/microsites.texy +++ /dev/null @@ -1,63 +0,0 @@ -Comment écrire 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 vous dites peut-être qu'un framework robuste n'est pas nécessaire pour un si petit projet. Mais si utiliser Nette Framework pouvait justement simplifier et accélérer ce travail ? - -Même en créant des sites simples, vous ne voulez pas renoncer au confort. Vous ne voulez pas réinventer ce qui est déjà résolu. Soyez paresseux et laissez-vous choyer. Nette Framework se prête aussi excellemment à un usage de micro-framework. - -À quoi peut ressembler un tel microsite ? Par exemple, tout le code du site peut tenir dans un unique fichier `index.php` situé dans le répertoire public : - -```php -<?php - -require __DIR__ . '/../vendor/autoload.php'; - -$configurator = new Nette\Bootstrap\Configurator; -$configurator->enableTracy(__DIR__ . '/../log'); -$configurator->setTempDirectory(__DIR__ . '/../temp'); - -// crée le conteneur DI d'après la configuration dans config.neon -$configurator->addConfig(__DIR__ . '/../app/config.neon'); -$container = $configurator->createContainer(); - -// met 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étecte la langue du navigateur et redirige vers l'URL /en ou /de, etc. - $supportedLangs = ['en', 'de', 'cs']; - $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); - return $presenter->redirectUrl("/$lang"); -}); - -// route pour l'URL https://example.com/cs ou https://example.com/en -$router->addRoute('<lang cs|en|de>', function ($presenter, string $lang) { - // affiche le template correspondant, par exemple ../templates/en.latte - $template = $presenter->createTemplate() - ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); - return $template; -}); - -// lance l'application ! -$container->getByType(Nette\Application\Application::class)->run(); -``` - -Tout le reste sera constitué de templates enregistrés dans le répertoire parent `/templates`. - -Le code PHP de `index.php` [met d'abord en place l'environnement |bootstrap:], puis définit les [routes |application:routing#Routage dynamique avec callbacks] et lance enfin l'application. L'avantage est que le deuxième paramètre de la fonction `addRoute()` peut être un callable, qui s'exécute lorsque la page correspondante est ouverte. - - -Pourquoi utiliser Nette pour des microsites ? ---------------------------------------------- - -- Les programmeurs qui ont essayé [Tracy|tracy:] ont aujourd'hui du mal à imaginer programmer sans elle. -- Vous profiterez surtout du système de templates [Latte|latte:], car même avec deux pages seulement, vous voudrez séparer le [layout et le contenu|latte:template-inheritance]. -- Et vous voudrez certainement compter sur l'[échappement automatique |latte:safety-first] pour éviter les failles XSS. -- Nette veille aussi à ce qu'en cas d'erreur, les messages d'erreur bruts de PHP ne s'affichent jamais ; c'est une page conviviale qui est présentée à la place. -- Si vous voulez recueillir les retours des utilisateurs, par exemple via un formulaire de contact, vous pouvez facilement ajouter la prise en charge des [formulaires|forms:] et de la [base de données|database:]. -- Vous pouvez aussi facilement faire [envoyer par e-mail|mail:] les formulaires remplis. -- Parfois, le [cache|caching:] peut être utile, par exemple lors du téléchargement et de l'affichage de flux. - -Dans le monde d'aujourd'hui, où la rapidité et l'efficacité sont cruciales, il est essentiel de disposer d'outils qui permettent d'obtenir des résultats sans délais inutiles. Nette Framework offre exactement cela : un développement rapide, la sécurité et tout un éventail d'outils comme Tracy et Latte qui fluidifient le travail. Il suffit d'installer quelques paquets Nette et construire un tel microsite devient incroyablement facile. Et vous pouvez être sûr qu'aucune faille de sécurité ne se cache dedans. diff --git a/best-practices/fr/pagination.texy b/best-practices/fr/pagination.texy deleted file mode 100644 index 7d42712ba4..0000000000 --- a/best-practices/fr/pagination.texy +++ /dev/null @@ -1,273 +0,0 @@ -Paginer les résultats de base de données -**************************************** - -.[perex] -En développant des applications web, vous rencontrez souvent l'exigence de limiter le nombre d'éléments listés par page, une technique appelée pagination. - -Partons d'un état où nous listons toutes les données sans pagination. Pour sélectionner les données dans la base, nous avons une classe `ArticleRepository`. Outre le constructeur, elle contient une méthode `findPublishedArticles` qui renvoie tous les articles publiés, triés par date de publication décroissante. - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC', - new \DateTime, - ); - } -} -``` - -Dans le presenter, nous injectons ensuite cette classe de modèle. Dans la méthode render, nous récupérons les articles publiés et les passons au template : - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(): void - { - $this->template->articles = $this->articleRepository->findPublishedArticles(); - } -} -``` - -Le template `default.latte` se chargera ensuite de lister les articles : - -```latte -{block content} -<h1>Articles</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> -``` - - -Nous pouvons ainsi lister tous les articles, mais cela devient problématique à mesure que leur nombre augmente. À ce moment-là, mettre en place un mécanisme de pagination devient utile. - -Ce mécanisme divise tous les articles en plusieurs pages et nous n'affichons que les articles appartenant à la page actuellement sélectionnée. Le nombre total de pages et la répartition des articles sont calculés par l'utilitaire [Paginator |utils:Paginator] à partir du nombre total d'articles et du nombre souhaité d'articles par page. - -Dans un premier temps, nous modifierons la méthode de récupération des articles dans la classe repository pour qu'elle puisse ne renvoyer que les articles d'une seule page. Nous ajouterons aussi une méthode donnant le nombre total d'articles dans la base, nécessaire à la configuration du Paginator : - -```php -namespace App\Model; - -use Nette; - - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(int $limit, int $offset): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC - LIMIT ? - OFFSET ?', - new \DateTime, $limit, $offset, - ); - } - - /** - * Renvoie le nombre total d'articles publiés - */ - public function getPublishedArticlesCount(): int - { - return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime); - } -} -``` - -Modifions ensuite le presenter. Nous passerons le numéro de la page courante à la méthode `renderDefault`. Si ce numéro ne fait pas partie de l'URL, nous fixerons la valeur par défaut à 1 (la première page). - -Nous étendrons aussi la méthode render pour créer et configurer une instance de Paginator et sélectionner les articles à afficher dans le template. Le `HomePresenter` modifié ressemblera à ceci : - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Récupère le nombre total d'articles publiés - $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - - // Crée et configure l'instance de Paginator - $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // nombre total d'éléments - $paginator->setItemsPerPage(10); // éléments par page - $paginator->setPage($page); // numéro de la page courante - - // Récupère dans la base un ensemble limité d'articles selon le calcul du Paginator - $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - - // les passe au template - $this->template->articles = $articles; - // ainsi que le Paginator lui-même, pour afficher les commandes de pagination - $this->template->paginator = $paginator; - } -} -``` - -Le template ne parcourt maintenant que les articles de la page courante. Il ne reste qu'à ajouter les liens de pagination : - -```latte -{block content} -<h1>Articles</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if !$paginator->isFirst()} - <a n:href="default, 1">Première</a> -  |  - <a n:href="default, $paginator->getPage() - 1">Précédente</a> -  |  - {/if} - - Page {$paginator->getPage()} sur {$paginator->getPageCount()} - - {if !$paginator->isLast()} -  |  - <a n:href="default, $paginator->getPage() + 1">Suivante</a> -  |  - <a n:href="default, $paginator->getPageCount()">Dernière</a> - {/if} -</div> -``` - - -Cela achève la mise en place de la pagination à l'aide du Paginator. Si vous utilisez [Nette Database Explorer |database:explorer] plutôt que [Nette Database Core |database:sql-way] comme couche de base de données, vous pouvez mettre en place la pagination sans utiliser directement l'utilitaire Paginator. La classe `Nette\Database\Table\Selection` dispose d'une méthode [page() |api:Nette\Database\Table\Selection::page()] qui encapsule la logique de pagination. - -Avec cette approche, le repository ressemblera à ceci : - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Explorer $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\Table\Selection - { - return $this->database->table('articles') - ->where('created_at < ', new \DateTime) - ->order('created_at DESC'); - } -} -``` - -Dans le presenter, nous n'avons pas besoin de créer d'instance de Paginator. Nous utiliserons à la place la méthode `page()` proposée par l'objet `Selection` renvoyé par le repository : - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Récupère les articles publiés - $articles = $this->articleRepository->findPublishedArticles(); - - // et ne passe au template que la portion limitée par le calcul de la méthode page - $lastPage = 0; - $this->template->articles = $articles->page($page, 10, $lastPage); - - // ainsi que les données nécessaires à l'affichage des options de pagination - $this->template->page = $page; - $this->template->lastPage = $lastPage; - } -} -``` - -Comme nous ne passons plus l'objet Paginator au template, nous devons adapter la partie qui affiche les liens de pagination : - -```latte -{block content} -<h1>Articles</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if $page > 1} - <a n:href="default, 1">Première</a> -  |  - <a n:href="default, $page - 1">Précédente</a> -  |  - {/if} - - Page {$page} sur {$lastPage} - - {if $page < $lastPage} -  |  - <a n:href="default, $page + 1">Suivante</a> -  |  - <a n:href="default, $lastPage">Dernière</a> - {/if} -</div> -``` - -Nous avons ainsi mis en place le mécanisme de pagination sans utiliser explicitement l'utilitaire Paginator. - -{{priority: -1}} diff --git a/best-practices/fr/passing-settings-to-presenters.texy b/best-practices/fr/passing-settings-to-presenters.texy deleted file mode 100644 index 0d1fae937d..0000000000 --- a/best-practices/fr/passing-settings-to-presenters.texy +++ /dev/null @@ -1,49 +0,0 @@ -Passer des réglages aux presenters -********************************** - -.[perex] -Vous avez besoin de passer aux presenters des arguments qui ne sont pas des objets (par exemple l'information indiquant si l'application tourne en mode débogage, des chemins de répertoires, etc.) et qui ne peuvent donc pas être passés automatiquement par autowiring ? La solution est de les encapsuler dans un objet `Settings` dédié. - -Le service `Settings` offre un moyen très simple mais efficace de fournir aux presenters des informations sur l'application en cours d'exécution. Sa structure exacte dépend entièrement de vos besoins. Exemple : - -```php -namespace App; - -class Settings -{ - public function __construct( - // depuis PHP 8.1, readonly peut être utilisé - public bool $debugMode, - public string $appDir, - // et ainsi de suite - ) {} -} -``` - -Exemple d'enregistrement dans la configuration : - -```neon -services: - - App\Settings( - %debugMode%, - %appDir%, - ) -``` - -Lorsqu'un presenter a besoin des informations fournies par ce service, il se contente de le demander dans son constructeur : - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private App\Settings $settings, - ) {} - - public function renderDefault() - { - if ($this->settings->debugMode) { - // ... - } - } -} -``` diff --git a/best-practices/fr/post-links.texy b/best-practices/fr/post-links.texy deleted file mode 100644 index e77ed7b7d7..0000000000 --- a/best-practices/fr/post-links.texy +++ /dev/null @@ -1,56 +0,0 @@ -Comment bien utiliser les liens POST -************************************ - -.[perex] -Dans les applications web, en particulier dans les interfaces d'administration, une règle fondamentale devrait être que les actions modifiant l'état du serveur ne sont pas effectuées par la méthode HTTP GET. Comme son nom l'indique, GET ne devrait servir qu'à récupérer des données, pas à les modifier. Pour des actions comme la suppression d'enregistrements, la méthode POST est plus appropriée. La méthode DELETE serait idéale, mais elle ne peut pas être invoquée sans JavaScript, c'est pourquoi POST est historiquement utilisée pour ce genre d'actions. - -Comment mettre cela en pratique ? Utilisez cette astuce toute simple. Au début de votre template de layout, créez un formulaire auxiliaire portant l'ID `postForm`. Vous utiliserez ensuite ce formulaire pour des actions comme les boutons de suppression : - -```latte .{file:@layout.latte} -<form method="post" id="postForm"></form> -``` - -Grâce à ce formulaire, au lieu d'un lien `<a>` classique, vous pouvez utiliser un `<button>`. Ce bouton peut être stylé pour ressembler à un lien ordinaire. Le framework CSS Bootstrap propose par exemple les classes `btn btn-link`, qui rendent le bouton visuellement indiscernable des autres liens. À l'aide de l'attribut `form="postForm"`, reliez le bouton au formulaire auxiliaire préparé : - -```latte .{file:admin.latte} -<table> - <tr n:foreach="$posts as $post"> - <td>{$post->title}</td> - <td> - <button class="btn btn-link" form="postForm" formaction="{link delete, $post->id}">supprimer</button> - <!-- au lieu de <a n:href="delete, $post->id">supprimer</a> --> - </td> - </tr> -</table> -``` - -Un clic sur ce bouton invoque désormais l'action `delete`. Pour que les requêtes ne soient acceptées que par la méthode POST et qu'elles proviennent du même domaine (une défense efficace contre les attaques CSRF), utilisez l'attribut `#[Requires]` : - -```php .{file:AdminPresenter.php} -use Nette\Application\Attributes\Requires; - -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST', sameOrigin: true)] - public function actionDelete(int $id): void - { - $this->facade->deletePost($id); // code hypothétique de suppression d'un enregistrement - $this->redirect('default'); - } -} -``` - -Cet attribut est disponible depuis Nette Application 3.2. Vous en apprendrez davantage sur ses possibilités sur la page [Comment utiliser l'attribut #Requires |attribute-requires]. - -Si vous utilisiez le signal `handleDelete()` au lieu de l'action `actionDelete()`, indiquer `sameOrigin: true` est inutile, car les signaux ont cette protection activée par défaut : - -```php .{file:AdminPresenter.php} -#[Requires(methods: 'POST')] -public function handleDelete(int $id): void -{ - $this->facade->deletePost($id); - $this->redirect('this'); -} -``` - -Cette approche renforce non seulement la sécurité de votre application, mais encourage aussi le respect des standards et des bonnes pratiques du web. Utiliser la méthode POST pour les actions qui changent l'état donne une application plus robuste et plus sûre. diff --git a/best-practices/fr/presenter-traits.texy b/best-practices/fr/presenter-traits.texy deleted file mode 100644 index f56299f78b..0000000000 --- a/best-practices/fr/presenter-traits.texy +++ /dev/null @@ -1,47 +0,0 @@ -Composer des presenters à partir de traits -****************************************** - -.[perex] -Si vous avez besoin d'implémenter la même fonctionnalité dans plusieurs presenters (par exemple vérifier que l'utilisateur est connecté), placer le code dans un ancêtre commun est une approche courante. Une autre possibilité est de créer des [traits |nette:introduction-to-object-oriented-programming#Traits] à usage unique. - -L'avantage des traits est que chaque presenter peut n'incorporer que ceux dont il a réellement besoin, d'autant que l'héritage multiple n'existe pas en PHP. - -Ces traits peuvent tirer parti du fait que toutes les [méthodes inject |inject-method-attribute#Méthodes inject*()] sont appelées les unes après les autres lors de la création de l'instance du presenter. Il vous suffit de veiller à ce que le nom de chaque méthode inject soit unique parmi tous les traits utilisés et le presenter lui-même. - -Les traits peuvent accrocher du code d'initialisation aux événements [onStartup ou onRender |application:presenters#Événements]. - -Exemples : - -```php -trait RequireLoggedUser -{ - public function injectRequireLoggedUser(): void - { - $this->onStartup[] = function () { - if (!$this->getUser()->isLoggedIn()) { - $this->redirect('Sign:in', $this->storeRequest()); - } - }; - } -} - -trait StandardTemplateFilters -{ - public function injectStandardTemplateFilters(TemplateBuilder $builder): void - { - $this->onRender[] = function () use ($builder) { - $builder->setupTemplate($this->template); - }; - } -} -``` - -Le presenter se contente ensuite d'utiliser ces traits : - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - use StandardTemplateFilters; - use RequireLoggedUser; -} -``` diff --git a/best-practices/fr/pretty-urls.texy b/best-practices/fr/pretty-urls.texy deleted file mode 100644 index 244b566ba4..0000000000 --- a/best-practices/fr/pretty-urls.texy +++ /dev/null @@ -1,204 +0,0 @@ -URLs élégantes avec slugs -************************* - -.[perex] -Une URL comme `/article/123-comment-faire-du-pain` a plus d'allure que `/article/123` et aide aussi bien les utilisateurs que les moteurs de recherche à comprendre ce qui se trouve sur la page. Ce guide montre comment les générer entièrement dans le routeur - sans toucher au moindre template - et comment faire en sorte que chaque visiteur atterrisse sur l'URL canonique. - - -Pourquoi des slugs dans les URLs -================================ - -Comparez ces deux adresses : - -``` -/article/123 -/article/123-comment-faire-du-pain -``` - -La seconde dit à l'utilisateur (et à Google) ce qui l'attend après le clic. C'est bon pour le SEO, cela rend les liens lisibles dans un chat ou un e-mail, et cela donne du sens à la barre d'adresse. - -Le slug n'est cependant pas un véritable identifiant. C'est l'ID qui détermine la page. Le slug est une décoration que l'application génère à partir du titre. Si le titre change, le slug devrait changer aussi. Et si quelqu'un modifie l'URL à la main ou suit un vieux lien, l'application devrait malgré tout trouver la bonne page. - - -L'objectif -========== - -Nous voulons une route qui gère tous ces cas : - -``` -/article/123 → ouvre l'article 123, redirige vers l'URL canonique -/article/123-comment-faire-du-pain → ouvre directement l'article 123 -/article/123-nimporte-quoi-de-saisi → ouvre l'article 123, redirige vers l'URL canonique -/article/ → 404 (pas d'ID) -``` - -Et nous voulons que chaque `n:href` et chaque appel `link()` de l'application produise automatiquement `/article/123-comment-faire-du-pain` - **sans réécrire un seul template**. - - -Le masque de la route -===================== - -L'astuce consiste à marquer le slug comme **facultatif** dans le masque à l'aide de crochets : - -```php -$router->addRoute('article/<id [0-9]+>[-<slug>]', 'Article:detail'); -``` - -Le masque `[-<slug>]` dit : il peut y avoir un trait d'union et un slug après l'ID, mais ce n'est pas obligatoire. La route accepte aussi bien `/article/123` que `/article/123-nimportequoi`. - -Une remarque sur le paramètre `<slug>` : par défaut, il correspond à n'importe quels caractères **sauf la barre oblique** - exactement ce que nous voulons. Si vous écrivez `<slug .+>`, le paramètre correspondra aussi aux barres obliques, si bien que `/article/123-quelquechose/autre` serait analysé comme un unique slug contenant `/`. Restez-en au `<slug>` par défaut, sauf si vous avez vraiment besoin de cela. - -Pour l'instant, l'URL est analysée correctement, mais les liens générés ne contiendront pas le slug. L'étape suivante consiste à apprendre à la route comment remplir le slug. - - -Générer le slug sans toucher aux templates -========================================== - -C'est la variante décisive. Les appels `n:href="Article:detail, $id"` existants continuent de fonctionner sans changement dans toute l'application - le routeur va chercher le titre lui-même. - -Nous procédons à l'aide d'un **filtre général** placé sous la clé chaîne vide : il voit tous les paramètres d'un coup et peut ajouter le slug : - -```php -use Nette\Routing\Route; -use Nette\Utils\Strings; - -$router->addRoute('article/<id [0-9]+>[-<slug>]', [ - 'presenter' => 'Article', - 'action' => 'detail', - '' => [ - Route::FilterOut => function (array $params) use ($slugProvider): array { - if (isset($params['id']) && empty($params['slug'])) { - $params['slug'] = $slugProvider->getSlug((int) $params['id']); - } - return $params; - }, - ], -]); -``` - -`FilterOut` s'exécute chaque fois que le routeur **génère** une URL. Si le slug n'a pas été passé, le filtre va chercher le titre et l'ajoute. - -Vous pouvez déployer les slugs dans toute une application en une seule modification : une unique définition de route. Chaque lien de chaque template se met automatiquement à produire `/article/123-comment-faire-du-pain`. Pas de grep, pas de chasse aux templates, aucun cas oublié. - - -Mettre la recherche en cache -============================ - -Un lien engendre une requête en base de données, mais une page typique en contient beaucoup : listes, fil d'Ariane, "derniers consultés", articles liés. Le même ID d'article apparaît souvent dans plusieurs liens d'une même requête, et vous ne voulez pas interroger la base à chaque fois. - -Un minuscule cache valable pour la requête en cours règle le problème. Enveloppez l'appel à la base dans un petit service : - -```php -final class SlugProvider -{ - /** @var array<int, string> */ - private array $cache = []; - - public function __construct( - private Nette\Database\Explorer $db, - ) { - } - - public function getSlug(int $id): string - { - return $this->cache[$id] ??= Strings::webalize(Strings::truncate( - (string) $this->db->fetchField('SELECT title FROM article WHERE id = ?', $id), - 100, '' - )); - } -} -``` - -Cela suffit : une seule requête en base par ID unique et par requête HTTP. - - -Passer le titre depuis le template (raccourci facultatif) -========================================================= - -Lorsque le titre est déjà sous la main dans le template, vous pouvez éviter complètement la recherche en base. Passez le titre comme paramètre nommé : - -```latte -<a n:href="Article:detail, $article->id, slug => $article->title">{$article->title}</a> -``` - -…et ajoutez un `FilterOut` propre au paramètre, qui transforme le titre en une chaîne utilisable dans une URL : - -```php -$router->addRoute('article/<id [0-9]+>[-<slug>]', [ - 'presenter' => 'Article', - 'action' => 'detail', - 'slug' => [ - Route::FilterOut => fn($title) => Strings::webalize(Strings::truncate($title, 100, '')), - ], - '' => [/* le filtre de recherche ci-dessus */], -]); -``` - -Les deux filtres coopèrent. Le filtre général s'exécute en premier ; voyant que le slug est déjà rempli avec le titre fourni, il saute la recherche en base. Le `FilterOut` du paramètre transforme ensuite ce titre en un vrai slug. Les templates qui ne passent pas le titre continuent de fonctionner : le filtre général trouve le slug vide et passe par la recherche. - -N'utilisez cela que là où cela compte (grandes listes rendues des centaines de fois par requête). Pour l'essentiel de l'application, la recherche mise en cache est assez rapide. - - -Canonisation : rediriger vers la bonne URL -========================================== - -Nous savons désormais générer `/article/123-comment-faire-du-pain`, mais la route accepte toujours `/article/123` et `/article/123-nimporte-quoi-decrit`. C'est voulu : nous voulons des URLs courtes (voir plus bas) et nous voulons que les vieux liens ou ceux saisis à la main continuent de fonctionner. Mais nous ne voulons pas que les moteurs de recherche indexent le même article sous plusieurs adresses. - -La solution est la [canonisation |application:presenters#Canonisation] : lorsque l'utilisateur arrive par une URL non canonique, l'application le redirige en 301 vers la bonne. C'est la méthode `canonicalize()` qui s'en charge : - -```php -public function actionDetail(int $id, ?string $slug = null): void -{ - $article = $this->facade->getArticle($id); - if (!$article) { - $this->error(); - } - - // génère l'URL canonique par le même FilterOut - // et redirige en HTTP 301 si elle diffère de l'URL actuelle - $this->canonicalize('detail', ['id' => $id]); - - $this->template->article = $article; -} -``` - -`canonicalize()` génère l'URL canonique de la même façon que le ferait `link()` (elle passe donc par le même `FilterOut`) et la compare à l'URL actuelle. Si elles diffèrent, elle redirige en HTTP 301. Les visiteurs atterrissent sur la bonne URL, les moteurs de recherche ne voient qu'une seule version canonique. - - -Un seul endroit décide de la forme du slug -========================================== - -Remarquez que l'appel `Strings::webalize(Strings::truncate(..., 100, ''))` vit à un seul endroit : dans `SlugProvider` (ou dans le `FilterOut` du paramètre). La même logique produit le lien dans le template, l'URL dans `redirect()` et la forme canonique dans `canonicalize()`. - -Si vous voulez changer les règles plus tard (autre limite de longueur, autre translittération, suppression de caractères supplémentaires), vous ne modifiez qu'une ligne. Sans cela, vous risqueriez que `redirect()` génère `/article/123-comment-faire-du-pain` alors que `canonicalize()` attend `/article/123-comment-faire-du-pa` (parce que quelqu'un a appliqué ailleurs une autre longueur de `truncate`), et l'application redirigerait en boucle. - - -Bonus : les URLs courtes fonctionnent toujours -============================================== - -Comme le slug est facultatif, les adresses sans lui fonctionnent toujours : - -``` -/article/123 -``` - -C'est utile pour : -- **les QR codes** - une URL plus courte donne un code moins dense et plus facile à scanner -- **les SMS et les chats** - cela tient dans un tweet et reste net -- **les supports imprimés** - une URL courte se tape plus vite - -Lorsqu'un utilisateur ouvre une telle URL, `canonicalize()` le redirige en 301 vers la version complète avec le slug, si bien que les moteurs de recherche ne voient toujours que la forme canonique. Vous pouvez avoir la concision et le SEO en même temps. - - -Résumé -====== - -- Le masque `<id>[-<slug>]` rend le slug facultatif. Le `<slug>` par défaut ne correspond pas à `/` ; n'utilisez `<slug .+>` que si vous voulez vraiment des barres obliques dans le slug. -- Un `FilterOut` général sous la clé `''` va chercher le titre d'après l'ID - **aucune modification de template nulle part dans l'application**. -- Enveloppez la recherche dans un minuscule cache valable pour la requête en cours ; une requête en base par ID unique suffit largement. -- Éventuellement, un `FilterOut` propre au paramètre permet aux templates de passer directement le titre et d'éviter la recherche. -- `$this->canonicalize()` dans l'action redirige les URLs non canoniques vers la bonne en HTTP 301. -- La formule du slug (`webalize` + `truncate`) vit à un seul endroit : changez-la une fois, l'effet est partout. -- Les URLs courtes, réduites à l'ID, continuent de fonctionner, ce qui est pratique pour les QR codes et les SMS. - -Vous en apprendrez davantage sur les filtres et la canonisation dans la documentation du [routage |application:routing#Filtres généraux] et des [presenters |application:presenters#Canonisation]. diff --git a/best-practices/fr/restore-request.texy b/best-practices/fr/restore-request.texy deleted file mode 100644 index 9f6e7f584f..0000000000 --- a/best-practices/fr/restore-request.texy +++ /dev/null @@ -1,62 +0,0 @@ -Comment revenir à une page précédente ? -*************************************** - -.[perex] -Que se passe-t-il si un utilisateur remplit un formulaire et que sa session de connexion expire ? Pour éviter la perte de données, nous pouvons enregistrer la requête courante (données du formulaire comprises) dans la session avant de rediriger vers la page de connexion. Dans Nette, c'est étonnamment simple. - -La requête courante peut être stockée dans la session à l'aide de la méthode `storeRequest()`. Cette méthode renvoie un identifiant unique (une courte chaîne) de la requête stockée. Elle enregistre le nom du presenter courant, sa vue et ses paramètres. Si un formulaire a été envoyé dans le cadre de la requête, les valeurs saisies dans les champs sont également enregistrées (à l'exception des fichiers envoyés). - -La requête se restaure à l'aide de la méthode `restoreRequest($key)`, à laquelle vous passez l'identifiant obtenu précédemment. Cette méthode redirige l'utilisateur vers le presenter et la vue d'origine. Si la requête stockée comportait cependant l'envoi d'un formulaire, `restoreRequest()` utilise la méthode `forward()` au lieu de rediriger. Elle repasse au formulaire les valeurs précédemment saisies et lui permet d'être rendu à nouveau. L'utilisateur peut ainsi renvoyer le formulaire sans perdre les données saisies. - -Point essentiel : `restoreRequest()` vérifie que l'utilisateur nouvellement connecté est bien celui qui avait envoyé le formulaire à l'origine. S'il s'agit d'un autre utilisateur, la requête stockée n'est pas restaurée et la méthode ne fait rien, ce qui renforce la sécurité. - -Illustrons cela par un exemple. Prenons un `AdminPresenter` dans lequel des données sont modifiées. Sa méthode `startup()` vérifie que l'utilisateur est connecté. Si ce n'est pas le cas, l'utilisateur est redirigé vers `SignPresenter`. Nous stockons en même temps la requête courante à l'aide de `storeRequest()` et passons sa clé (le `$backlink`) à `SignPresenter`. - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - protected function startup() - { - parent::startup(); - - if (!$this->user->isLoggedIn()) { - $this->redirect('Sign:in', ['backlink' => $this->storeRequest()]); - } - } -} -``` - -Le `SignPresenter` contiendra, outre le formulaire de connexion, un paramètre persistant `$backlink` où la clé est stockée. Comme le paramètre est persistant, sa valeur est conservée même après l'envoi du formulaire de connexion. - - -```php -use Nette\Application\Attributes\Persistent; - -class SignPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $backlink = ''; - - protected function createComponentSignInForm() - { - $form = new Nette\Application\UI\Form; - // ... ajout des champs du formulaire ... - $form->onSuccess[] = $this->signInFormSuceeded(...); - return $form; - } - - private function signInFormSuceeded($form) - { - // ... connexion de l'utilisateur ici ... - - $this->restoreRequest($this->backlink); - $this->redirect('Admin:'); - } -} -``` - -Nous passons la clé (`$this->backlink`) de la requête stockée à la méthode `restoreRequest()`. Celle-ci redirige alors (ou fait un forward) l'utilisateur vers le presenter et la vue d'origine. - -Si la clé n'est cependant pas valide (par exemple parce qu'elle a expiré de la session), la méthode ne fait rien. L'appel suivant `$this->redirect('Admin:')` sert donc de repli et redirige vers une page par défaut comme `AdminPresenter`. - -{{priority: -1}} diff --git a/best-practices/it/@home.texy b/best-practices/it/@home.texy deleted file mode 100644 index ebfbdea34c..0000000000 --- a/best-practices/it/@home.texy +++ /dev/null @@ -1,69 +0,0 @@ -Tutorial e best practice -************************ - -.[perex] -Tutorial, soluzioni ai compiti più comuni e best practice per Nette. - - -<div class=documentation> -<div> - - -Nette Application ------------------ -- [Metodi e attributi inject |inject-method-attribute] -- [Comporre i presenter con i trait |presenter-traits] -- [Passare impostazioni ai presenter |passing-settings-to-presenters] -- [Come ripristinare una richiesta |restore-request] -- [Paginare i risultati del database |pagination] -- [Snippet dinamici |dynamic-snippets] -- [Come usare l'attributo #Requires |attribute-requires] -- [Come usare correttamente i link POST |post-links] -- [URL leggibili con gli slug |pretty-urls] - -</div> -<div> - - -Form ----- -- [Riutilizzare i form |form-reuse] -- [Form per creare e modificare record |creating-editing-form] -- [Creiamo un form di contatto |lets-create-contact-form] -- [Selectbox dipendenti |https://blog.nette.org/en/dependent-selectboxes-elegantly-in-nette-and-pure-js] - -</div> -<div> - - -Generale --------- -- [Come caricare un file di configurazione |bootstrap:] -- [Come scrivere microsite |microsites] -- [Perché Nette usa la notazione PascalCase per le costanti? |https://blog.nette.org/en/for-less-screaming-in-the-code] -- [Perché Nette non usa il suffisso Interface? |https://blog.nette.org/en/prefixes-and-suffixes-do-not-belong-in-interface-names] -- [Composer: consigli d'uso |composer] -- [Introduzione alla programmazione orientata agli oggetti |nette:introduction-to-object-oriented-programming] - -</div> -<div> - - -Soluzioni di esempio --------------------- -- [Esempi di Nette |https://github.com/nette-examples] -- [Doctrine & Nette |https://contributte.org/nettrine/] -- [Esempi di Contributte |https://contributte.org/examples.html] -- [Sito di Doctrine ORM |https://github.com/MinecordNetwork/Website] -- [Quick start |quickstart:] - -</div> -<div> - - -Video ------ -Centinaia di registrazioni degli incontri Last Saturday e video su Nette si trovano tutti in un unico posto, sul "canale YouTube di Nette Framework":https://www.youtube.com/user/NetteFramework. - -</div> -</div> diff --git a/best-practices/it/@left-menu.texy b/best-practices/it/@left-menu.texy deleted file mode 100644 index b1050b0687..0000000000 --- a/best-practices/it/@left-menu.texy +++ /dev/null @@ -1,34 +0,0 @@ -Tutorial e best practice -************************ -- [Panoramica |@home] - -Nette Application -***************** -- [Metodi e attributi inject |inject-method-attribute] -- [Comporre i presenter con i trait |presenter-traits] -- [Passare impostazioni ai presenter |passing-settings-to-presenters] -- [Come ripristinare una richiesta |restore-request] -- [Paginare i risultati del database |pagination] -- [Snippet dinamici |dynamic-snippets] -- [Come usare l'attributo #Requires |attribute-requires] -- [Come usare correttamente i link POST |post-links] -- [URL leggibili con slug |pretty-urls] - -Form -**** -- [Riutilizzare i form |form-reuse] -- [Form per creare e modificare record |creating-editing-form] -- [Creiamo un form di contatto |lets-create-contact-form] - -Generale -******** -- [Come scrivere microsite |microsites] -- [Composer: consigli d'uso |composer] - - -Letture consigliate -******************* -- [Documentazione di Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Risoluzione dei problemi |nette:troubleshooting] diff --git a/best-practices/it/@meta.texy b/best-practices/it/@meta.texy deleted file mode 100644 index 353ed5b4b6..0000000000 --- a/best-practices/it/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Tutorial e best practice}} diff --git a/best-practices/it/attribute-requires.texy b/best-practices/it/attribute-requires.texy deleted file mode 100644 index 3953b3c047..0000000000 --- a/best-practices/it/attribute-requires.texy +++ /dev/null @@ -1,177 +0,0 @@ -Come usare l'attributo `#[Requires]` -************************************ - -.[perex] -Quando scrivete un'applicazione web, capita spesso di dover limitare l'accesso a certe sue parti. Magari volete che alcune richieste possano inviare dati solo tramite un form (cioè con il metodo POST), oppure che siano accessibili solo alle chiamate AJAX. In Nette Framework 3.2 è comparso un nuovo strumento che permette di impostare limiti del genere in modo elegante e chiaro: l'attributo `#[Requires]`. - -Un attributo è un contrassegno particolare in PHP che si scrive prima della definizione di una classe o di un metodo. Poiché si tratta in sostanza di una classe, perché gli esempi seguenti funzionino occorre indicare la clausola `use`: - -```php -use Nette\Application\Attributes\Requires; -``` - -L'attributo `#[Requires]` si può usare sulla classe stessa del presenter e su questi metodi: - -- `action<Action>()` -- `render<View>()` -- `handle<Signal>()` -- `createComponent<Name>()` - -Gli ultimi due metodi riguardano anche i componenti, quindi potete usare l'attributo pure con essi. - -Se le condizioni indicate dall'attributo non sono soddisfatte, viene generato un errore HTTP 4xx. - - -Metodi HTTP ------------ - -Potete indicare quali metodi HTTP (come GET, POST ecc.) sono ammessi per l'accesso. Se per esempio volete consentire l'accesso solo tramite l'invio di un form, impostate: - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST')] - public function actionDelete(int $id): void - { - } -} -``` - -Perché per le azioni che cambiano lo stato dovreste usare POST invece di GET e come farlo? [Leggete la guida |post-links]. - -Potete indicare un metodo oppure un array di metodi. Un caso particolare è il valore `'*'`, che consente tutti i metodi, cosa che i presenter [per motivi di sicurezza non permettono per impostazione predefinita |application:presenters#Controllo del metodo HTTP]. - - -Chiamate AJAX -------------- - -Se volete che un presenter o un metodo sia accessibile solo per le richieste AJAX, usate: - -```php -#[Requires(ajax: true)] -class AjaxPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Stessa origine --------------- - -Per aumentare la sicurezza potete richiedere che la richiesta provenga dallo stesso dominio. Così eviterete la [vulnerabilità CSRF |nette:vulnerability-protection#Cross-Site Request Forgery (CSRF)]: - -```php -#[Requires(sameOrigin: true)] -class SecurePresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Per i metodi `handle<Signal>()` l'accesso dallo stesso dominio è richiesto automaticamente. Se quindi volete consentire l'accesso da qualsiasi dominio, indicate: - -```php -#[Requires(sameOrigin: false)] -public function handleList(): void -{ -} -``` - - -Accesso tramite forward ------------------------ - -A volte è utile limitare l'accesso a un presenter in modo che sia disponibile solo indirettamente, per esempio con i metodi `forward()` o `switch()` da un altro presenter. Così si proteggono per esempio i presenter di errore, perché non sia possibile richiamarli da URL: - -```php -#[Requires(forward: true)] -class ForwardedPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Nella pratica capita spesso di dover contrassegnare certe viste alle quali si può accedere solo in base alla logica del presenter. Anche in questo caso perché non si possano aprire 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 ------------------ - -Potete anche limitare un certo codice, per esempio la creazione di un componente, in modo che sia accessibile solo per determinate azioni del presenter: - -```php -class EditDeletePresenter extends Nette\Application\UI\Presenter -{ - #[Requires(actions: ['add', 'edit'])] - public function createComponentPostForm() - { - } -} -``` - -Nel caso di una sola azione non serve scrivere un array: `#[Requires(actions: 'default')]` - - -Attributi propri ----------------- - -Se volete usare l'attributo `#[Requires]` ripetutamente con le stesse impostazioni, potete creare un vostro attributo che eredita da `#[Requires]` e lo configura secondo le vostre esigenze. - -Per esempio `#[SingleAction]` consente l'accesso solo tramite 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 con tutti i metodi HTTP usati per le 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 ------------ - -L'attributo `#[Requires]` vi dà grande flessibilità e controllo su come si accede alle vostre pagine web. Con regole semplici ma efficaci potete aumentare la sicurezza e il corretto funzionamento della vostra applicazione. Come vedete, usare gli attributi in Nette può non solo semplificare il vostro lavoro, ma anche renderlo più sicuro. diff --git a/best-practices/it/composer.texy b/best-practices/it/composer.texy deleted file mode 100644 index b779a4a8d2..0000000000 --- a/best-practices/it/composer.texy +++ /dev/null @@ -1,282 +0,0 @@ -Consigli per l'uso di Composer -****************************** - -<div class=perex> - -Composer è uno strumento per gestire le dipendenze in PHP. Permette di dichiarare le librerie da cui il vostro progetto dipende e le installa e aggiorna per voi. Impareremo: - -- come installare Composer -- come usarlo in un progetto nuovo o esistente - -</div> - - -Installazione -============= - -Composer è un file eseguibile `.phar` che scaricate e installate così. - - -Windows -------- - -Usate l'installer ufficiale [Composer-Setup.exe|https://getcomposer.org/Composer-Setup.exe]. - - -Linux, macOS ------------- - -Vi bastano 4 comandi, che potete copiare da [questa pagina |https://getcomposer.org/download/]. - -Inoltre, copiandolo in una cartella che si trova nel `PATH` di sistema, Composer diventa accessibile globalmente: - -```shell -$ mv ./composer.phar ~/bin/composer # oppure /usr/local/bin/composer -``` - - -Uso nel progetto -================ - -Per iniziare a usare Composer nel vostro progetto vi basta il file `composer.json`. Questo file descrive le dipendenze del vostro progetto e può contenere anche altri metadati. Il `composer.json` più semplice può apparire così: - -```js -{ - "require": { - "nette/database": "^3.0" - } -} -``` - -Diciamo qui che la nostra applicazione (o libreria) richiede il pacchetto `nette/database` (il nome del pacchetto è composto dal nome del vendor e dal nome del progetto) e che vuole una versione che corrisponda al vincolo `^3.0` (cioè l'ultima versione 3). - -Con il file `composer.json` nella radice del progetto lanciamo quindi: - -```shell -composer update -``` - -Composer scaricherà Nette Database nella directory `vendor/`. Crea anche il file `composer.lock`, che contiene l'informazione su quali versioni esatte delle librerie ha installato. - -Composer genera il file `vendor/autoload.php`. Potete semplicemente includere questo file e iniziare a usare le classi delle librerie senza altro lavoro: - -```php -require __DIR__ . '/vendor/autoload.php'; - -$db = new Nette\Database\Connection('sqlite::memory:'); -``` - - -Aggiornare i pacchetti alle ultime versioni -=========================================== - -Per aggiornare le librerie usate alle ultime versioni secondo i vincoli definiti in `composer.json` serve il comando `composer update`. Per esempio con la dipendenza `"nette/database": "^3.0"` installerà l'ultima versione 3.x.x, ma non la versione 4. - -Per aggiornare i vincoli nel file `composer.json`, per esempio a `"nette/database": "^4.1"`, e permettere così l'installazione dell'ultima versione, serve il comando `composer require nette/database`. - -Per aggiornare tutti i pacchetti Nette usati bisognerebbe elencarli tutti sulla riga di comando, per esempio: - -```shell -composer require nette/application nette/forms latte/latte tracy/tracy ... -``` - -Il che è poco pratico. Usate quindi il semplice script "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff che lo fa per voi: - -```shell -php composer-frontline.php -``` - - -Creare un nuovo progetto -======================== - -Un nuovo progetto Nette lo create con un unico comando: - -```shell -composer create-project nette/web-project nome-del-progetto -``` - -Al posto di `nome-del-progetto` inserite il nome della directory per il vostro progetto ed eseguite il comando. Composer scaricherà da GitHub il repository `nette/web-project`, che contiene già il file `composer.json`, e subito dopo installerà il Nette Framework stesso. Non resta che [impostare i permessi delle directory |nette:troubleshooting#Impostazione dei permessi delle directory] `temp/` e `log/` e il progetto dovrebbe essere vivo. - -Se sapete su quale versione di PHP sarà ospitato il progetto, non dimenticate di [impostarla |#Versione di PHP]. - - -Versione di PHP -=============== - -Composer installa sempre le versioni dei pacchetti compatibili con la versione di PHP che state usando (più precisamente con la versione di PHP usata sulla riga di comando quando lanciate Composer). Che però probabilmente non è la stessa versione usata dal vostro hosting. Per questo è molto importante aggiungere al file `composer.json` l'informazione sulla versione di PHP sull'hosting. Da quel momento verranno installate solo versioni dei pacchetti compatibili con l'hosting. - -Per impostare per esempio che il progetto girerà su PHP 8.2.3 usate il comando: - -```shell -composer config platform.php 8.2.3 -``` - -Così la versione verrà scritta nel file `composer.json`: - -```js -{ - "config": { - "platform": { - "php": "8.2.3" - } - } -} -``` - -Il numero di versione di PHP si indica però nel file anche in un altro punto, nella sezione `require`. Mentre il primo numero determina per quale versione vengono installati i pacchetti, il secondo dice per quale versione è scritta l'applicazione stessa. In base a esso, per esempio, PhpStorm imposta il *PHP language level*. (Naturalmente non ha senso che queste versioni differiscano, quindi la doppia indicazione è una svista.) Questa versione la impostate con il comando: - -```shell -composer require php 8.2.3 --no-update -``` - -Oppure direttamente nel file `composer.json`: - -```js -{ - "require": { - "php": "8.2.3" - } -} -``` - - -Ignorare la versione di PHP -=========================== - -I pacchetti indicano di solito sia la versione minima di PHP con cui sono compatibili, sia la versione massima con cui sono stati testati. Se avete intenzione di usare una versione di PHP ancora più recente, magari per fare delle prove, Composer si rifiuterà di installare un pacchetto del genere. La soluzione è l'opzione `--ignore-platform-req=php+`, che fa ignorare a Composer i limiti superiori della versione di PHP richiesta. - - -Segnalazioni false -================== - -Aggiornando i pacchetti o cambiando i numeri di versione capitano dei conflitti. Un pacchetto ha requisiti che sono in conflitto con un altro e così via. Composer però a volte emette segnalazioni false. Segnala un conflitto che in realtà non esiste. In un caso del genere aiuta cancellare il file `composer.lock` e riprovare. - -Se il messaggio di errore persiste, allora è serio e bisogna leggerlo per capire cosa e come modificare. - - -Packagist.org - repository globale -================================== - -[Packagist |https://packagist.org] è il repository principale in cui Composer cerca i pacchetti per impostazione predefinita. Qui potete anche pubblicare i vostri pacchetti. - - -E se non volessimo il repository centrale ------------------------------------------ - -Se abbiamo in azienda applicazioni o librerie interne che non possono essere ospitate pubblicamente, possiamo crearci i nostri repository. - -Di più sui repository nella [documentazione ufficiale |https://getcomposer.org/doc/05-repositories.md#repositories]. - - -Autoloading -=========== - -Una caratteristica fondamentale di Composer è che fornisce l'autoloading per tutte le classi che installa. Lo attivate includendo il file `vendor/autoload.php`. - -Composer si può però usare anche per caricare altre classi al di fuori della directory `vendor/`. La prima possibilità è lasciare che Composer scandisca le directory e sottodirectory indicate, trovi tutte le classi e le includa nell'autoloader. Per ottenerlo impostate in `composer.json` `autoload > classmap`: - -```js -{ - "autoload": { - "classmap": [ - "src/", # include la directory src/ e le sue sottodirectory - ] - } -} -``` - -Dopo di che bisogna lanciare il comando `composer dumpautoload` a ogni modifica per rigenerare le tabelle di autoloading. Il che è estremamente scomodo. È molto meglio affidare questo compito a [RobotLoader|robot-loader:], che svolge la stessa attività automaticamente in background e molto più velocemente. - -La seconda possibilità è rispettare lo standard [PSR-4 |https://www.php-fig.org/psr/psr-4/]. In parole semplici, è un sistema in cui i namespace e i nomi delle classi corrispondono alla struttura delle directory e ai nomi dei file, per esempio `App\Core\RouterFactory` si troverà nel file `/percorso/verso/App/Core/RouterFactory.php`. Esempio di configurazione: - -```js -{ - "autoload": { - "psr-4": { - "App\\": "app/" # il namespace App\ è nella directory app/ - } - } -} -``` - -Come configurare questo comportamento lo trovate nella [documentazione di Composer |https://getcomposer.org/doc/04-schema.md#psr-4]. - - -Provare le versioni nuove -========================= - -Volete provare una nuova versione di sviluppo di un pacchetto? Ecco come. Prima di tutto aggiungete al file `composer.json` questa coppia di opzioni, che permettono di installare versioni di sviluppo, ma Composer vi ricorrerà solo se nessuna combinazione di versioni stabili soddisfa i requisiti: - -```js -{ - "minimum-stability": "dev", - "prefer-stable": true, -} -``` - -Consigliamo inoltre di cancellare il file `composer.lock`, perché a volte Composer rifiuta inspiegabilmente l'installazione e questo risolve il problema. - -Diciamo che il pacchetto sia `nette/utils` e la nuova versione la 4.0. La installate con il comando: - -```shell -composer require nette/utils:4.0.x-dev -``` - -Oppure potete installare una versione concreta, per esempio la 4.0.0-RC2: - -```shell -composer require nette/utils:4.0.0-RC2 -``` - -Se però un altro pacchetto dipende dalla libreria ed è vincolato a una versione più vecchia (per esempio `^3.1`), la soluzione ideale è aggiornare quel pacchetto perché funzioni con la nuova versione. Se però volete solo aggirare la limitazione e costringere Composer a installare la versione di sviluppo facendo finta che sia una versione più vecchia (per esempio la 3.1.6), potete usare la parola chiave `as`: - -```shell -composer require nette/utils "4.0.x-dev as 3.1.6" -``` - - -Richiamare comandi -================== - -Tramite Composer potete richiamare comandi e script vostri predefiniti come se fossero comandi nativi di Composer. Per gli script che si trovano nella directory `vendor/bin` non serve indicare questo percorso. - -Come esempio definiamo in `composer.json` uno script che usa [Nette Tester |tester:] per eseguire i test: - -```js -{ - "scripts": { - "tester": "tester tests -s" - } -} -``` - -I test li lanciamo poi con `composer tester`. Potete richiamare il comando anche se non vi trovate nella directory radice del progetto, ma in una delle sue sottodirectory. - - -Mandate un grazie -================= - -Vi mostriamo un trucco con cui farete piacere agli autori open source. In modo semplice date su GitHub una stella alle librerie che il vostro progetto usa. Basta installare la libreria `symfony/thanks`: - -```shell -composer global require symfony/thanks -``` - -E poi lanciare: - -```shell -composer thanks -``` - -Provate! - - -Configurazione -============== - -Composer è strettamente integrato con lo strumento di versionamento [Git |https://git-scm.com]. Se non avete Git installato, bisogna dire a Composer di non usarlo: - -```shell -composer -g config preferred-install dist -``` diff --git a/best-practices/it/creating-editing-form.texy b/best-practices/it/creating-editing-form.texy deleted file mode 100644 index 9ac114e0de..0000000000 --- a/best-practices/it/creating-editing-form.texy +++ /dev/null @@ -1,204 +0,0 @@ -Form per creare e modificare un record -************************************** - -.[perex] -Come realizzare correttamente in Nette l'aggiunta e la modifica di un record usando per entrambe lo stesso form? - -In molti casi i form per aggiungere e per modificare un record sono identici e si distinguono al massimo per l'etichetta del pulsante. Mostreremo esempi di presenter semplici in cui useremo il form prima per aggiungere un record, poi per modificarlo e infine uniremo le due soluzioni. - - -Aggiungere un record --------------------- - -Esempio di presenter per aggiungere un record. Lasceremo il lavoro vero e proprio sul database alla classe `Facade`, il cui codice non è essenziale per questo esempio. - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentRecordForm(): Form - { - $form = new Form; - - // ... aggiunta dei campi del form ... - - $form->onSuccess[] = $this->recordFormSucceeded(...); - return $form; - } - - private function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // aggiunta del record al database - $this->flashMessage('Aggiunto con successo'); - $this->redirect('...'); - } - - public function renderAdd(): void - { - // ... - } -} -``` - - -Modificare un record --------------------- - -Vediamo ora come apparirebbe un presenter per modificare un record: - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - private $record; - - public function __construct( - private Facade $facade, - ) { - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // verifica dell'esistenza del record - || !$this->facade->isEditAllowed(/*...*/) // controllo dei permessi - ) { - $this->error(); // errore 404 - } - - $this->record = $record; - } - - protected function createComponentRecordForm(): Form - { - // verifica che l'azione sia 'edit' - if ($this->getAction() !== 'edit') { - $this->error(); - } - - $form = new Form; - - // ... aggiunta dei campi del form ... - - $form->setDefaults($this->record); // impostazione dei valori predefiniti - $form->onSuccess[] = $this->recordFormSucceeded(...); - return $form; - } - - private function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->update($this->record->id, $data); // aggiornamento del record - $this->flashMessage('Aggiornato con successo'); - $this->redirect('...'); - } -} -``` - -Nel metodo *action*, che viene invocato all'inizio del [ciclo di vita del presenter |application:presenters#Ciclo di vita del presenter], verifichiamo l'esistenza del record e il permesso dell'utente di modificarlo. - -Salviamo il record nella proprietà `$record`, così è disponibile nel metodo `createComponentRecordForm()` per impostare i valori predefiniti e in `recordFormSucceeded()` per accedere all'ID. Una soluzione alternativa è impostare i valori predefiniti direttamente in `actionEdit()` e ottenere il valore dell'ID (parte dell'URL) con `getParameter('id')`: - - -```php - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - // verifica dell'esistenza e controllo dei permessi - ) { - $this->error(); - } - - // impostazione dei valori predefiniti del form - $this->getComponent('recordForm') - ->setDefaults($record); - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); - // ... - } -``` - -Però, e questa dovrebbe essere **la cosa più importante di tutto il codice**, quando creiamo il form dobbiamo assicurarci che l'azione sia davvero `edit`. Altrimenti la verifica nel metodo `actionEdit()` non avverrebbe affatto! - - -Lo stesso form per aggiungere e modificare ------------------------------------------- - -Uniamo ora i due presenter in uno solo. Potremmo distinguere l'azione nel metodo `createComponentRecordForm()` e configurare il form di conseguenza, oppure possiamo delegarlo direttamente ai metodi action ed eliminare il controllo condizionale: - - -```php -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - public function actionAdd(): void - { - $form = $this->getComponent('recordForm'); - $form->onSuccess[] = $this->addingFormSucceeded(...); - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // verifica dell'esistenza del record - || !$this->facade->isEditAllowed(/*...*/) // controllo dei permessi - ) { - $this->error(); // errore 404 - } - - $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // impostazione dei valori predefiniti - $form->onSuccess[] = $this->editingFormSucceeded(...); - } - - protected function createComponentRecordForm(): Form - { - // verifica che l'azione sia 'add' oppure 'edit' - if (!in_array($this->getAction(), ['add', 'edit'])) { - $this->error(); - } - - $form = new Form; - - // ... aggiunta dei campi del form ... - - return $form; - } - - private function addingFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // aggiunta del record al database - $this->flashMessage('Aggiunto con successo'); - $this->redirect('...'); - } - - private function editingFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // aggiornamento del record - $this->flashMessage('Aggiornato con successo'); - $this->redirect('...'); - } -} -``` - -{{priority: -1}} diff --git a/best-practices/it/dynamic-snippets.texy b/best-practices/it/dynamic-snippets.texy deleted file mode 100644 index cb506c1be9..0000000000 --- a/best-practices/it/dynamic-snippets.texy +++ /dev/null @@ -1,176 +0,0 @@ -Snippet dinamici -**************** - -.[perex] -Come usare AJAX per aggiornare solo le parti di pagina che cambiano davvero, per esempio i singoli elementi di un elenco, grazie agli snippet dinamici di Latte. - -Durante lo sviluppo di un'applicazione capita spesso di dover eseguire operazioni AJAX, per esempio sulle singole righe di una tabella o sugli elementi di un elenco. Come esempio prendiamo un elenco di articoli in cui gli utenti connessi possono valutare ogni articolo con "mi piace" o "non mi piace". Il codice del presenter e il template corrispondente senza AJAX assomiglierebbero a questo (mostriamo le parti più importanti; il codice presuppone l'esistenza di un servizio per gestire le valutazioni e ottenere gli articoli, la cui implementazione concreta non è essenziale per questa guida): - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - $this->redirect('this'); -} - -public function handleUnlike(int $articleId): void -{ - $this->ratingService->removeLike($articleId, $this->user->id); - $this->redirect('this'); -} -``` - -Template: - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {if !$article->liked} - <a n:href="like!, $article->id" class=ajax>Mi piace</a> - {else} - <a n:href="unlike!, $article->id" class=ajax>Non mi piace più</a> - {/if} -</article> -``` - - -Ajaxizzazione -============= - -Aggiungiamo ora il supporto AJAX a questa semplice applicazione. Cambiare la valutazione di un articolo non è così importante da richiedere un redirect dell'intera pagina, quindi idealmente dovrebbe avvenire con AJAX in background. Useremo lo [script gestore dei componenti aggiuntivi |application:ajax#Naja] con la convenzione consueta secondo cui i link AJAX hanno la classe CSS `ajax`. - -Ma come realizzarlo concretamente? Nette offre due strade: gli snippet dinamici e i componenti. Entrambe hanno pregi e difetti, quindi le mostreremo una per una. - - -La strada degli snippet dinamici -================================ - -Nella terminologia di Latte, snippet dinamico indica un uso particolare del tag `{snippet}` in cui nel nome dello snippet compare una variabile. Uno snippet del genere non si può collocare in un punto qualsiasi del template: deve essere avvolto da uno snippet statico (normale) oppure trovarsi dentro un `{snippetArea}`. Il nostro template potrebbe essere modificato così: - - -```latte -{snippet articlesContainer} - <article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {snippet article-{$article->id}} - {if !$article->liked} - <a n:href="like!, $article->id" class=ajax>Mi piace</a> - {else} - <a n:href="unlike!, $article->id" class=ajax>Non mi piace più</a> - {/if} - {/snippet} - </article> -{/snippet} -``` - -Ogni articolo definisce ora uno snippet il cui nome contiene l'ID dell'articolo. Tutti questi snippet dinamici sono poi avvolti insieme da uno snippet statico chiamato `articlesContainer`. Se omettessimo lo snippet esterno, Latte lancerebbe un'eccezione. - -Non resta che aggiungere al presenter la logica di ridisegno: basta ridisegnare l'involucro statico. - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - if ($this->isAjax()) { - $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- non necessario - } else { - $this->redirect('this'); - } -} -``` - -Modificate allo stesso modo il metodo `handleUnlike()` corrispondente e AJAX funziona! - -Questa soluzione ha però un punto debole. Se esaminiamo più da vicino la richiesta AJAX, scopriamo che l'applicazione, pur apparendo efficiente dall'esterno (restituisce un solo snippet per l'articolo in questione), sul server esegue in realtà il rendering di *tutti* gli snippet. Mette nel payload quello richiesto e scarta gli altri (cioè li ha anche inutilmente caricati e renderizzati). - -Per ottimizzare la cosa dobbiamo intervenire dove la collezione `$articles` viene passata al template (diciamo nel metodo `renderDefault()`). Sfrutteremo il fatto che la gestione del segnale avviene prima dei metodi `render<Qualcosa>`: - -```php -public function handleLike(int $articleId): void -{ - // ... - if ($this->isAjax()) { - // ... - $this->template->articles = [ - $this->db->table('articles')->get($articleId), - ]; - } else { - // ... -} - -public function renderDefault(): void -{ - if (!isset($this->template->articles)) { - $this->template->articles = $this->db->table('articles'); - } -} -``` - -Ora, durante l'elaborazione del segnale, al template non viene passata l'intera collezione di articoli, ma solo un array con il singolo articolo che ci interessa, quello che vogliamo renderizzare e inviare nel payload al browser. Il ciclo `{foreach}` viene quindi eseguito una sola volta e non si renderizza nessuno snippet superfluo. - - -La strada dei componenti -======================== - -Un approccio del tutto diverso rinuncia agli snippet dinamici. Il trucco sta nel racchiudere tutta la logica in un componente separato. Invece che il presenter, a occuparsi della valutazione sarà un apposito `LikeControl`. La classe apparirà così (conterrà anche i metodi `render`, `handleUnlike` ecc.): - -```php -class LikeControl extends Nette\Application\UI\Control -{ - public function __construct( - private Article $article, - ) { - } - - public function handleLike(): void - { - $this->ratingService->saveLike($this->article->id, $this->presenter->user->id); - if ($this->presenter->isAjax()) { - $this->redrawControl(); - } else { - $this->presenter->redirect('this'); - } - } -} -``` - -Template del componente: - -```latte -{snippet} - {if !$article->liked} - <a n:href="like!" class=ajax>Mi piace</a> - {else} - <a n:href="unlike!" class=ajax>Non mi piace più</a> - {/if} -{/snippet} -``` - -Naturalmente cambierà il template della vista e dovremo aggiungere una factory al presenter. Poiché creeremo un'istanza di questo componente per ogni articolo caricato dal database, useremo la classe [Multiplier |application:Multiplier] per gestirne la creazione. - -```php -protected function createComponentLikeControl() -{ - $articles = $this->db->table('articles'); - return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { - return new LikeControl($articles[$articleId]); - }); -} -``` - -Il template della vista si riduce al minimo indispensabile (e non contiene alcuno snippet!): - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {control "likeControl-$article->id"} -</article> -``` - -Abbiamo quasi finito: l'applicazione funzionerà ora con AJAX. Anche qui serve un'ottimizzazione, perché con l'uso di Nette Database la gestione del segnale carica inutilmente dal database tutti gli articoli invece del solo articolo che interessa. Il vantaggio, però, è che non avviene alcun rendering superfluo: viene renderizzata solo la specifica istanza del componente. - -{{priority: -1}} diff --git a/best-practices/it/form-reuse.texy b/best-practices/it/form-reuse.texy deleted file mode 100644 index b2647d659b..0000000000 --- a/best-practices/it/form-reuse.texy +++ /dev/null @@ -1,348 +0,0 @@ -Riutilizzare i form in più punti -******************************** - -.[perex] -Nette offre diverse possibilità per usare lo stesso form in più punti senza duplicare il codice. In questo articolo mostreremo le varie soluzioni, comprese quelle che dovreste evitare. - - -Factory di form -=============== - -Un approccio di base per usare lo stesso componente in più punti è creare un metodo o una classe che genera questo componente, e poi richiamarlo dai vari punti dell'applicazione. Un metodo o una classe del genere si chiama *factory*. Non confondetela per favore con il design pattern *metodo factory*, che descrive un modo particolare di usare le factory e con questo tema non ha nulla a che vedere. - -Come esempio creiamo una factory che costruisce un form di modifica: - -```php -use Nette\Application\UI\Form; - -class FormFactory -{ - public function createEditForm(): Form - { - $form = new Form; - $form->addText('title', 'Titolo:'); - // qui si aggiungono gli altri campi del form - $form->addSubmit('send', 'Salva'); - return $form; - } -} -``` - -Ora potete usare questa factory in vari punti della vostra applicazione, per esempio nei presenter o nei componenti. E lo fate [facendovela passare come dipendenza |dependency-injection:passing-dependencies]. Per prima cosa quindi registriamo la classe nel file di configurazione: - -```neon -services: - - FormFactory -``` - -E poi la usiamo nel presenter: - - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->createEditForm(); - $form->onSuccess[] = function () { - // elaborazione dei dati inviati - }; - return $form; - } -} -``` - -La factory di form la potete estendere con altri metodi per creare gli altri tipi di form di cui la vostra applicazione ha bisogno. E naturalmente possiamo aggiungere anche un metodo che crea un form di base senza elementi, che gli altri metodi poi useranno: - -```php -class FormFactory -{ - public function createForm(): Form - { - $form = new Form; - return $form; - } - - public function createEditForm(): Form - { - $form = $this->createForm(); - $form->addText('title', 'Titolo:'); - // qui si aggiungono gli altri campi del form - $form->addSubmit('send', 'Salva'); - return $form; - } -} -``` - -Il metodo `createForm()` non fa ancora nulla di utile, ma questo cambierà presto. - - -Dipendenze della factory -======================== - -Con il tempo può emergere l'esigenza che i form siano multilingue. Il che significa che a tutti i form dobbiamo impostare un [translator |forms:rendering#Traduzione]. Per farlo modifichiamo la classe `FormFactory` in modo che si faccia passare nel costruttore l'oggetto `Translator` e lo imposti al form creato: - -```php -use Nette\Localization\Translator; - -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function createForm(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } - - // ... -} -``` - -Poiché il metodo `createForm()` viene chiamato anche dagli altri metodi che creano form concreti, basta impostare il translator solo qui. E abbiamo finito. Non serve modificare il codice di nessun presenter o componente, il che è ottimo. - - -Più classi factory -================== - -In alternativa potete creare più classi per ogni form che volete usare nell'applicazione. Questo approccio può aumentare la leggibilità del codice e rendere più semplice la gestione dei form. Lasciamo che la `FormFactory` originale crei solo un form puro con la configurazione di base (per esempio con il supporto alla traduzione) e per il form di modifica creiamo una nuova factory `EditFormFactory`. - -```php -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function create(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } -} - - -// ✅ uso della composizione -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - // qui si aggiungono gli altri campi del form - $form->addSubmit('send', 'Salva'); - return $form; - } -} -``` - -È molto importante che il legame tra le classi `FormFactory` e `EditFormFactory` sia realizzato con la [composizione |nette:introduction-to-object-oriented-programming#Composizione] e non con l'[ereditarietà tra oggetti |nette:introduction-to-object-oriented-programming#Ereditarietà]: - -```php -// ⛔ NO! L'EREDITARIETÀ QUI NON C'ENTRA -class EditFormFactory extends FormFactory -{ - public function create(): Form - { - $form = parent::create(); - $form->addText('title', 'Titolo:'); - // qui si aggiungono gli altri campi del form - $form->addSubmit('send', 'Salva'); - return $form; - } -} -``` - -Usare qui l'ereditarietà sarebbe del tutto controproducente. Incontrereste dei problemi molto in fretta. Per esempio, nel momento in cui voleste aggiungere dei parametri al metodo `create()`, PHP segnalerebbe un errore perché la sua firma sarebbe diversa da quella dell'antenato. Oppure nel momento in cui passaste alla classe `EditFormFactory` una dipendenza tramite il costruttore. Nascerebbe quello che si chiama [inferno dei costruttori |dependency-injection:passing-dependencies#L'inferno dei costruttori]. - -In generale è meglio preferire la [composizione all'ereditarietà |dependency-injection:faq#Perché si preferisce la composizione all'ereditarietà?]. - - -Gestione del form -================= - -Anche il gestore del form, che viene richiamato dopo l'invio riuscito, può far parte della classe factory. Funzionerà passando i dati inviati al modello perché li elabori. Gli eventuali errori li [restituirà |forms:validation#Elaborare gli errori] al form. Il modello nell'esempio seguente è rappresentato dalla classe `Facade`: - -```php -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - private Facade $facade, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - $form->addText('title', 'Titolo:'); - // qui si aggiungono gli altri campi del form - $form->addSubmit('send', 'Salva'); - $form->onSuccess[] = $this->processForm(...); - return $form; - } - - private function processForm(Form $form, array $data): void - { - try { - // elaborazione dei dati inviati - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - } - } -} -``` - -Il redirect vero e proprio lo lasciamo però al presenter. Esso aggiunge all'evento `onSuccess` un altro gestore, che esegue il redirect. Grazie a questo si potrà usare il form in presenter diversi e ognuno reindirizzerà altrove. - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditFormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->create(); - $form->onSuccess[] = function () { - $this->flashMessage('Il record è stato salvato'); - $this->redirect('Homepage:'); - }; - return $form; - } -} -``` - -Questa soluzione sfrutta la proprietà dei form per cui, se sul form o su un suo elemento viene chiamato `addError()`, i gestori `onSuccess` successivi non vengono più richiamati. - - -Ereditare dalla classe Form -=========================== - -Un form assemblato non dovrebbe essere un discendente della classe `Form`. In altre parole, non usate questa soluzione: - -```php -// ⛔ NO! L'EREDITARIETÀ QUI NON C'ENTRA -class EditForm extends Form -{ - public function __construct(Translator $translator) - { - parent::__construct(); - $this->addText('title', 'Titolo:'); - // qui si aggiungono gli altri campi del form - $this->addSubmit('send', 'Salva'); - $this->setTranslator($translator); - } -} -``` - -Invece di assemblare il form nel costruttore, usate una factory. - -È importante rendersi conto che la classe `Form` è anzitutto uno strumento per assemblare i form, cioè un *form builder*. E il form assemblato si può considerare il suo prodotto. Ma il prodotto non è un caso particolare del builder, tra loro non c'è la relazione *is a* su cui si fonda l'ereditarietà. - - -Componente con form -=================== - -Un approccio del tutto diverso consiste nel creare un [componente |application:components] che contiene un form. Questo dà nuove possibilità, per esempio renderizzare il form in un modo particolare, dato che il componente ha un proprio template. Oppure si possono usare i segnali per la comunicazione AJAX e per caricare informazioni nel form, per esempio per i suggerimenti, e così via. - - -```php -use Nette\Application\UI\Form; - -class EditControl extends Nette\Application\UI\Control -{ - public array $onSave = []; - - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentForm(): Form - { - $form = new Form; - $form->addText('title', 'Titolo:'); - // qui si aggiungono gli altri campi del form - $form->addSubmit('send', 'Salva'); - $form->onSuccess[] = $this->processForm(...); - - return $form; - } - - private function processForm(Form $form, array $data): void - { - try { - // elaborazione dei dati inviati - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - return; - } - - // richiamo dell'evento - $this->onSave($this, $data); - } -} -``` - -Creiamo poi una factory che produrrà questo componente. Basta [definirne l'interfaccia |application:components#Componenti con dipendenze]: - -```php -interface EditControlFactory -{ - function create(): EditControl; -} -``` - -E aggiungerla al file di configurazione: - -```neon -services: - - EditControlFactory -``` - -E ora possiamo farci passare la factory e usarla nel presenter: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditControlFactory $controlFactory, - ) { - } - - protected function createComponentEditForm(): EditControl - { - $control = $this->controlFactory->create(); - - $control->onSave[] = function (EditControl $control, $data) { - $this->redirect('this'); - // oppure redirect al risultato della modifica, per esempio: - // $this->redirect('detail', ['id' => $data->id]); - }; - - return $control; - } -} -``` diff --git a/best-practices/it/inject-method-attribute.texy b/best-practices/it/inject-method-attribute.texy deleted file mode 100644 index c76db495d8..0000000000 --- a/best-practices/it/inject-method-attribute.texy +++ /dev/null @@ -1,61 +0,0 @@ -Metodi e attributi inject -************************* - -.[perex] -Questo articolo si concentra sui vari modi di passare le dipendenze ai presenter nel framework Nette. Confronteremo il metodo preferito, l'iniezione tramite costruttore, con le alternative, cioè i metodi e gli attributi `inject`. - -Per i presenter, come per le altre classi, il modo preferito di passare le dipendenze è tramite il [costruttore |dependency-injection:passing-dependencies#Iniezione tramite costruttore]. Se però create un antenato comune dal quale gli altri presenter ereditano (per esempio `BasePresenter`) e anche questo antenato richiede delle dipendenze, può nascere un problema noto come [inferno dei costruttori |dependency-injection:passing-dependencies#L'inferno dei costruttori]. Lo si può aggirare con metodi alternativi, cioè con i metodi e gli attributi inject (in passato annotazioni). - - -Metodi `inject*()` -================== - -È una forma di passaggio delle dipendenze tramite [setter |dependency-injection:passing-dependencies#Setter injection]. I nomi di questi setter devono iniziare con il prefisso `inject`. Nette DI chiama automaticamente i metodi con questo nome subito dopo aver creato l'istanza del presenter, passando loro tutte le dipendenze necessarie. Devono quindi essere dichiarati pubblici. - -I metodi `inject*()` si possono vedere come estensioni del costruttore, suddivise in più metodi. Questo permette a `BasePresenter` di ricevere le proprie dipendenze con un metodo separato, lasciando libero il costruttore per i suoi discendenti: - -```php -abstract class BasePresenter extends Nette\Application\UI\Presenter -{ - private Foo $foo; - - public function injectBase(Foo $foo): void - { - $this->foo = $foo; - } -} - -class MyPresenter extends BasePresenter -{ - private Bar $bar; - - public function __construct(Bar $bar) - { - $this->bar = $bar; - } -} -``` - -Un presenter può avere un numero qualsiasi di metodi `inject*()`, e ognuno può accettare un numero qualsiasi di parametri. Questo approccio si presta bene anche ai casi in cui un presenter è [composto da trait |presenter-traits] e ogni trait richiede le proprie dipendenze. - - -Attributi `Inject` -================== - -È una forma di [iniezione nelle proprietà |dependency-injection:passing-dependencies#Iniezione tramite proprietà]. Basta contrassegnare le proprietà da iniettare e Nette DI passerà automaticamente le dipendenze subito dopo aver creato l'istanza del presenter. Perché l'iniezione sia possibile, queste proprietà devono essere dichiarate pubbliche. - -Le proprietà si contrassegnano con un attributo: (in passato si usava l'annotazione `/** @inject */`) - -```php -use Nette\DI\Attributes\Inject; // questa riga è importante - -class MyPresenter extends Nette\Application\UI\Presenter -{ - #[Inject] - public Cache $cache; -} -``` - -Il vantaggio di questo modo di passare le dipendenze era la sintassi molto concisa. Con l'introduzione della [constructor property promotion |https://blog.nette.org/en/php-8-0-complete-overview-of-news#toc-constructor-property-promotion], però, usare il costruttore appare spesso più semplice. - -Al contrario, questo metodo soffre degli stessi difetti dell'iniezione nelle proprietà in generale: ci manca il controllo sulle modifiche della variabile e la variabile entra a far parte dell'interfaccia pubblica della classe, cosa in generale indesiderabile. diff --git a/best-practices/it/lets-create-contact-form.texy b/best-practices/it/lets-create-contact-form.texy deleted file mode 100644 index 86252327f0..0000000000 --- a/best-practices/it/lets-create-contact-form.texy +++ /dev/null @@ -1,218 +0,0 @@ -Creiamo un form di contatto -*************************** - -.[perex] -Vediamo come creare in Nette un form di contatto, compreso l'invio per email dei dati inseriti. Cominciamo! - -Per prima cosa dobbiamo creare un nuovo progetto. Come farlo lo spiega la pagina [Per iniziare |nette:installation]. Poi possiamo passare alla creazione del form. - -Il modo più semplice è creare il [form direttamente nel presenter |forms:in-presenter]. Possiamo sfruttare l'`HomePresenter` già pronto. Vi aggiungeremo il componente `contactForm`, che rappresenta il nostro form. Lo facciamo aggiungendo al codice del presenter il metodo factory `createComponentContactForm()`, che creerà il componente: - -```php -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - protected function createComponentContactForm(): Form - { - $form = new Form; - $form->addText('name', 'Nome:') - ->setRequired('Inserite il vostro nome'); - $form->addEmail('email', 'E-mail:') - ->setRequired('Inserite la vostra email'); - $form->addTextArea('message', 'Messaggio:') - ->setRequired('Inserite un messaggio'); - $form->addSubmit('send', 'Invia'); - $form->onSuccess[] = $this->contactFormSucceeded(...); - return $form; - } - - private function contactFormSucceeded(Form $form, $data): void - { - // invio dell'email - } -} -``` - -Come vedete abbiamo creato due metodi. Il primo, `createComponentContactForm()`, crea una nuova istanza del form. Contiene i campi per il nome, l'email e il messaggio, aggiunti rispettivamente con i metodi `addText()`, `addEmail()` e `addTextArea()`. Abbiamo aggiunto anche il pulsante di invio. E se l'utente lasciasse un campo vuoto? In quel caso dovremmo avvertirlo che il campo è obbligatorio. L'abbiamo ottenuto con il metodo `setRequired()`. Infine abbiamo agganciato a `onSuccess` un gestore di [evento |nette:glossary#Eventi], che si attiva quando il form viene inviato con successo. Nel nostro caso chiama il metodo `contactFormSucceeded`, che si occuperà di elaborare i dati inseriti. Lo scriveremo tra un attimo. - -Rendiamo il componente `contactForm` nel template `Home/default.latte`: - -```latte -{block content} -<h1>Form di contatto</h1> -{control contactForm} -``` - -Per l'invio vero e proprio dell'email creeremo una nuova classe chiamata `ContactFacade` e la metteremo nel file `app/Model/ContactFacade.php`: - -```php -namespace App\Model; - -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $mail = new Message; - $mail->addTo('admin@example.com') // la vostra email - ->setFrom($email, $name) - ->setSubject('Messaggio dal form di contatto') - ->setBody($message); - - $this->mailer->send($mail); - } -} -``` - -Il metodo `sendMessage()` crea e invia l'email. Per farlo usa un cosiddetto mailer, che riceve come dipendenza tramite il costruttore. Leggete di più sull'[invio delle email |mail:]. - -Torniamo ora al presenter e completiamo il metodo `contactFormSucceeded()`. Chiamerà il metodo `sendMessage()` della classe `ContactFacade` passandogli i dati inseriti nel form. E come otteniamo l'oggetto `ContactFacade`? Ce lo faremo passare tramite il costruttore usando la dependency injection: - -```php -use App\Model\ContactFacade; -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - public function __construct( - private ContactFacade $facade, - ) { - } - - protected function createComponentContactForm(): Form - { - // ... - } - - public function contactFormSucceeded(stdClass $data): void - { - $this->facade->sendMessage($data->email, $data->name, $data->message); - $this->flashMessage('Il messaggio è stato inviato'); - $this->redirect('this'); - } -} -``` - -Dopo l'invio dell'email mostriamo all'utente un [messaggio flash |application:components#Messaggi flash] che conferma l'avvenuto invio. Poi reindirizziamo, perché il form non possa essere reinviato aggiornando la pagina nel browser. - - -Se tutto è impostato correttamente, dovreste ora riuscire a inviare un'email dal vostro form di contatto. Congratulazioni! - - -Template HTML dell'email ------------------------- - -Per ora viene inviata una email in testo semplice che contiene solo il messaggio inserito nel form. Nell'email possiamo però usare l'HTML e renderne l'aspetto più gradevole. Creeremo il template in Latte e lo salveremo come `app/Model/contactEmail.latte`: - -```latte -<html> - <title>Messaggio dal form di contatto - - -

    Nome: {$name}

    -

    E-mail: {$email}

    -

    Messaggio: {$message}

    - - -``` - -Resta da modificare `ContactFacade` perché usi questo template. Nel costruttore ci faremo passare la classe `LatteFactory`, che sa creare l'oggetto `Latte\Engine`, cioè il [renderer dei template Latte |latte:develop#Come fare il rendering di un template]. Con il metodo `renderToString()` renderizziamo il template in una stringa. Il primo parametro è il percorso del file del template, il secondo un array di variabili da passargli. - -```php -namespace App\Model; - -use Nette\Bridges\ApplicationLatte\LatteFactory; -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $latte = $this->latteFactory->create(); - $body = $latte->renderToString(__DIR__ . '/contactEmail.latte', [ - 'email' => $email, - 'name' => $name, - 'message' => $message, - ]); - - $mail = new Message; - $mail->addTo('admin@example.com') // la vostra email - ->setFrom($email, $name) - ->setHtmlBody($body); - - $this->mailer->send($mail); - } -} -``` - -Il contenuto HTML dell'email generato lo passiamo poi al metodo `setHtmlBody()` invece dell'originale `setBody()`. Non dobbiamo nemmeno indicare l'oggetto dell'email con `setSubject()`, perché la libreria lo ricava automaticamente dall'elemento `` del template. - - -Configurazione --------------- - -Nel codice della classe `ContactFacade` è ancora scritta fissa la nostra email di amministrazione `admin@example.com`. Sarebbe meglio spostarla nel file di configurazione. Come si fa? - -Per prima cosa modifichiamo la classe `ContactFacade` sostituendo la stringa con l'email con una variabile passata dal costruttore: - -```php -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - private string $adminEmail, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - // ... - $mail = new Message; - $mail->addTo($this->adminEmail) - ->setFrom($email, $name) - ->setHtmlBody($body); - // ... - } -} -``` - -Il secondo passo è indicare il valore di questa variabile nella configurazione. Nel file `app/config/services.neon` aggiungiamo: - -```neon -services: - - App\Model\ContactFacade(adminEmail: admin@example.com) -``` - -E questo è tutto. Se nella sezione `services` ci fossero molte voci e vi sembrasse che l'indirizzo email vi si perde, possiamo trasformarlo in un parametro. Modifichiamo la voce così: - -```neon -services: - - App\Model\ContactFacade(adminEmail: %adminEmail%) -``` - -E nel file `app/config/common.neon` definiamo questo parametro: - -```neon -parameters: - adminEmail: admin@example.com -``` - -Ed è fatta! diff --git a/best-practices/it/microsites.texy b/best-practices/it/microsites.texy deleted file mode 100644 index 8c09b66602..0000000000 --- a/best-practices/it/microsites.texy +++ /dev/null @@ -1,63 +0,0 @@ -Come scrivere microsite -*********************** - -Immaginate di dover creare in fretta un piccolo sito per un evento aziendale in arrivo. Deve essere semplice, veloce e senza complicazioni inutili. Potreste pensare che per un progetto così piccolo un framework robusto non serva. Ma se usare il Nette Framework potesse in realtà semplificare e accelerare il lavoro? - -Anche quando create siti semplici non volete rinunciare alla comodità. Non volete reinventare ciò che è già stato risolto. Siate pure pigri e lasciatevi coccolare. Il Nette Framework è eccellente anche come micro-framework. - -Come può apparire un microsite del genere? Per esempio, tutto il codice del sito può stare in un unico file `index.php` nella directory pubblica: - -```php -<?php - -require __DIR__ . '/../vendor/autoload.php'; - -$configurator = new Nette\Bootstrap\Configurator; -$configurator->enableTracy(__DIR__ . '/../log'); -$configurator->setTempDirectory(__DIR__ . '/../temp'); - -// creiamo il container DI in base alla configurazione in config.neon -$configurator->addConfig(__DIR__ . '/../app/config.neon'); -$container = $configurator->createContainer(); - -// impostiamo il routing -$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) { - // rileviamo la lingua del browser e reindirizziamo all'URL /en oppure /de ecc. - $supportedLangs = ['en', 'de', 'cs']; - $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); - return $presenter->redirectUrl("/$lang"); -}); - -// rotta per l'URL https://example.com/cs oppure https://example.com/en -$router->addRoute('<lang cs|en|de>', function ($presenter, string $lang) { - // mostriamo il template corrispondente, per esempio ../templates/en.latte - $template = $presenter->createTemplate() - ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); - return $template; -}); - -// avviamo l'applicazione! -$container->getByType(Nette\Application\Application::class)->run(); -``` - -Tutto il resto saranno template salvati nella directory superiore `/templates`. - -Il codice PHP in `index.php` prima [prepara l'ambiente |bootstrap:], poi definisce le [rotte |application:routing#Routing dinamico con le callback] e infine avvia l'applicazione. Il vantaggio è che il secondo parametro della funzione `addRoute()` può essere un callable, che viene eseguito quando si accede alla pagina corrispondente. - - -Perché usare Nette per i microsite? ------------------------------------ - -- I programmatori che hanno provato [Tracy|tracy:] oggi non sanno più immaginare di programmare senza. -- Soprattutto sfrutterete il sistema di template [Latte|latte:], perché già con due pagine vorrete separare [layout e contenuto|latte:template-inheritance]. -- E di sicuro volete contare sull'[escaping automatico |latte:safety-first] per evitare vulnerabilità XSS. -- Nette garantisce anche che, in caso di errore, non vengano mai mostrati i messaggi di errore grezzi di PHP, ma una pagina comprensibile per l'utente. -- Se volete raccogliere le reazioni degli utenti, per esempio con un form di contatto, potete aggiungere facilmente il supporto per i [form|forms:] e il [database|database:]. -- Potete anche far [inviare per email|mail:] i form compilati, senza sforzo. -- A volte può tornare utile la [cache|caching:], per esempio quando scaricate e mostrate dei feed. - -Nel mondo di oggi, dove velocità ed efficienza sono decisive, è essenziale avere strumenti che permettano di ottenere risultati senza ritardi inutili. Il Nette Framework offre esattamente questo: sviluppo rapido, sicurezza e un'ampia gamma di strumenti come Tracy e Latte che semplificano il lavoro. Basta installare qualche pacchetto Nette e costruire un microsite del genere diventa incredibilmente facile. E potete stare tranquilli che non ci sono vulnerabilità di sicurezza nascoste. diff --git a/best-practices/it/pagination.texy b/best-practices/it/pagination.texy deleted file mode 100644 index 2edc1daf9d..0000000000 --- a/best-practices/it/pagination.texy +++ /dev/null @@ -1,273 +0,0 @@ -Paginare i risultati del database -********************************* - -.[perex] -Sviluppando applicazioni web incontrerete spesso l'esigenza di limitare il numero di elementi elencati per pagina, tecnica nota come paginazione. - -Partiamo dalla situazione in cui elenchiamo tutti i dati senza paginazione. Per selezionare i dati dal database abbiamo la classe `ArticleRepository`. Oltre al costruttore contiene il metodo `findPublishedArticles`, che restituisce tutti gli articoli pubblicati ordinati per data di pubblicazione decrescente. - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC', - new \DateTime, - ); - } -} -``` - -Nel presenter ci facciamo poi iniettare questa classe di modello. Nel metodo render otteniamo gli articoli pubblicati e li passiamo al template: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(): void - { - $this->template->articles = $this->articleRepository->findPublishedArticles(); - } -} -``` - -Il template `default.latte` si occuperà poi di elencare gli articoli: - -```latte -{block content} -<h1>Articoli</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> -``` - - -In questo modo sappiamo elencare tutti gli articoli, ma la cosa diventa problematica quando il loro numero cresce. A quel punto torna utile realizzare un meccanismo di paginazione. - -Questo meccanismo divide tutti gli articoli in più pagine e mostra solo gli articoli della pagina attualmente selezionata. Il numero complessivo di pagine e la ripartizione degli articoli vengono calcolati dallo strumento [Paginator |utils:Paginator] in base al numero totale di articoli e al numero desiderato di articoli per pagina. - -Nel primo passo modifichiamo nella classe repository il metodo che ottiene gli articoli, in modo che possa restituire gli articoli di una sola pagina. Aggiungiamo anche un metodo per ottenere il numero totale di articoli nel database, necessario per configurare il Paginator: - -```php -namespace App\Model; - -use Nette; - - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(int $limit, int $offset): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC - LIMIT ? - OFFSET ?', - new \DateTime, $limit, $offset, - ); - } - - /** - * Restituisce il numero totale di articoli pubblicati - */ - public function getPublishedArticlesCount(): int - { - return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime); - } -} -``` - -Passiamo poi a modificare il presenter. Al metodo `renderDefault` passeremo il numero della pagina corrente. Se questo numero non fa parte dell'URL, imposteremo il valore predefinito 1 (la prima pagina). - -Estenderemo inoltre il metodo render per creare e configurare un'istanza del Paginator e selezionare gli articoli giusti da mostrare nel template. L'`HomePresenter` modificato apparirà così: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // otteniamo il numero totale di articoli pubblicati - $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - - // creiamo e configuriamo l'istanza del Paginator - $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // numero totale di elementi - $paginator->setItemsPerPage(10); // elementi per pagina - $paginator->setPage($page); // numero della pagina corrente - - // carichiamo dal database un insieme limitato di articoli secondo il calcolo del Paginator - $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - - // li passiamo al template - $this->template->articles = $articles; - // e con essi anche il Paginator stesso, per mostrare i controlli di paginazione - $this->template->paginator = $paginator; - } -} -``` - -Il template ora itera solo sugli articoli della pagina corrente. Basta aggiungere i link di paginazione: - -```latte -{block content} -<h1>Articoli</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if !$paginator->isFirst()} - <a n:href="default, 1">Prima</a> -  |  - <a n:href="default, $paginator->getPage() - 1">Precedente</a> -  |  - {/if} - - Pagina {$paginator->getPage()} di {$paginator->getPageCount()} - - {if !$paginator->isLast()} -  |  - <a n:href="default, $paginator->getPage() + 1">Successiva</a> -  |  - <a n:href="default, $paginator->getPageCount()">Ultima</a> - {/if} -</div> -``` - - -Così abbiamo completato la paginazione con l'aiuto del Paginator. Se come livello database usate [Nette Database Explorer |database:explorer] invece di [Nette Database Core |database:sql-way], potete realizzare la paginazione anche senza usare direttamente lo strumento Paginator. La classe `Nette\Database\Table\Selection` contiene infatti il metodo [page() |api:Nette\Database\Table\Selection::page()], che racchiude la logica di paginazione. - -Con questo approccio il repository apparirà così: - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Explorer $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\Table\Selection - { - return $this->database->table('articles') - ->where('created_at < ', new \DateTime) - ->order('created_at DESC'); - } -} -``` - -Nel presenter non dobbiamo creare l'istanza del Paginator. Al suo posto useremo il metodo `page()` dell'oggetto `Selection` restituito dal repository: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // otteniamo gli articoli pubblicati - $articles = $this->articleRepository->findPublishedArticles(); - - // e al template passiamo solo la loro parte limitata dal calcolo del metodo page - $lastPage = 0; - $this->template->articles = $articles->page($page, 10, $lastPage); - - // e con essa anche i dati necessari a mostrare i controlli di paginazione - $this->template->page = $page; - $this->template->lastPage = $lastPage; - } -} -``` - -Poiché al template non passiamo più l'oggetto Paginator, dobbiamo modificare la parte che mostra i link di paginazione: - -```latte -{block content} -<h1>Articoli</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if $page > 1} - <a n:href="default, 1">Prima</a> -  |  - <a n:href="default, $page - 1">Precedente</a> -  |  - {/if} - - Pagina {$page} di {$lastPage} - - {if $page < $lastPage} -  |  - <a n:href="default, $page + 1">Successiva</a> -  |  - <a n:href="default, $lastPage">Ultima</a> - {/if} -</div> -``` - -Abbiamo così realizzato il meccanismo di paginazione senza usare esplicitamente lo strumento Paginator. - -{{priority: -1}} diff --git a/best-practices/it/passing-settings-to-presenters.texy b/best-practices/it/passing-settings-to-presenters.texy deleted file mode 100644 index e49d139737..0000000000 --- a/best-practices/it/passing-settings-to-presenters.texy +++ /dev/null @@ -1,49 +0,0 @@ -Passare impostazioni ai presenter -********************************* - -.[perex] -Dovete passare ai presenter argomenti non oggetto (come un flag che indica la modalità debug, dei percorsi di directory ecc.) che non si possono passare automaticamente tramite autowiring? La soluzione è racchiuderli in un apposito oggetto `Settings`. - -Il servizio `Settings` offre un modo molto semplice ma efficace di fornire ai presenter informazioni sull'applicazione in esecuzione. La sua struttura concreta dipende interamente dalle vostre esigenze. Esempio: - -```php -namespace App; - -class Settings -{ - public function __construct( - // da PHP 8.1 si può usare readonly - public bool $debugMode, - public string $appDir, - // e così via - ) {} -} -``` - -Esempio di registrazione nella configurazione: - -```neon -services: - - App\Settings( - %debugMode%, - %appDir%, - ) -``` - -Quando un presenter ha bisogno delle informazioni fornite da questo servizio, si limita a chiederlo nel proprio costruttore: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private App\Settings $settings, - ) {} - - public function renderDefault() - { - if ($this->settings->debugMode) { - // ... - } - } -} -``` diff --git a/best-practices/it/post-links.texy b/best-practices/it/post-links.texy deleted file mode 100644 index 875151fe53..0000000000 --- a/best-practices/it/post-links.texy +++ /dev/null @@ -1,56 +0,0 @@ -Come usare correttamente i link POST -************************************ - -.[perex] -Nelle applicazioni web, in particolare nelle interfacce di amministrazione, dovrebbe valere una regola fondamentale: le azioni che modificano lo stato del server non si eseguono con il metodo HTTP GET. Come dice il nome, GET dovrebbe servire solo a ottenere dati, non a modificarli. Per azioni come l'eliminazione di record è più appropriato usare il metodo POST. Il metodo DELETE sarebbe ideale, ma non si può richiamare senza JavaScript, ed è per questo che storicamente si è usato POST per azioni del genere. - -Come realizzarlo in pratica? Usate questo semplice trucco. All'inizio del vostro template di layout create un form di supporto con l'ID `postForm`. Userete poi questo form per azioni come i pulsanti di eliminazione: - -```latte .{file:@layout.latte} -<form method="post" id="postForm"></form> -``` - -Grazie a questo form, al posto di un normale link `<a>` potete usare un `<button>`. Questo pulsante si può stilizzare perché sembri un normale link. Il framework CSS Bootstrap, per esempio, offre le classi `btn btn-link`, che rendono il pulsante visivamente indistinguibile dagli altri link. Con l'attributo `form="postForm"` collegate il pulsante al form di supporto preparato: - -```latte .{file:admin.latte} -<table> - <tr n:foreach="$posts as $post"> - <td>{$post->title}</td> - <td> - <button class="btn btn-link" form="postForm" formaction="{link delete, $post->id}">elimina</button> - <!-- invece di <a n:href="delete, $post->id">elimina</a> --> - </td> - </tr> -</table> -``` - -Cliccando questo pulsante viene ora richiamata l'azione `delete`. Perché le richieste vengano accettate solo con il metodo POST e provengano dallo stesso dominio (una difesa efficace contro gli attacchi CSRF), usate l'attributo `#[Requires]`: - -```php .{file:AdminPresenter.php} -use Nette\Application\Attributes\Requires; - -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST', sameOrigin: true)] - public function actionDelete(int $id): void - { - $this->facade->deletePost($id); // codice ipotetico per eliminare un record - $this->redirect('default'); - } -} -``` - -Questo attributo è disponibile da Nette Application 3.2. Potete saperne di più sulle sue possibilità nella pagina [Come usare l'attributo #Requires |attribute-requires]. - -Se invece dell'azione `actionDelete()` usaste il segnale `handleDelete()`, non è necessario indicare `sameOrigin: true`, perché i segnali hanno questa protezione attiva per impostazione predefinita: - -```php .{file:AdminPresenter.php} -#[Requires(methods: 'POST')] -public function handleDelete(int $id): void -{ - $this->facade->deletePost($id); - $this->redirect('this'); -} -``` - -Questo approccio non solo aumenta la sicurezza della vostra applicazione, ma favorisce anche il rispetto dei corretti standard e delle buone pratiche del web. Usare i metodi POST per le azioni che cambiano lo stato porta a un'applicazione più solida e più sicura. diff --git a/best-practices/it/presenter-traits.texy b/best-practices/it/presenter-traits.texy deleted file mode 100644 index e301aadf4b..0000000000 --- a/best-practices/it/presenter-traits.texy +++ /dev/null @@ -1,47 +0,0 @@ -Comporre i presenter con i trait -******************************** - -.[perex] -Se dovete implementare la stessa funzionalità in più presenter (per esempio la verifica del login dell'utente), un approccio comune è collocare il codice in un antenato comune. Un'altra possibilità è creare [trait |nette:introduction-to-object-oriented-programming#Trait] monouso. - -Il vantaggio dei trait è che ogni presenter può incorporare solo i trait di cui ha davvero bisogno, tanto più che in PHP l'ereditarietà multipla non è supportata. - -Questi trait possono sfruttare il fatto che tutti i [metodi inject |inject-method-attribute#Metodi inject*()] vengono chiamati in sequenza al momento della creazione dell'istanza del presenter. Dovete solo assicurarvi che il nome di ogni metodo inject sia univoco tra tutti i trait usati e il presenter stesso. - -I trait possono agganciare del codice di inizializzazione agli eventi [onStartup o onRender |application:presenters#Eventi]. - -Esempi: - -```php -trait RequireLoggedUser -{ - public function injectRequireLoggedUser(): void - { - $this->onStartup[] = function () { - if (!$this->getUser()->isLoggedIn()) { - $this->redirect('Sign:in', $this->storeRequest()); - } - }; - } -} - -trait StandardTemplateFilters -{ - public function injectStandardTemplateFilters(TemplateBuilder $builder): void - { - $this->onRender[] = function () use ($builder) { - $builder->setupTemplate($this->template); - }; - } -} -``` - -Il presenter poi si limita a usare questi trait: - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - use StandardTemplateFilters; - use RequireLoggedUser; -} -``` diff --git a/best-practices/it/pretty-urls.texy b/best-practices/it/pretty-urls.texy deleted file mode 100644 index abee2b4ac7..0000000000 --- a/best-practices/it/pretty-urls.texy +++ /dev/null @@ -1,204 +0,0 @@ -URL leggibili con gli slug -************************** - -.[perex] -Gli URL come `/article/123-how-to-bake-bread` sono più belli di `/article/123` e aiutano sia gli utenti sia i motori di ricerca a capire cosa c'è nella pagina. Questa guida mostra come generarli interamente nel router, senza toccare un solo template, e come assicurarsi che ogni visitatore arrivi all'URL canonico. - - -Perché gli slug negli URL -========================= - -Confrontate questi due indirizzi: - -``` -/article/123 -/article/123-how-to-bake-bread -``` - -Il secondo dice all'utente (e a Google) cosa lo aspetta dopo il clic. Fa bene alla SEO, rende i link leggibili in chat o via e-mail e dà un senso alla barra degli indirizzi. - -Lo slug non è però un vero identificatore. La pagina è determinata dall'ID. Lo slug è una decorazione che l'applicazione genera dal titolo. Se il titolo cambia, dovrebbe cambiare anche lo slug. E se qualcuno modifica l'URL a mano o segue un vecchio link, l'applicazione dovrebbe comunque trovare la pagina giusta. - - -L'obiettivo -=========== - -Vogliamo una route che gestisca tutti questi casi: - -``` -/article/123 → apre l'articolo 123, reindirizza all'URL canonico -/article/123-how-to-bake-bread → apre direttamente l'articolo 123 -/article/123-anything-someone-typed → apre l'articolo 123, reindirizza all'URL canonico -/article/ → 404 (nessun ID) -``` - -E vogliamo che ogni chiamata a `n:href` e a `link()` in tutta l'applicazione produca automaticamente `/article/123-how-to-bake-bread`, **senza riscrivere un solo template**. - - -La maschera della route -======================= - -Il trucco è contrassegnare lo slug come **facoltativo** nella maschera, con le parentesi quadre: - -```php -$router->addRoute('article/<id [0-9]+>[-<slug>]', 'Article:detail'); -``` - -La maschera `[-<slug>]` dice: dopo l'ID può esserci un trattino e uno slug, ma non è obbligatorio. La route accetta sia `/article/123` sia `/article/123-anything`. - -Una nota sul parametro `<slug>`: per impostazione predefinita corrisponde a qualsiasi carattere **tranne la barra**, esattamente ciò che vogliamo. Se scrivete `<slug .+>`, il parametro corrisponderà anche alle barre, quindi `/article/123-something/else` verrebbe analizzato come un unico slug contenente `/`. Restate al `<slug>` predefinito, a meno che non vi serva davvero. - -Finora l'URL viene analizzato correttamente, ma i link generati non conterranno lo slug. Il passo successivo è insegnare alla route come riempirlo. - - -Generare lo slug senza toccare i template -========================================= - -È questa la variante decisiva. Le chiamate `n:href="Article:detail, $id"` esistenti continuano a funzionare invariate in tutta l'applicazione: il router cerca il titolo da sé. - -Lo facciamo con un **filtro generale** sotto la chiave stringa vuota: vede tutti i parametri insieme e può aggiungere lo slug: - -```php -use Nette\Routing\Route; -use Nette\Utils\Strings; - -$router->addRoute('article/<id [0-9]+>[-<slug>]', [ - 'presenter' => 'Article', - 'action' => 'detail', - '' => [ - Route::FilterOut => function (array $params) use ($slugProvider): array { - if (isset($params['id']) && empty($params['slug'])) { - $params['slug'] = $slugProvider->getSlug((int) $params['id']); - } - return $params; - }, - ], -]); -``` - -`FilterOut` viene eseguito ogni volta che il router **genera** un URL. Se lo slug non è stato passato, il filtro cerca il titolo e lo aggiunge. - -Potete introdurre gli slug in tutta un'applicazione con un'unica modifica: una sola definizione di route. Ogni link di ogni template comincia a produrre automaticamente `/article/123-how-to-bake-bread`. Nessun grep, nessuna caccia nei template, nessun caso limite dimenticato. - - -Mettete in cache la ricerca -=========================== - -Un link genera una query al database, ma una pagina tipica ne ha molti: elenchi, breadcrumb, "visti di recente", articoli correlati. Lo stesso ID di articolo compare spesso in più link durante una singola richiesta, e non volete interrogare il database ogni volta. - -Una piccola cache per richiesta risolve il problema. Racchiudete la chiamata al database in un piccolo servizio: - -```php -final class SlugProvider -{ - /** @var array<int, string> */ - private array $cache = []; - - public function __construct( - private Nette\Database\Explorer $db, - ) { - } - - public function getSlug(int $id): string - { - return $this->cache[$id] ??= Strings::webalize(Strings::truncate( - (string) $this->db->fetchField('SELECT title FROM article WHERE id = ?', $id), - 100, '' - )); - } -} -``` - -Basta questo: una sola query al database per ogni ID univoco per richiesta. - - -Passare il titolo dal template (percorso rapido facoltativo) -============================================================ - -Quando il titolo è già a portata di mano nel template, potete saltare del tutto la ricerca nel database. Passate il titolo come parametro nominale: - -```latte -<a n:href="Article:detail, $article->id, slug => $article->title">{$article->title}</a> -``` - -…e aggiungete un `FilterOut` sul singolo parametro, che trasformi il titolo in una stringa adatta all'URL: - -```php -$router->addRoute('article/<id [0-9]+>[-<slug>]', [ - 'presenter' => 'Article', - 'action' => 'detail', - 'slug' => [ - Route::FilterOut => fn($title) => Strings::webalize(Strings::truncate($title, 100, '')), - ], - '' => [/* il ripiego con la ricerca visto sopra */], -]); -``` - -I due filtri collaborano. Il filtro generale viene eseguito per primo e, vedendo lo slug già riempito con il titolo fornito, salta la ricerca nel database. Il `FilterOut` del singolo parametro trasforma poi quel titolo in uno slug vero e proprio. I template che non passano il titolo continuano a funzionare: il filtro generale trova lo slug vuoto e percorre la via della ricerca. - -Usate questo approccio solo dove conta (grandi elenchi disegnati centinaia di volte per richiesta). Per la maggior parte dell'applicazione la ricerca con cache è abbastanza veloce. - - -Canonizzazione: reindirizzare all'URL giusto -============================================ - -Ora sappiamo generare `/article/123-how-to-bake-bread`, ma la route accetta ancora `/article/123` e `/article/123-anything-someone-wrote`. È voluto: vogliamo URL brevi (ne parliamo tra poco) e vogliamo che i link vecchi o scritti a mano continuino a funzionare. Non vogliamo però che i motori di ricerca indicizzino lo stesso articolo sotto più indirizzi. - -La soluzione è la [canonizzazione |application:presenters#Canonizzazione]: quando l'utente arriva tramite un URL non canonico, l'applicazione lo reindirizza con un 301 a quello corretto. Se ne occupa il metodo `canonicalize()`: - -```php -public function actionDetail(int $id, ?string $slug = null): void -{ - $article = $this->facade->getArticle($id); - if (!$article) { - $this->error(); - } - - // genera l'URL canonico attraverso lo stesso FilterOut - // e reindirizza con HTTP 301 se differisce dall'URL corrente - $this->canonicalize('detail', ['id' => $id]); - - $this->template->article = $article; -} -``` - -`canonicalize()` genera l'URL canonico nello stesso modo di `link()` (quindi passa per lo stesso `FilterOut`) e lo confronta con l'URL corrente. Se differiscono, reindirizza con HTTP 301. I visitatori arrivano all'URL giusto, i motori di ricerca vedono una sola versione canonica. - - -Un unico punto che decide l'aspetto dello slug -============================================== - -Notate che la chiamata `Strings::webalize(Strings::truncate(..., 100, ''))` vive in un unico punto, dentro `SlugProvider` (oppure nel `FilterOut` del singolo parametro). La stessa logica produce il link nel template, l'URL in `redirect()` e la forma canonica in `canonicalize()`. - -Se in seguito volete cambiare le regole (un limite di lunghezza diverso, una traslitterazione diversa, la rimozione di altri caratteri), cambiate una riga. Senza questo rischiereste che `redirect()` generi `/article/123-how-to-bake-bread` mentre `canonicalize()` si aspetta `/article/123-how-to-bake-bre` (perché altrove qualcuno ha usato un `truncate` di lunghezza diversa), e l'applicazione reindirizzerebbe in un ciclo infinito. - - -Bonus: gli URL brevi continuano a funzionare -============================================ - -Poiché lo slug è facoltativo, gli indirizzi che ne sono privi continuano a funzionare: - -``` -/article/123 -``` - -È utile per: -- **i codici QR**: un URL più breve significa un codice meno denso e più facile da scansionare -- **gli SMS e le chat**: sta in un tweet e ha un aspetto ordinato -- **i materiali stampati**: un URL breve si digita più in fretta - -Quando un utente apre un URL del genere, `canonicalize()` lo reindirizza con un 301 alla versione completa con lo slug, così i motori di ricerca vedono comunque solo la forma canonica. Potete avere insieme brevità e SEO. - - -Riepilogo -========= - -- La maschera `<id>[-<slug>]` rende lo slug facoltativo. Il `<slug>` predefinito non corrisponde a `/`; usate `<slug .+>` solo se volete davvero le barre nello slug. -- Un `FilterOut` generale sotto la chiave `''` cerca il titolo a partire dall'ID: **nessuna modifica ai template in tutta l'applicazione**. -- Racchiudete la ricerca in una piccola cache per richiesta; una query al database per ogni ID univoco basta e avanza. -- Facoltativamente, un `FilterOut` sul singolo parametro permette ai template di passare direttamente il titolo e di saltare la ricerca. -- `$this->canonicalize()` nell'azione reindirizza gli URL non canonici a quello giusto, con HTTP 301. -- La formula dello slug (`webalize` + `truncate`) vive in un unico punto: cambiatela una volta e ha effetto ovunque. -- Gli URL brevi con il solo ID continuano a funzionare, il che torna comodo per i codici QR e gli SMS. - -Trovate maggiori informazioni sui filtri e sulla canonizzazione nella documentazione del [routing |application:routing#Filtri generali] e dei [presenter |application:presenters#Canonizzazione]. diff --git a/best-practices/it/restore-request.texy b/best-practices/it/restore-request.texy deleted file mode 100644 index 22e4afab3e..0000000000 --- a/best-practices/it/restore-request.texy +++ /dev/null @@ -1,62 +0,0 @@ -Come tornare a una pagina precedente? -************************************* - -.[perex] -Cosa succede se un utente sta compilando un form e la sua sessione di login scade? Per non perdere i dati, possiamo salvare nella sessione la richiesta corrente (dati del form compresi) prima di reindirizzare alla pagina di login. In Nette è sorprendentemente semplice. - -La richiesta corrente si può salvare nella sessione con il metodo `storeRequest()`. Questo metodo restituisce un identificatore univoco (una stringa breve) della richiesta salvata. Il metodo salva il nome del presenter corrente, la sua vista e i suoi parametri. Se nell'ambito della richiesta è stato inviato un form, vengono salvati anche i valori inseriti nei campi (esclusi i file caricati). - -La richiesta si ripristina con il metodo `restoreRequest($key)`, al quale passate l'identificatore ottenuto in precedenza. Questo metodo reindirizza l'utente al presenter e alla vista originali. Se però la richiesta salvata comprendeva l'invio di un form, `restoreRequest()` usa il metodo `forward()` invece di reindirizzare. Ripassa al form i valori compilati in precedenza e ne permette il rendering. L'utente può così reinviare il form senza perdere i dati inseriti. - -Cosa essenziale, `restoreRequest()` verifica che l'utente appena connesso sia lo stesso che aveva inviato il form. Se l'utente è diverso, la richiesta salvata non viene ripristinata e il metodo non fa nulla, il che aumenta la sicurezza. - -Illustriamolo con un esempio. Consideriamo un `AdminPresenter` in cui si modificano dei dati. Il suo metodo `startup()` verifica se l'utente è connesso. In caso contrario l'utente viene reindirizzato a `SignPresenter`. Allo stesso tempo salviamo la richiesta corrente con `storeRequest()` e ne passiamo la chiave (il `$backlink`) a `SignPresenter`. - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - protected function startup() - { - parent::startup(); - - if (!$this->user->isLoggedIn()) { - $this->redirect('Sign:in', ['backlink' => $this->storeRequest()]); - } - } -} -``` - -`SignPresenter` conterrà, oltre al form di login, un parametro persistente `$backlink` in cui è salvata la chiave. Poiché il parametro è persistente, il suo valore si conserva anche dopo l'invio del form di login. - - -```php -use Nette\Application\Attributes\Persistent; - -class SignPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $backlink = ''; - - protected function createComponentSignInForm() - { - $form = new Nette\Application\UI\Form; - // ... aggiunta dei campi del form ... - $form->onSuccess[] = $this->signInFormSuceeded(...); - return $form; - } - - private function signInFormSuceeded($form) - { - // ... qui l'utente viene connesso ... - - $this->restoreRequest($this->backlink); - $this->redirect('Admin:'); - } -} -``` - -Passiamo la chiave (`$this->backlink`) della richiesta salvata al metodo `restoreRequest()`. Esso reindirizza (o inoltra) poi l'utente al presenter e alla vista originali. - -Se però la chiave non è valida (per esempio è scaduta dalla sessione), il metodo non fa nulla. La chiamata successiva `$this->redirect('Admin:')` funge quindi da ripiego e reindirizza a una pagina predefinita, come `AdminPresenter`. - -{{priority: -1}} diff --git a/best-practices/ja/@home.texy b/best-practices/ja/@home.texy deleted file mode 100644 index 3365044dcf..0000000000 --- a/best-practices/ja/@home.texy +++ /dev/null @@ -1,69 +0,0 @@ -チュートリアルとベストプラクティス -***************** - -.[perex] -チュートリアル、よくある課題の解き方、そして Nette のベストプラクティス。 - - -<div class=documentation> -<div> - - -Nette Application ------------------ -- [inject メソッドとアトリビュート |inject-method-attribute] -- [トレイトによるプレゼンターの構成 |presenter-traits] -- [プレゼンターへの設定の受け渡し |passing-settings-to-presenters] -- [リクエストを復元する方法 |restore-request] -- [データベース結果のページネーション |pagination] -- [動的スニペット |dynamic-snippets] -- [#Requires アトリビュートの使い方 |attribute-requires] -- [POST リンクの正しい使い方 |post-links] -- [スラッグを使った読みやすい URL |pretty-urls] - -</div> -<div> - - -フォーム ----- -- [フォームの再利用 |form-reuse] -- [レコードの作成と編集のためのフォーム |creating-editing-form] -- [お問い合わせフォームを作ろう |lets-create-contact-form] -- [連動するセレクトボックス |https://blog.nette.org/en/dependent-selectboxes-elegantly-in-nette-and-pure-js] - -</div> -<div> - - -一般 ---- -- [設定ファイルの読み込み方 |bootstrap:] -- [マイクロサイトの書き方 |microsites] -- [なぜ Nette は定数に PascalCase を使うのか? |https://blog.nette.org/en/for-less-screaming-in-the-code] -- [なぜ Nette は Interface の接尾辞を使わないのか? |https://blog.nette.org/en/prefixes-and-suffixes-do-not-belong-in-interface-names] -- [Composer: 利用のヒント |composer] -- [オブジェクト指向プログラミング入門 |nette:introduction-to-object-oriented-programming] - -</div> -<div> - - -解決例 ---- -- [Nette examples |https://github.com/nette-examples] -- [Doctrine & Nette |https://contributte.org/nettrine/] -- [Contributte examples |https://contributte.org/examples.html] -- [Doctrine ORM Website |https://github.com/MinecordNetwork/Website] -- [クイックスタート |quickstart:] - -</div> -<div> - - -動画 ---- -Last Saturday のミートアップの録画や Nette についての動画は、"Nette Framework の YouTube チャンネル":https://www.youtube.com/user/NetteFramework に何百本もまとまっています。 - -</div> -</div> diff --git a/best-practices/ja/@left-menu.texy b/best-practices/ja/@left-menu.texy deleted file mode 100644 index 2735f42d4d..0000000000 --- a/best-practices/ja/@left-menu.texy +++ /dev/null @@ -1,34 +0,0 @@ -チュートリアルとベストプラクティス -***************** -- [概要 |@home] - -Nette Application -***************** -- [inject メソッドとアトリビュート |inject-method-attribute] -- [トレイトによるプレゼンターの構成 |presenter-traits] -- [プレゼンターへの設定の受け渡し |passing-settings-to-presenters] -- [リクエストを復元する方法 |restore-request] -- [データベース結果のページネーション |pagination] -- [動的スニペット |dynamic-snippets] -- [#Requires アトリビュートの使い方 |attribute-requires] -- [POST リンクの正しい使い方 |post-links] -- [スラッグを使った読みやすい URL |pretty-urls] - -フォーム -**** -- [フォームの再利用 |form-reuse] -- [レコードの作成と編集のためのフォーム |creating-editing-form] -- [お問い合わせフォームを作ろう |lets-create-contact-form] - -一般 -*** -- [マイクロサイトの書き方 |microsites] -- [Composer: 利用のヒント |composer] - - -関連情報 -**** -- [Nette ドキュメント |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [トラブルシューティング |nette:troubleshooting] diff --git a/best-practices/ja/@meta.texy b/best-practices/ja/@meta.texy deleted file mode 100644 index f76b18d672..0000000000 --- a/best-practices/ja/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: チュートリアルとベストプラクティス}} diff --git a/best-practices/ja/attribute-requires.texy b/best-practices/ja/attribute-requires.texy deleted file mode 100644 index de9e6c340a..0000000000 --- a/best-practices/ja/attribute-requires.texy +++ /dev/null @@ -1,177 +0,0 @@ -`#[Requires]` アトリビュートの使い方 -************************* - -.[perex] -ウェブアプリケーションを書いていると、アプリケーションの一部へのアクセスを制限したくなる場面によく出会います。あるリクエストはフォーム経由(つまり POST メソッド)でしかデータを送れないようにしたい、あるいは AJAX の呼び出しからだけアクセスできるようにしたい、といった具合です。Nette Framework 3.2 では、そうした制限をきれいに分かりやすく設定できる新しい道具が導入されました。`#[Requires]` アトリビュートです。 - -アトリビュートとは、クラスやメソッドの定義の前に書く PHP の特別な印です。実体はクラスなので、以下の例を動かすには `use` 句が必要です。 - -```php -use Nette\Application\Attributes\Requires; -``` - -`#[Requires]` アトリビュートは、プレゼンターのクラス自体と、次のメソッドに使えます。 - -- `action<Action>()` -- `render<View>()` -- `handle<Signal>()` -- `createComponent<Name>()` - -最後の 2 つはコンポーネントにも当てはまるので、コンポーネントでもこのアトリビュートを使えます。 - -アトリビュートが定める条件が満たされない場合、HTTP 4xx のエラーが発生します。 - - -HTTP メソッド ---------- - -アクセスを許す HTTP メソッド(GET、POST など)を指定できます。たとえばフォームの送信でのみアクセスを許したいなら、次のようにします。 - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST')] - public function actionDelete(int $id): void - { - } -} -``` - -状態を変える操作になぜ GET ではなく POST を使うべきなのか、そしてどう使うのかは、[ガイドをお読みください |post-links]。 - -メソッドはひとつでも、配列でも指定できます。特別なのは値 `'*'` で、すべてのメソッドを許します。プレゼンターは[安全のため既定ではそれを許しません |application:presenters#HTTP メソッドのチェック]。 - - -AJAX の呼び出し ----------- - -プレゼンターやメソッドを AJAX のリクエストからだけアクセスできるようにしたい場合は、次のようにします。 - -```php -#[Requires(ajax: true)] -class AjaxPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -同一オリジン ------- - -安全性を高めるために、リクエストが同じドメインから来ることを求められます。これは [CSRF 脆弱性 |nette:vulnerability-protection#Cross-Site Request Forgery (CSRF)]を防ぎます。 - -```php -#[Requires(sameOrigin: true)] -class SecurePresenter extends Nette\Application\UI\Presenter -{ -} -``` - -`handle<Signal>()` メソッドでは、同じドメインからのアクセスが自動的に要求されます。ですから、どのドメインからのアクセスも許したい場合は次のように指定します。 - -```php -#[Requires(sameOrigin: false)] -public function handleList(): void -{ -} -``` - - -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 - { - } -} -``` - - -特定のアクション --------- - -コンポーネントの生成などのコードを、プレゼンターの特定のアクションでのみ使えるように制限することもできます。 - -```php -class EditDeletePresenter extends Nette\Application\UI\Presenter -{ - #[Requires(actions: ['add', 'edit'])] - public function createComponentPostForm() - { - } -} -``` - -アクションがひとつなら配列を書く必要はありません: `#[Requires(actions: 'default')]` - - -カスタムアトリビュート ------------ - -同じ設定で `#[Requires]` アトリビュートを繰り返し使いたいなら、`#[Requires]` を継承して自分の必要に合わせて設定した独自のアトリビュートを作れます。 - -たとえば `#[SingleAction]` は `default` アクションからのアクセスだけを許します。 - -```php -#[\Attribute] -class SingleAction extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(actions: 'default'); - } -} - -#[SingleAction] -class SingleActionPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -あるいは `#[RestMethods]` は、REST API で使うすべての HTTP メソッドでのアクセスを許します。 - -```php -#[\Attribute] -class RestMethods extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']); - } -} - -#[RestMethods] -class ApiPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -まとめ ---- - -`#[Requires]` アトリビュートは、ウェブページへのアクセスのされ方について大きな柔軟さと制御を与えてくれます。単純ながら強力な規則によって、アプリケーションの安全性と正しい動作を高められます。ご覧のとおり、Nette でアトリビュートを使うことは作業を簡単にするだけでなく、安全にもしてくれるのです。 diff --git a/best-practices/ja/composer.texy b/best-practices/ja/composer.texy deleted file mode 100644 index c485a72eaa..0000000000 --- a/best-practices/ja/composer.texy +++ /dev/null @@ -1,282 +0,0 @@ -Composer: 利用のヒント -**************** - -<div class=perex> - -Composer は PHP の依存関係を管理するための道具です。プロジェクトが依存するライブラリを宣言すると、そのインストールと更新を代わりに行ってくれます。ここでは次のことを学びます。 - -- Composer のインストール方法 -- 新しいプロジェクトや既存のプロジェクトでの使い方 - -</div> - - -インストール -====== - -Composer は実行できる `.phar` ファイルで、次のようにダウンロードしてインストールします。 - - -Windows -------- - -公式のインストーラ [Composer-Setup.exe|https://getcomposer.org/Composer-Setup.exe]を使います。 - - -Linux、macOS ------------ - -必要なのは 4 つのコマンドだけで、[このページ |https://getcomposer.org/download/]からコピーできます。 - -さらに、システムの `PATH` に含まれるフォルダにコピーすれば、Composer をどこからでも使えるようになります。 - -```shell -$ mv ./composer.phar ~/bin/composer # または /usr/local/bin/composer -``` - - -プロジェクトでの利用 -========== - -プロジェクトで Composer を使い始めるのに必要なのは `composer.json` ファイルだけです。このファイルはプロジェクトの依存関係を記述し、ほかのメタデータを含むこともあります。最も単純な `composer.json` は次のようなものです。 - -```js -{ - "require": { - "nette/database": "^3.0" - } -} -``` - -ここでは、私たちのアプリケーション(またはライブラリ)が `nette/database` パッケージを必要とし(パッケージ名はベンダー名とプロジェクト名から成ります)、`^3.0` というバージョン制約に合う版(つまり最新のバージョン 3)を求めている、と述べています。 - -`composer.json` ファイルをプロジェクトのルートに置いたら、次を実行します。 - -```shell -composer update -``` - -Composer は Nette Database を `vendor/` ディレクトリにダウンロードします。あわせて `composer.lock` ファイルも作られ、どのライブラリのどのバージョンを正確にインストールしたかの情報が入ります。 - -Composer は `vendor/autoload.php` ファイルを生成します。このファイルを読み込むだけで、余計な作業なしにライブラリのクラスを使い始められます。 - -```php -require __DIR__ . '/vendor/autoload.php'; - -$db = new Nette\Database\Connection('sqlite::memory:'); -``` - - -パッケージを最新版に更新する -============== - -`composer.json` で定めた制約に従って、使っているライブラリを最新版に更新するには `composer update` コマンドを使います。たとえば依存関係が `"nette/database": "^3.0"` なら、最新の 3.x.x 版がインストールされ、バージョン 4 はインストールされません。 - -`composer.json` ファイルの制約自体を、たとえば `"nette/database": "^4.1"` に更新して最新版をインストールできるようにするには、`composer require nette/database` コマンドを使います。 - -使っているすべての Nette のパッケージを更新するには、コマンドラインにそれらをすべて並べる必要があります。たとえば次のようにです。 - -```shell -composer require nette/application nette/forms latte/latte tracy/tracy ... -``` - -これは実用的ではありません。そこで、代わりにやってくれる簡単なスクリプト "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff を使ってください。 - -```shell -php composer-frontline.php -``` - - -新しいプロジェクトの作成 -============ - -新しい Nette のプロジェクトは、ひとつのコマンドで作れます。 - -```shell -composer create-project nette/web-project name-of-the-project -``` - -`name-of-the-project` をプロジェクトのディレクトリ名に置き換えて実行してください。Composer が GitHub から `nette/web-project` リポジトリ(すでに `composer.json` ファイルを含んでいます)をダウンロードし、続いて Nette Framework 自体をインストールします。あとは `temp/` と `log/` ディレクトリの[権限を設定する |nette:troubleshooting#ディレクトリの権限の設定]だけで、プロジェクトが動き出すはずです。 - -プロジェクトを置くホスティングの PHP のバージョンが分かっているなら、必ず[それを設定してください |#PHP のバージョン]。 - - -PHP のバージョン -========== - -Composer は常に、今使っている PHP のバージョン(正確には Composer を実行したコマンドラインの PHP のバージョン)に合うパッケージの版をインストールします。これはウェブホスティングが使うバージョンと同じとは限りません。ですから、ホスティングの PHP のバージョンの情報を `composer.json` ファイルに足すことが決定的に重要です。そうすれば、ホスティングに合う版のパッケージだけがインストールされます。 - -たとえばプロジェクトが PHP 8.2.3 で動くと指定するには、次のコマンドを使います。 - -```shell -composer config platform.php 8.2.3 -``` - -バージョンは `composer.json` ファイルに次のように書かれます。 - -```js -{ - "config": { - "platform": { - "php": "8.2.3" - } - } -} -``` - -ただし PHP のバージョン番号は、このファイルの別の場所、`require` セクションにも書かれます。最初の番号がパッケージをインストールする際のバージョンを決めるのに対し、2 つめはアプリケーション自体がどのバージョン向けに書かれているかを示します。たとえば PhpStorm はこれを使って *PHP language level* を設定します。(もちろんこの 2 つが食い違うのは意味をなさないので、二重に書くことになっているのは設計上の見落としです。)このバージョンは次のコマンドで設定します。 - -```shell -composer require php 8.2.3 --no-update -``` - -あるいは `composer.json` ファイルに直接書きます。 - -```js -{ - "require": { - "php": "8.2.3" - } -} -``` - - -PHP のバージョンを無視する -=============== - -パッケージはふつう、対応する最も低い PHP のバージョンと、テスト済みの最も高いバージョンの両方を指定しています。テストなどのためにそれより新しい PHP のバージョンを使うつもりなら、Composer はそのパッケージのインストールを拒みます。その答えが `--ignore-platform-req=php+` オプションで、これを使うと Composer は要求される PHP バージョンの上限を無視します。 - - -誤った報告 -===== - -パッケージを更新したりバージョン番号を変えたりすると、ときどき衝突が起こります。あるパッケージの要求が別のパッケージと衝突する、といった具合です。ただし Composer は、実際には存在しない衝突を報告することがあります。そうした場合は `composer.lock` ファイルを削除してやり直すとうまくいくことがあります。 - -それでもエラーメッセージが残るなら、それは本物です。何をどう直すべきか、メッセージを読んで理解する必要があります。 - - -Packagist.org - グローバルなリポジトリ -=========================== - -[Packagist |https://packagist.org]は、Composer が既定でパッケージを探す主要なリポジトリです。ここで自分のパッケージを公開することもできます。 - - -中央のリポジトリを使いたくない場合 ------------------ - -社内のアプリケーションやライブラリで公開できないものがあるなら、それ用に自分たちのリポジトリを作れます。 - -リポジトリについて詳しくは[公式ドキュメント |https://getcomposer.org/doc/05-repositories.md#repositories]をご覧ください。 - - -オートローディング -========= - -Composer の重要な機能は、インストールしたすべてのクラスにオートローディングを提供することです。`vendor/autoload.php` ファイルを読み込めば有効になります。 - -さらに Composer を使って、`vendor/` ディレクトリの外のクラスを読み込むこともできます。ひとつめの方法は、指定したディレクトリとそのサブディレクトリを Composer に走査させ、見つけたすべてのクラスをオートローダーに登録させることです。そのためには `composer.json` の `autoload > classmap` を設定します。 - -```js -{ - "autoload": { - "classmap": [ - "src/", # src/ ディレクトリとそのサブディレクトリを含めます - ] - } -} -``` - -その場合、変更のたびに `composer dumpautoload` コマンドを実行してオートローディングの表を作り直す必要があります。これはきわめて面倒です。この仕事は [RobotLoader|robot-loader:]に任せるほうがずっと良く、同じことを自動的に、しかもはるかに速くバックグラウンドで行ってくれます。 - -ふたつめの方法は [PSR-4 |https://www.php-fig.org/psr/psr-4/]に従うことです。簡単にいえば、名前空間とクラス名がディレクトリ構造とファイル名に対応するしくみで、たとえば `App\Core\RouterFactory` は `/path/to/App/Core/RouterFactory.php` ファイルに置かれます。設定の例です。 - -```js -{ - "autoload": { - "psr-4": { - "App\\": "app/" # App\ 名前空間は app/ ディレクトリにあります - } - } -} -``` - -この振る舞いの設定方法について詳しくは [Composer のドキュメント |https://getcomposer.org/doc/04-schema.md#psr-4]をご覧ください。 - - -新しいバージョンを試す -=========== - -パッケージの新しい開発版を試したいですか。やり方を説明します。まず `composer.json` ファイルに次の 2 つのオプションを足します。これで開発版をインストールできるようになりますが、安定版の組み合わせで要求を満たせない場合にだけ Composer は開発版に頼ります。 - -```js -{ - "minimum-stability": "dev", - "prefer-stable": true, -} -``` - -あわせて `composer.lock` ファイルの削除もおすすめします。Composer が理由もはっきりしないままインストールを拒むことがあり、これで解決することがあるからです。 - -パッケージが `nette/utils` で、新しいバージョンが 4.0 だとしましょう。次のコマンドでインストールします。 - -```shell -composer require nette/utils:4.0.x-dev -``` - -あるいは特定のバージョン、たとえば 4.0.0-RC2 をインストールすることもできます。 - -```shell -composer require nette/utils:4.0.0-RC2 -``` - -ただし別のパッケージがそのライブラリに依存していて、古いバージョン(たとえば `^3.1`)に固定されている場合、理想的な解はその依存元のパッケージを新しいバージョンで動くよう更新することです。とはいえ、制限を回避して、開発版を古いバージョン(たとえば 3.1.6)のふりをさせたまま Composer にインストールさせたいだけなら、`as` キーワードを使えます。 - -```shell -composer require nette/utils "4.0.x-dev as 3.1.6" -``` - - -コマンドの呼び出し -========= - -自分で定義したコマンドやスクリプトを、Composer 本来のコマンドであるかのように Composer 経由で呼び出せます。`vendor/bin` ディレクトリにあるスクリプトなら、そのパスを指定する必要もありません。 - -例として、[Nette Tester |tester:]でテストを実行するスクリプトを `composer.json` に定義してみましょう。 - -```js -{ - "scripts": { - "tester": "tester tests -s" - } -} -``` - -あとは `composer tester` でテストを実行します。プロジェクトのルートディレクトリでなく、そのサブディレクトリにいてもこのコマンドを呼べます。 - - -感謝を伝える -====== - -オープンソースの作者を喜ばせる小技を紹介します。プロジェクトが使っているライブラリに、GitHub で簡単にスターを付けられます。`symfony/thanks` ライブラリをインストールするだけです。 - -```shell -composer global require symfony/thanks -``` - -そして次を実行します。 - -```shell -composer thanks -``` - -試してみてください。 - - -設定 -=== - -Composer はバージョン管理ツール [Git |https://git-scm.com]と密に結びついています。Git をインストールしていない場合は、それを使わないよう Composer に伝える必要があります。 - -```shell -composer -g config preferred-install dist -``` diff --git a/best-practices/ja/creating-editing-form.texy b/best-practices/ja/creating-editing-form.texy deleted file mode 100644 index 1a3c0827f7..0000000000 --- a/best-practices/ja/creating-editing-form.texy +++ /dev/null @@ -1,204 +0,0 @@ -レコードの作成と編集のためのフォーム -****************** - -.[perex] -Nette でレコードの追加と編集を、同じフォームを使って正しく実装するにはどうすればよいでしょうか。 - -多くの場合、レコードの追加と編集のフォームは同一で、違うのはボタンのラベルくらいです。ここでは簡単なプレゼンターの例を通して、まずフォームをレコードの追加に使い、次に編集に使い、最後にその 2 つをひとつにまとめます。 - - -レコードの追加 -------- - -レコードを追加するプレゼンターの例です。実際のデータベースとのやり取りは `Facade` クラスに任せます。そのコードはこの例の本筋ではありません。 - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentRecordForm(): Form - { - $form = new Form; - - // ... フォームの項目を追加します ... - - $form->onSuccess[] = $this->recordFormSucceeded(...); - return $form; - } - - private function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // レコードをデータベースに追加します - $this->flashMessage('Successfully added'); - $this->redirect('...'); - } - - public function renderAdd(): void - { - // ... - } -} -``` - - -レコードの編集 -------- - -次に、レコードを編集するプレゼンターがどうなるかを見てみましょう。 - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - private $record; - - public function __construct( - private Facade $facade, - ) { - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // レコードの存在を確かめます - || !$this->facade->isEditAllowed(/*...*/) // 権限を確かめます - ) { - $this->error(); // 404 エラー - } - - $this->record = $record; - } - - protected function createComponentRecordForm(): Form - { - // アクションが 'edit' であることを確かめます - if ($this->getAction() !== 'edit') { - $this->error(); - } - - $form = new Form; - - // ... フォームの項目を追加します ... - - $form->setDefaults($this->record); // 既定値を設定します - $form->onSuccess[] = $this->recordFormSucceeded(...); - return $form; - } - - private function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->update($this->record->id, $data); // レコードを更新します - $this->flashMessage('Successfully updated'); - $this->redirect('...'); - } -} -``` - -[プレゼンターのライフサイクル |application:presenters#プレゼンターのライフサイクル]の初めに呼ばれる *action* メソッドで、レコードの存在とユーザーの編集権限を確かめます。 - -レコードは `$record` プロパティに保存し、既定値の設定のために `createComponentRecordForm()` メソッドから、ID にアクセスするために `recordFormSucceeded()` から使えるようにしています。別の方法として、既定値を `actionEdit()` で直接設定し、ID の値(URL の一部)を `getParameter('id')` で取得することもできます。 - - -```php - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - // 存在の確認と権限のチェック - ) { - $this->error(); - } - - // フォームの既定値を設定します - $this->getComponent('recordForm') - ->setDefaults($record); - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); - // ... - } -``` - -ただし、そして**これがこのコード全体で最も大切な学び**ですが、フォームを作るときにアクションが本当に `edit` であることを確かめなければなりません。さもないと、`actionEdit()` メソッドでの確認がまったく行われないからです。 - - -追加と編集で同じフォームを使う ---------------- - -では 2 つのプレゼンターをひとつにまとめましょう。`createComponentRecordForm()` メソッドの中でアクションを見分けてフォームを設定してもよいですし、それをアクションのメソッドに任せて条件のチェックをなくすこともできます。 - - -```php -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - public function actionAdd(): void - { - $form = $this->getComponent('recordForm'); - $form->onSuccess[] = $this->addingFormSucceeded(...); - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // レコードの存在を確かめます - || !$this->facade->isEditAllowed(/*...*/) // 権限を確かめます - ) { - $this->error(); // 404 エラー - } - - $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // 既定値を設定します - $form->onSuccess[] = $this->editingFormSucceeded(...); - } - - protected function createComponentRecordForm(): Form - { - // アクションが 'add' か 'edit' であることを確かめます - if (!in_array($this->getAction(), ['add', 'edit'])) { - $this->error(); - } - - $form = new Form; - - // ... フォームの項目を追加します ... - - return $form; - } - - private function addingFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // レコードをデータベースに追加します - $this->flashMessage('Successfully added'); - $this->redirect('...'); - } - - private function editingFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // レコードを更新します - $this->flashMessage('Successfully updated'); - $this->redirect('...'); - } -} -``` - -{{priority: -1}} diff --git a/best-practices/ja/dynamic-snippets.texy b/best-practices/ja/dynamic-snippets.texy deleted file mode 100644 index fa5595934c..0000000000 --- a/best-practices/ja/dynamic-snippets.texy +++ /dev/null @@ -1,176 +0,0 @@ -動的スニペット -******* - -.[perex] -Latte の動的スニペットを使い、AJAX でページのうち実際に変わる部分だけ、たとえばリストの個々の項目だけを更新する方法です。 - -アプリケーションを開発していると、テーブルの行やリストの項目ごとに AJAX の操作を行いたくなることがよくあります。例として、記事の一覧を考えてみましょう。ログインしたユーザーは各記事に「いいね」や「よくない」の評価を付けられます。AJAX なしのプレゼンターのコードと対応するテンプレートは、次のようなものになります(関係の深い部分だけを示します。評価の処理と記事の取得を担うサービスがあると仮定しますが、その具体的な実装はこのガイドの本筋ではありません)。 - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - $this->redirect('this'); -} - -public function handleUnlike(int $articleId): void -{ - $this->ratingService->removeLike($articleId, $this->user->id); - $this->redirect('this'); -} -``` - -テンプレート: - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {if !$article->liked} - <a n:href="like!, $article->id" class=ajax>いいね</a> - {else} - <a n:href="unlike!, $article->id" class=ajax>いいねを取り消す</a> - {/if} -</article> -``` - - -AJAX 化 -====== - -では、この簡単なアプリケーションに AJAX の機能を足しましょう。記事の評価を変えることはページ全体のリダイレクトを要するほど重大ではないので、理想的にはバックグラウンドの AJAX で済ませたいところです。AJAX のリンクに CSS クラス `ajax` を付けるという一般的な決まりに従って、[アドオンのハンドラースクリプト |application:ajax#Naja]を使います。 - -しかし具体的にはどう実装すればよいでしょうか。Nette には 2 つのやり方があります。動的スニペットと、コンポーネントです。それぞれ長所と短所があるので、両方を見ていきましょう。 - - -動的スニペットのやり方 -=========== - -Latte の用語でいう動的スニペットとは、スニペットの名前に変数を使う `{snippet}` タグの特別な使い方を指します。そうしたスニペットはテンプレートのどこにでも置けるわけではなく、静的な(通常の)スニペットで包むか、`{snippetArea}` の中に置く必要があります。テンプレートは次のように書き換えられます。 - - -```latte -{snippet articlesContainer} - <article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {snippet article-{$article->id}} - {if !$article->liked} - <a n:href="like!, $article->id" class=ajax>いいね</a> - {else} - <a n:href="unlike!, $article->id" class=ajax>いいねを取り消す</a> - {/if} - {/snippet} - </article> -{/snippet} -``` - -各記事が、記事の ID を名前に含むスニペットを定義するようになりました。これらの動的スニペットはすべて、`articlesContainer` という名前の静的スニペットでまとめて包まれています。この外側のスニペットを省くと、Latte は例外を投げます。 - -あとはプレゼンターに再描画のロジックを足すだけです。静的な包みを再描画すればよいのです。 - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - if ($this->isAjax()) { - $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- 不要です - } else { - $this->redirect('this'); - } -} -``` - -対応する `handleUnlike()` メソッドも同じように書き換えれば、AJAX が動きます。 - -ただし、この解には欠点があります。AJAX のリクエストをよく調べると、外からは効率よく見えても(特定の記事のスニペットひとつしか返しません)、サーバー側では実際に*すべての*スニペットを描いていることが分かります。必要なスニペットをペイロードに入れ、ほかは捨てているのです(つまり無駄に取得して描いています)。 - -これを最適化するには、`$articles` のコレクションがテンプレートに渡される場所(たとえば `renderDefault()` メソッド)に手を入れる必要があります。シグナルの処理が `render<Something>` メソッドより前に起こることを利用します。 - -```php -public function handleLike(int $articleId): void -{ - // ... - if ($this->isAjax()) { - // ... - $this->template->articles = [ - $this->db->table('articles')->get($articleId), - ]; - } else { - // ... -} - -public function renderDefault(): void -{ - if (!isset($this->template->articles)) { - $this->template->articles = $this->db->table('articles'); - } -} -``` - -これでシグナルの処理中は、記事のコレクション全体ではなく、該当する記事ひとつだけを含む配列がテンプレートに渡されます。描いてペイロードでブラウザに送ろうとしている、まさにその記事です。おかげで `{foreach}` ループは一度しか回らず、不要なスニペットは描かれません。 - - -コンポーネントのやり方 -=========== - -まったく別のやり方として、動的スニペットをそもそも使わない方法があります。仕掛けは、ロジック全体を独立したコンポーネントにまとめてしまうことです。プレゼンターが評価を扱う代わりに、専用の `LikeControl` がそれを担当します。クラスは次のようになります(`render`、`handleUnlike` などのメソッドも含みます)。 - -```php -class LikeControl extends Nette\Application\UI\Control -{ - public function __construct( - private Article $article, - ) { - } - - public function handleLike(): void - { - $this->ratingService->saveLike($this->article->id, $this->presenter->user->id); - if ($this->presenter->isAjax()) { - $this->redrawControl(); - } else { - $this->presenter->redirect('this'); - } - } -} -``` - -コンポーネントのテンプレート: - -```latte -{snippet} - {if !$article->liked} - <a n:href="like!" class=ajax>いいね</a> - {else} - <a n:href="unlike!" class=ajax>いいねを取り消す</a> - {/if} -{/snippet} -``` - -当然ながらビューのテンプレートも変わり、プレゼンターにファクトリを足す必要があります。データベースから取得した記事ごとにこのコンポーネントのインスタンスを作るので、その生成を管理するために [Multiplier |application:Multiplier]クラスを使います。 - -```php -protected function createComponentLikeControl() -{ - $articles = $this->db->table('articles'); - return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { - return new LikeControl($articles[$articleId]); - }); -} -``` - -ビューのテンプレートは最小限にまで縮み、しかもスニペットが完全になくなります。 - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {control "likeControl-$article->id"} -</article> -``` - -これでほぼ完成です。アプリケーションは AJAX で動くようになります。ここでも最適化は必要で、Nette Database を使っているためにシグナルの処理が該当する記事だけでなくすべての記事を無駄に読み込んでしまいます。ただし利点として、描画されるのは特定のコンポーネントのインスタンスだけなので、無駄な描画は起こりません。 - -{{priority: -1}} diff --git a/best-practices/ja/form-reuse.texy b/best-practices/ja/form-reuse.texy deleted file mode 100644 index cc72357358..0000000000 --- a/best-practices/ja/form-reuse.texy +++ /dev/null @@ -1,348 +0,0 @@ -複数の場所でのフォームの再利用 -*************** - -.[perex] -Nette には、同じフォームを複数の場所でコードを重複させずに再利用する方法がいくつかあります。この記事では、避けるべきものも含めてさまざまな解を扱います。 - - -フォームのファクトリ -========== - -コンポーネントを複数の場所で再利用するための基本的なやり方は、そのコンポーネントを作るメソッドやクラスを用意することです。そのメソッドをアプリケーションのさまざまな場所から呼びます。こうしたメソッドやクラスを*ファクトリ*と呼びます。これを *factory method* デザインパターンと混同しないでください。あちらはファクトリの特定の使い方を述べたもので、この話題とは直接の関係がありません。 - -例として、編集フォームを組み立てるファクトリを作ってみましょう。 - -```php -use Nette\Application\UI\Form; - -class FormFactory -{ - public function createEditForm(): Form - { - $form = new Form; - $form->addText('title', 'タイトル:'); - // ここにほかのフォーム項目を足します - $form->addSubmit('send', '保存'); - return $form; - } -} -``` - -これでこのファクトリを、プレゼンターやコンポーネントなどアプリケーションのさまざまな場所で使えます。[依存関係として要求する |dependency-injection:passing-dependencies]ことでそうします。まず設定ファイルにクラスを登録します。 - -```neon -services: - - FormFactory -``` - -そしてプレゼンターで使います。 - - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->createEditForm(); - $form->onSuccess[] = function () { - // 送信されたデータの処理 - }; - return $form; - } -} -``` - -アプリケーションの必要に応じて、ほかの種類のフォームを作るメソッドをファクトリに足していけます。当然ながら、要素のない基本のフォームを作るメソッドを足して、ほかのメソッドからそれを使うこともできます。 - -```php -class FormFactory -{ - public function createForm(): Form - { - $form = new Form; - return $form; - } - - public function createEditForm(): Form - { - $form = $this->createForm(); - $form->addText('title', 'タイトル:'); - // ここにほかのフォーム項目を足します - $form->addSubmit('send', '保存'); - return $form; - } -} -``` - -`createForm()` メソッドはまだあまり役に立ちませんが、それはすぐに変わります。 - - -ファクトリの依存関係 -========== - -やがてフォームを多言語対応にする必要が出てくるかもしれません。つまり、すべてのフォームに[トランスレーター |forms:rendering#翻訳]を設定するということです。そのためには `FormFactory` クラスを書き換え、コンストラクタで `Translator` オブジェクトを依存関係として受け取り、作ったフォームに渡すようにします。 - -```php -use Nette\Localization\Translator; - -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function createForm(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } - - // ... -} -``` - -`createForm()` メソッドは個別のフォームを作るほかのメソッドからも呼ばれるので、ここでトランスレーターを設定すれば十分です。これで完了です。プレゼンターやコンポーネントのコードを直す必要はまったくなく、素晴らしいことです。 - - -ファクトリのクラスを増やす -============= - -別の方法として、アプリケーションで使うフォームごとに独立したファクトリのクラスを作ることもできます。こうするとコードが読みやすくなり、フォームの管理も簡単になります。もとの `FormFactory` には(翻訳のサポートなど)基本的な設定を持つ素のフォームだけを作らせ、編集フォーム専用の新しいファクトリ `EditFormFactory` を作りましょう。 - -```php -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function create(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } -} - - -// ✅ コンポジションを使う -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - // ここにほかのフォーム項目を足します - $form->addSubmit('send', '保存'); - return $form; - } -} -``` - -大事なのは、`FormFactory` と `EditFormFactory` クラスの関係が[オブジェクトの継承 |nette:introduction-to-object-oriented-programming#合成]ではなく[コンポジション |nette:introduction-to-object-oriented-programming#継承]で実現されていることです。 - -```php -// ⛔ だめです! ここに継承は要りません -class EditFormFactory extends FormFactory -{ - public function create(): Form - { - $form = parent::create(); - $form->addText('title', 'タイトル:'); - // ここにほかのフォーム項目を足します - $form->addSubmit('send', '保存'); - return $form; - } -} -``` - -ここで継承を使うのはまったく逆効果です。すぐに問題にぶつかります。たとえば `create()` メソッドにパラメータを足したくなったら、シグネチャが親と違うので PHP がエラーを報告します。あるいは `EditFormFactory` クラスにコンストラクタで依存関係を渡す場合。これは[コンストラクタ地獄 |dependency-injection:passing-dependencies#コンストラクタ地獄]として知られる状態を招きます。 - -一般に、[継承よりコンポジションを優先する |dependency-injection:faq#なぜ継承よりコンポジションが好まれるのですか?]ほうが良いのです。 - - -フォームの処理 -======= - -送信が成功したときに呼ばれるフォームのハンドラも、ファクトリのクラスの一部にできます。ハンドラは送られたデータをモデル層に渡して処理させます。処理中のエラーはフォームに[戻されます |forms:validation#エラーの処理]。次の例では、モデルを `Facade` クラスが表しています。 - -```php -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - private Facade $facade, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - $form->addText('title', 'タイトル:'); - // ここにほかのフォーム項目を足します - $form->addSubmit('send', '保存'); - $form->onSuccess[] = $this->processForm(...); - return $form; - } - - private function processForm(Form $form, array $data): void - { - try { - // 送信されたデータの処理 - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - } - } -} -``` - -ただしリダイレクトはプレゼンター自身に任せましょう。プレゼンターは `onSuccess` イベントにもうひとつハンドラを足し、そこでリダイレクトを行います。おかげで同じフォームをさまざまなプレゼンターで使え、成功時にはそれぞれ別の場所へリダイレクトできます。 - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditFormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->create(); - $form->onSuccess[] = function () { - $this->flashMessage('レコードを保存しました'); - $this->redirect('Homepage:'); - }; - return $form; - } -} -``` - -この解は、フォームやその要素で `addError()` が呼ばれると、以降の `onSuccess` ハンドラが呼ばれないというフォームの性質を利用しています。 - - -Form クラスの継承 -=========== - -組み立てたフォームは `Form` クラスの子孫であるべきではありません。言い換えると、次のやり方は避けてください。 - -```php -// ⛔ だめです! ここに継承は要りません -class EditForm extends Form -{ - public function __construct(Translator $translator) - { - parent::__construct(); - $this->addText('title', 'タイトル:'); - // ここにほかのフォーム項目を足します - $this->addSubmit('send', '保存'); - $this->setTranslator($translator); - } -} -``` - -コンストラクタの中でフォームを組み立てる代わりに、ファクトリを使ってください。 - -大事なのは、`Form` クラスが第一にフォームを組み立てるための道具、つまり *form builder* であると認識することです。組み立てられたフォームは、その成果物と見なせます。しかし成果物は builder の一種ではありません。両者のあいだに、継承の土台となる *is a* の関係はないのです。 - - -フォームのコンポーネント -============ - -まったく別の方法として、フォームを包む[コンポーネント |application:components]を作ることもできます。これは新しい可能性を開きます。コンポーネントは自分のテンプレートを持つので、たとえばフォームを特定の形で描けます。あるいはシグナルを使って AJAX で通信し、候補の表示などのために情報を動的にフォームに読み込むこともできます。 - - -```php -use Nette\Application\UI\Form; - -class EditControl extends Nette\Application\UI\Control -{ - public array $onSave = []; - - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentForm(): Form - { - $form = new Form; - $form->addText('title', 'タイトル:'); - // ここにほかのフォーム項目を足します - $form->addSubmit('send', '保存'); - $form->onSuccess[] = $this->processForm(...); - - return $form; - } - - private function processForm(Form $form, array $data): void - { - try { - // 送信されたデータの処理 - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - return; - } - - // イベントの発火 - $this->onSave($this, $data); - } -} -``` - -次に、このコンポーネントを作るファクトリを用意します。[インターフェースを定義する |application:components#依存関係を持つコンポーネント]だけで十分です。 - -```php -interface EditControlFactory -{ - function create(): EditControl; -} -``` - -そして設定ファイルに足します。 - -```neon -services: - - EditControlFactory -``` - -これでファクトリを要求して、プレゼンターで使えます。 - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditControlFactory $controlFactory, - ) { - } - - protected function createComponentEditForm(): EditControl - { - $control = $this->controlFactory->create(); - - $control->onSave[] = function (EditControl $control, $data) { - $this->redirect('this'); - // あるいは編集結果へリダイレクトします。たとえば: - // $this->redirect('detail', ['id' => $data->id]); - }; - - return $control; - } -} -``` diff --git a/best-practices/ja/inject-method-attribute.texy b/best-practices/ja/inject-method-attribute.texy deleted file mode 100644 index c5127f5c21..0000000000 --- a/best-practices/ja/inject-method-attribute.texy +++ /dev/null @@ -1,61 +0,0 @@ -inject メソッドとアトリビュート -******************* - -.[perex] -この記事では、Nette フレームワークでプレゼンターに依存関係を渡すさまざまな方法を扱います。推奨される方法であるコンストラクタインジェクションと、`inject` メソッドやアトリビュートといった代替手段を比べていきます。 - -プレゼンターでも、ほかのクラスと同じく、[コンストラクタ |dependency-injection:passing-dependencies#コンストラクタインジェクション]で依存関係を渡すのが推奨される方法です。しかし、ほかのプレゼンターが継承する共通の祖先(`BasePresenter` など)を作り、その祖先も依存関係を必要とする場合、[コンストラクタ地獄 |dependency-injection:passing-dependencies#コンストラクタ地獄]として知られる問題が生じます。これは別の方法、すなわち inject メソッドとアトリビュート(以前はアノテーション)で回避できます。 - - -`inject*()` メソッド -================ - -これは[セッター |dependency-injection:passing-dependencies#セッターインジェクション]による依存関係の受け渡しの一種です。セッターの名前は接頭辞 `inject` で始まらなければなりません。Nette DI は、プレゼンターのインスタンスを作った直後にこの名前のメソッドを自動的に呼び、必要な依存関係をすべて渡します。ですから public として宣言する必要があります。 - -`inject*()` メソッドは、複数のメソッドに分割されたコンストラクタの延長と見なせます。これにより `BasePresenter` は別のメソッドで依存関係を受け取れるようになり、コンストラクタは子孫のために空けておけます。 - -```php -abstract class BasePresenter extends Nette\Application\UI\Presenter -{ - private Foo $foo; - - public function injectBase(Foo $foo): void - { - $this->foo = $foo; - } -} - -class MyPresenter extends BasePresenter -{ - private Bar $bar; - - public function __construct(Bar $bar) - { - $this->bar = $bar; - } -} -``` - -プレゼンターは `inject*()` メソッドをいくつでも持てますし、それぞれがいくつでもパラメータを取れます。この方法は、プレゼンターが[トレイトから構成されている |presenter-traits]場合で、各トレイトが独自の依存関係を必要とするときにも向いています。 - - -`Inject` アトリビュート -================ - -これは[プロパティへの注入 |dependency-injection:passing-dependencies#プロパティインジェクション]の一種です。注入したいプロパティに印を付けるだけで、Nette DI がプレゼンターのインスタンスを作った直後に依存関係を自動的に渡します。注入を可能にするため、これらのプロパティは public として宣言する必要があります。 - -プロパティにはアトリビュートで印を付けます(以前は `/** @inject */` アノテーションが使われていました)。 - -```php -use Nette\DI\Attributes\Inject; // この行が大事です - -class MyPresenter extends Nette\Application\UI\Presenter -{ - #[Inject] - public Cache $cache; -} -``` - -この受け渡し方法の利点は、構文がとても簡潔なことでした。しかし[コンストラクタのプロパティ昇格 |https://blog.nette.org/en/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]が導入されてからは、コンストラクタを使うほうが簡単に見えることが多くなりました。 - -逆にこの方法は、プロパティへの注入一般と同じ欠点を抱えています。変数の変更を制御できず、その変数がクラスの公開インターフェースの一部になってしまいます。これは一般に望ましくありません。 diff --git a/best-practices/ja/lets-create-contact-form.texy b/best-practices/ja/lets-create-contact-form.texy deleted file mode 100644 index dc494337f1..0000000000 --- a/best-practices/ja/lets-create-contact-form.texy +++ /dev/null @@ -1,218 +0,0 @@ -お問い合わせフォームを作ろう -************** - -.[perex] -Nette でお問い合わせフォームを作り、送られたデータをメールで送るところまでを見ていきましょう。さっそく始めます。 - -まず新しいプロジェクトを作る必要があります。方法は[はじめに |nette:installation]のページで説明しています。それができたら、フォームを作り始められます。 - -最も簡単なのは、[プレゼンターの中に直接フォームを作る |forms:in-presenter]方法です。すでにある `HomePresenter` を使えます。フォームを表す `contactForm` というコンポーネントを追加しましょう。プレゼンターのコードに、そのコンポーネントを作るファクトリメソッド `createComponentContactForm()` を足します。 - -```php -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - protected function createComponentContactForm(): Form - { - $form = new Form; - $form->addText('name', 'お名前:') - ->setRequired('お名前を入力してください'); - $form->addEmail('email', 'メールアドレス:') - ->setRequired('メールアドレスを入力してください'); - $form->addTextArea('message', 'メッセージ:') - ->setRequired('メッセージを入力してください'); - $form->addSubmit('send', '送信'); - $form->onSuccess[] = $this->contactFormSucceeded(...); - return $form; - } - - private function contactFormSucceeded(Form $form, $data): void - { - // メールの送信 - } -} -``` - -ご覧のとおり、メソッドを 2 つ作りました。ひとつめの `createComponentContactForm()` は新しいフォームのインスタンスを作ります。名前、メールアドレス、メッセージの項目を、それぞれ `addText()`、`addEmail()`、`addTextArea()` メソッドで追加しています。送信ボタンも足しました。では、ユーザーが項目を空のままにしたらどうなるでしょうか。その場合は必須であることを伝えるべきです。それを `setRequired()` メソッドで実現しました。最後に `onSuccess` に[イベント |nette:glossary#イベント]ハンドラを結びつけています。これはフォームが正しく送信されたときに発火します。ここでは `contactFormSucceeded` メソッドを呼び、送られたデータの処理を担当させます。このメソッドはこのあと実装します。 - -`contactForm` コンポーネントを `Home/default.latte` テンプレートで描きましょう。 - -```latte -{block content} -<h1>お問い合わせフォーム</h1> -{control contactForm} -``` - -メールの送信そのもののために、`ContactFacade` という新しいクラスを作り、`app/Model/ContactFacade.php` に置きます。 - -```php -namespace App\Model; - -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $mail = new Message; - $mail->addTo('admin@example.com') // あなたのメールアドレス - ->setFrom($email, $name) - ->setSubject('お問い合わせフォームからのメッセージ') - ->setBody($message); - - $this->mailer->send($mail); - } -} -``` - -`sendMessage()` メソッドがメールを作って送ります。そのために mailer サービスを使い、コンストラクタで依存関係として受け取っています。詳しくは[メールの送信 |mail:]をご覧ください。 - -ではプレゼンターに戻って `contactFormSucceeded()` メソッドを仕上げましょう。フォームから送られたデータを渡して、`ContactFacade` クラスの `sendMessage()` メソッドを呼びます。では `ContactFacade` オブジェクトはどうやって手に入れるのでしょうか。依存性注入を使い、コンストラクタで要求します。 - -```php -use App\Model\ContactFacade; -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - public function __construct( - private ContactFacade $facade, - ) { - } - - protected function createComponentContactForm(): Form - { - // ... - } - - public function contactFormSucceeded(stdClass $data): void - { - $this->facade->sendMessage($data->email, $data->name, $data->message); - $this->flashMessage('メッセージを送信しました'); - $this->redirect('this'); - } -} -``` - -メールを送ったあと、送信を知らせる[フラッシュメッセージ |application:components#フラッシュメッセージ]をユーザーに表示します。それからリダイレクトして、ブラウザの再読み込みでフォームが再送信されないようにします。 - - -ここまで正しく設定できていれば、お問い合わせフォームからメールを送れるはずです。おめでとうございます。 - - -HTML メールのテンプレート ---------------- - -今のところ、フォームから送られたメッセージだけを含むプレーンテキストのメールが送られます。しかしメールに HTML を使えば、見た目をより魅力的にできます。そのためのテンプレートを Latte で作り、`app/Model/contactEmail.latte` として保存します。 - -```latte -<html> - <title>お問い合わせフォームからのメッセージ - - -

    お名前: {$name}

    -

    メールアドレス: {$email}

    -

    メッセージ: {$message}

    - - -``` - -あとは、このテンプレートを使うよう `ContactFacade` を書き換えるだけです。コンストラクタで `LatteFactory` クラスを要求します。これは [Latte のテンプレートレンダラー |latte:develop#テンプレートをレンダリングするには]である `Latte\Engine` オブジェクトを作れます。`renderToString()` メソッドでテンプレートを文字列に描きます。第 1 パラメータはテンプレートファイルへのパス、第 2 パラメータはそこに渡す変数の配列です。 - -```php -namespace App\Model; - -use Nette\Bridges\ApplicationLatte\LatteFactory; -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $latte = $this->latteFactory->create(); - $body = $latte->renderToString(__DIR__ . '/contactEmail.latte', [ - 'email' => $email, - 'name' => $name, - 'message' => $message, - ]); - - $mail = new Message; - $mail->addTo('admin@example.com') // あなたのメールアドレス - ->setFrom($email, $name) - ->setHtmlBody($body); - - $this->mailer->send($mail); - } -} -``` - -生成した HTML のメール本文は、もとの `setBody()` ではなく `setHtmlBody()` メソッドに渡します。`setSubject()` でメールの件名を指定する必要もありません。ライブラリがテンプレートの `` 要素から自動的に取り出してくれるからです。 - - -設定 ---- - -`ContactFacade` クラスのコードには、管理者のメールアドレス `admin@example.com` がまだ直接書かれています。これは設定ファイルに移したほうがよいでしょう。どうすればよいでしょうか。 - -まず `ContactFacade` クラスを書き換え、直接書かれたメールアドレスの文字列を、コンストラクタで渡される変数に置き換えます。 - -```php -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - private string $adminEmail, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - // ... - $mail = new Message; - $mail->addTo($this->adminEmail) - ->setFrom($email, $name) - ->setHtmlBody($body); - // ... - } -} -``` - -次の段階は、この変数の値を設定で与えることです。`app/config/services.neon` ファイルに次を足します。 - -```neon -services: - - App\Model\ContactFacade(adminEmail: admin@example.com) -``` - -これで完了です。`services` セクションに項目が多く、メールアドレスがその中で埋もれてしまうと感じるなら、パラメータにできます。次のように書き換えてください。 - -```neon -services: - - App\Model\ContactFacade(adminEmail: %adminEmail%) -``` - -そしてこのパラメータを `app/config/common.neon` ファイルで定義します。 - -```neon -parameters: - adminEmail: admin@example.com -``` - -これで出来上がりです。 diff --git a/best-practices/ja/microsites.texy b/best-practices/ja/microsites.texy deleted file mode 100644 index 5d47fd2b93..0000000000 --- a/best-practices/ja/microsites.texy +++ /dev/null @@ -1,63 +0,0 @@ -マイクロサイトの書き方 -*********** - -会社の近々のイベントのために、小さなウェブサイトを手早く作る必要があるとしましょう。シンプルで、速く、余計な面倒がないものが求められます。こんな小さなプロジェクトに堅牢なフレームワークは要らない、と思うかもしれません。しかし、Nette Framework を使うことでその作業がむしろ簡単に、そして速くなるとしたらどうでしょうか。 - -単純なウェブサイトを作るときでも、快適さを手放したくはありません。すでに解決済みのことを作り直したくもありません。遠慮なく怠けて、甘やかされてください。Nette Framework はマイクロフレームワークとしても優れています。 - -そんなマイクロサイトはどんな形になるでしょうか。たとえば、サイト全体のコードが公開ディレクトリの `index.php` ファイル 1 本に収まります。 - -```php -<?php - -require __DIR__ . '/../vendor/autoload.php'; - -$configurator = new Nette\Bootstrap\Configurator; -$configurator->enableTracy(__DIR__ . '/../log'); -$configurator->setTempDirectory(__DIR__ . '/../temp'); - -// config.neon の設定にもとづいて DI コンテナを作ります -$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); - return $presenter->redirectUrl("/$lang"); -}); - -// URL https://example.com/cs や https://example.com/en のルート -$router->addRoute('<lang cs|en|de>', 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` ディレクトリに置かれたテンプレートです。 - -`index.php` の PHP コードは、まず[環境を整え |bootstrap:]、次に[ルート |application:routing#コールバックによる動的なルーティング]を定義し、最後にアプリケーションを実行します。利点は、`addRoute()` 関数の第 2 パラメータに callable を渡せて、対応するページにアクセスされたときにそれが実行されることです。 - - -なぜマイクロサイトに Nette を使うのか ----------------------- - -- [Tracy|tracy:]を試したプログラマーは、今日それなしでコードを書くことをほとんど想像できません。 -- 何より [Latte|latte:]のテンプレートシステムの恩恵を受けられます。ページが 2 つしかなくても、[レイアウトと内容|latte:template-inheritance]は分けたくなるからです。 -- そして XSS 脆弱性を防ぐために、[自動エスケープ |latte:safety-first]に頼りたくなるはずです。 -- Nette は、エラーが起きたときに生の PHP のエラーメッセージが表示されないようにし、代わりに親しみやすいページを出します。 -- お問い合わせフォームなどでユーザーの声を集めたくなったら、[フォーム|forms:]と[データベース|database:]のサポートを簡単に足せます。 -- 記入されたフォームを[メールで送る|mail:]のも簡単です。 -- ときには[キャッシュ|caching:]が役立つこともあります。たとえばフィードを取得して表示する場合です。 - -速さと効率が決定的な今の時代、無駄な遅れなく結果にたどり着ける道具を持っていることは重要です。Nette Framework はまさにそれを提供します。素早い開発、安全性、そして作業を滑らかにする Tracy や Latte といった幅広い道具です。Nette のパッケージをいくつかインストールするだけで、こうしたマイクロサイトを作るのは驚くほど簡単になります。しかも隠れたセキュリティの穴がないと安心できます。 diff --git a/best-practices/ja/pagination.texy b/best-practices/ja/pagination.texy deleted file mode 100644 index 6320181781..0000000000 --- a/best-practices/ja/pagination.texy +++ /dev/null @@ -1,273 +0,0 @@ -データベース結果のページネーション -***************** - -.[perex] -ウェブアプリケーションを開発していると、1 ページに並べる項目の数を制限したいという要求によく出会います。これはページネーションとして知られる手法です。 - -まずはページネーションなしですべてのデータを並べる状態から始めましょう。データベースからデータを取り出すために、`ArticleRepository` クラスがあります。コンストラクタのほかに、公開済みの記事を公開日の降順で返す `findPublishedArticles` メソッドを持っています。 - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC', - new \DateTime, - ); - } -} -``` - -プレゼンターでは、このモデルのクラスを注入します。render メソッドで公開済みの記事を取得し、テンプレートに渡します。 - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(): void - { - $this->template->articles = $this->articleRepository->findPublishedArticles(); - } -} -``` - -`default.latte` テンプレートが記事の一覧を受け持ちます。 - -```latte -{block content} -<h1>記事</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> -``` - - -これですべての記事を並べられますが、記事の数が増えると問題になります。そこでページネーションのしくみを実装すると役に立ちます。 - -このしくみは、すべての記事をいくつかのページに分け、今選ばれているページに属する記事だけを表示します。ページの総数と記事の分け方は、記事の総数と 1 ページあたりの記事数をもとに [Paginator |utils:Paginator]ユーティリティが計算します。 - -まず、リポジトリのクラスの記事取得メソッドを、1 ページ分の記事だけを返せるように書き換えます。あわせて、Paginator の設定に必要な、データベース内の記事の総数を得るメソッドも足します。 - -```php -namespace App\Model; - -use Nette; - - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(int $limit, int $offset): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC - LIMIT ? - OFFSET ?', - new \DateTime, $limit, $offset, - ); - } - - /** - * 公開済みの記事の総数を返します - */ - public function getPublishedArticlesCount(): int - { - return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime); - } -} -``` - -次にプレゼンターを書き換えます。現在のページ番号を `renderDefault` メソッドに渡します。この数が URL の一部でなければ、既定値の 1(最初のページ)を設定します。 - -さらに render メソッドを拡張して、Paginator のインスタンスを作って設定し、テンプレートに表示する記事を適切に選びます。書き換えた `HomePresenter` は次のようになります。 - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // 公開済みの記事の総数を取得します - $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - - // Paginator のインスタンスを作って設定します - $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // 項目の総数 - $paginator->setItemsPerPage(10); // 1 ページあたりの項目数 - $paginator->setPage($page); // 現在のページ番号 - - // Paginator の計算にもとづいて、限られた数の記事をデータベースから取得します - $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - - // テンプレートに渡します - $this->template->articles = $articles; - // ページネーションの操作部品を表示するために Paginator 自体も渡します - $this->template->paginator = $paginator; - } -} -``` - -テンプレートは現在のページの記事だけを反復するようになりました。あとはページネーションのリンクを足すだけです。 - -```latte -{block content} -<h1>記事</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if !$paginator->isFirst()} - <a n:href="default, 1">最初</a> -  |  - <a n:href="default, $paginator->getPage() - 1">前へ</a> -  |  - {/if} - - ページ {$paginator->getPage()} / {$paginator->getPageCount()} - - {if !$paginator->isLast()} -  |  - <a n:href="default, $paginator->getPage() + 1">次へ</a> -  |  - <a n:href="default, $paginator->getPageCount()">最後</a> - {/if} -</div> -``` - - -これで Paginator を使ったページネーションの実装は完了です。データベース層として [Nette Database Explorer |database:explorer]を [Nette Database Core |database:sql-way]の代わりに使っているなら、Paginator ユーティリティを直接使わずにページネーションを実装できます。`Nette\Database\Table\Selection` クラスには、ページネーションのロジックを包んだ [page() |api:Nette\Database\Table\Selection::page()] メソッドがあります。 - -この方法なら、リポジトリは次のようになります。 - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Explorer $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\Table\Selection - { - return $this->database->table('articles') - ->where('created_at < ', new \DateTime) - ->order('created_at DESC'); - } -} -``` - -プレゼンターでは Paginator のインスタンスを作る必要がありません。代わりに、リポジトリが返す `Selection` オブジェクトの `page()` メソッドを使います。 - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // 公開済みの記事を取得します - $articles = $this->articleRepository->findPublishedArticles(); - - // page メソッドの計算で限られた分だけをテンプレートに渡します - $lastPage = 0; - $this->template->articles = $articles->page($page, 10, $lastPage); - - // ページネーションの選択肢を表示するのに必要なデータも渡します - $this->template->page = $page; - $this->template->lastPage = $lastPage; - } -} -``` - -Paginator オブジェクトをテンプレートに渡さなくなったので、ページネーションのリンクを表示する部分を調整する必要があります。 - -```latte -{block content} -<h1>記事</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if $page > 1} - <a n:href="default, 1">最初</a> -  |  - <a n:href="default, $page - 1">前へ</a> -  |  - {/if} - - ページ {$page} / {$lastPage} - - {if $page < $lastPage} -  |  - <a n:href="default, $page + 1">次へ</a> -  |  - <a n:href="default, $lastPage">最後</a> - {/if} -</div> -``` - -こうして、Paginator ユーティリティを明示的に使わずにページネーションのしくみを実装できました。 - -{{priority: -1}} diff --git a/best-practices/ja/passing-settings-to-presenters.texy b/best-practices/ja/passing-settings-to-presenters.texy deleted file mode 100644 index 8da3364e8a..0000000000 --- a/best-practices/ja/passing-settings-to-presenters.texy +++ /dev/null @@ -1,49 +0,0 @@ -プレゼンターへの設定の受け渡し -*************** - -.[perex] -オートワイヤリングでは自動的に渡せない、オブジェクトでない引数(デバッグモードを示すフラグ、ディレクトリのパスなど)をプレゼンターに渡す必要がありますか。その答えは、それらを専用の `Settings` オブジェクトにまとめることです。 - -`Settings` サービスは、動いているアプリケーションについての情報をプレゼンターに届ける、とても単純ながら効果的な方法です。その具体的な構造は、あなたの必要に完全に委ねられています。例を挙げます。 - -```php -namespace App; - -class Settings -{ - public function __construct( - // PHP 8.1 以降は readonly が使えます - public bool $debugMode, - public string $appDir, - // など - ) {} -} -``` - -設定での登録の例です。 - -```neon -services: - - App\Settings( - %debugMode%, - %appDir%, - ) -``` - -このサービスが提供する情報をプレゼンターが必要とするなら、コンストラクタでそれを要求するだけです。 - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private App\Settings $settings, - ) {} - - public function renderDefault() - { - if ($this->settings->debugMode) { - // ... - } - } -} -``` diff --git a/best-practices/ja/post-links.texy b/best-practices/ja/post-links.texy deleted file mode 100644 index 535c45a8cd..0000000000 --- a/best-practices/ja/post-links.texy +++ /dev/null @@ -1,56 +0,0 @@ -POST リンクの正しい使い方 -*************** - -.[perex] -ウェブアプリケーション、とくに管理画面では、サーバーの状態を変える操作を HTTP の GET メソッドで行わない、という基本の原則を守るべきです。名前が示すとおり、GET はデータの取得にだけ使うもので、変更に使うものではありません。レコードの削除のような操作には POST メソッドのほうが適しています。理想をいえば DELETE メソッドですが、JavaScript なしでは呼び出せません。だからこそ、こうした操作には歴史的に POST が使われてきました。 - -実際にはどう実装すればよいでしょうか。次の簡単な工夫を使います。レイアウトテンプレートの先頭に、ID が `postForm` の補助的なフォームを作ります。削除ボタンなどの操作では、このフォームを使います。 - -```latte .{file:@layout.latte} -<form method="post" id="postForm"></form> -``` - -このフォームのおかげで、通常の `<a>` リンクの代わりに `<button>` を使えます。このボタンは普通のリンクのように見えるようスタイルを当てられます。たとえば Bootstrap の CSS フレームワークには `btn btn-link` クラスがあり、ボタンをほかのリンクと見分けがつかないようにできます。`form="postForm"` 属性を使って、ボタンを用意した補助フォームに結びつけます。 - -```latte .{file:admin.latte} -<table> - <tr n:foreach="$posts as $post"> - <td>{$post->title}</td> - <td> - <button class="btn btn-link" form="postForm" formaction="{link delete, $post->id}">削除</button> - <!-- <a n:href="delete, $post->id">削除</a> の代わり --> - </td> - </tr> -</table> -``` - -このボタンをクリックすると `delete` アクションが呼ばれます。リクエストが POST メソッドでのみ、しかも同じドメインから来たものだけ受け付けられるようにするには(CSRF 攻撃への実効的な防御になります)、`#[Requires]` アトリビュートを使います。 - -```php .{file:AdminPresenter.php} -use Nette\Application\Attributes\Requires; - -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST', sameOrigin: true)] - public function actionDelete(int $id): void - { - $this->facade->deletePost($id); // レコードを削除する架空のコード - $this->redirect('default'); - } -} -``` - -このアトリビュートは Nette Application 3.2 から使えます。その機能について詳しくは [#Requires アトリビュートの使い方 |attribute-requires]のページをご覧ください。 - -`actionDelete()` アクションの代わりに `handleDelete()` シグナルを使っていた場合、`sameOrigin: true` を指定する必要はありません。シグナルではこの保護が既定で有効だからです。 - -```php .{file:AdminPresenter.php} -#[Requires(methods: 'POST')] -public function handleDelete(int $id): void -{ - $this->facade->deletePost($id); - $this->redirect('this'); -} -``` - -この方法はアプリケーションの安全性を高めるだけでなく、ウェブの適切な標準と作法に沿うことにもつながります。状態を変える操作に POST メソッドを使えば、より堅牢で安全なアプリケーションになります。 diff --git a/best-practices/ja/presenter-traits.texy b/best-practices/ja/presenter-traits.texy deleted file mode 100644 index 85e91e8a56..0000000000 --- a/best-practices/ja/presenter-traits.texy +++ /dev/null @@ -1,47 +0,0 @@ -トレイトによるプレゼンターの構成 -**************** - -.[perex] -同じ機能を複数のプレゼンターで実装する必要がある場合(ユーザーのログイン確認など)、共通の祖先にコードを置くのがよくあるやり方です。もうひとつの選択肢が、目的をひとつに絞った[トレイト |nette:introduction-to-object-oriented-programming#トレイト]を作ることです。 - -トレイトを使う利点は、PHP が多重継承に対応していないこともあり、各プレゼンターが本当に必要なトレイトだけを取り込めることです。 - -これらのトレイトは、プレゼンターのインスタンスが作られるときに[すべての inject メソッド |inject-method-attribute#inject*() メソッド]が順に呼ばれることを活かせます。使うすべてのトレイトとプレゼンター自身のあいだで、各 inject メソッドの名前が重ならないようにするだけです。 - -トレイトは [onStartup や onRender |application:presenters#イベント]のイベントに初期化のコードを結びつけられます。 - -例を挙げます。 - -```php -trait RequireLoggedUser -{ - public function injectRequireLoggedUser(): void - { - $this->onStartup[] = function () { - if (!$this->getUser()->isLoggedIn()) { - $this->redirect('Sign:in', $this->storeRequest()); - } - }; - } -} - -trait StandardTemplateFilters -{ - public function injectStandardTemplateFilters(TemplateBuilder $builder): void - { - $this->onRender[] = function () use ($builder) { - $builder->setupTemplate($this->template); - }; - } -} -``` - -あとはプレゼンターがこれらのトレイトを使うだけです。 - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - use StandardTemplateFilters; - use RequireLoggedUser; -} -``` diff --git a/best-practices/ja/pretty-urls.texy b/best-practices/ja/pretty-urls.texy deleted file mode 100644 index 2ecc54d02f..0000000000 --- a/best-practices/ja/pretty-urls.texy +++ /dev/null @@ -1,204 +0,0 @@ -スラッグを使った読みやすい URL -***************** - -.[perex] -`/article/123-how-to-bake-bread` のような URL は `/article/123` より見栄えがよく、ユーザーにも検索エンジンにもページの内容を伝えてくれます。このガイドでは、テンプレートに一切手を触れずにルーターだけでそれを生成する方法と、すべての訪問者が正規の URL にたどり着くようにする方法を紹介します。 - - -なぜ URL にスラッグを入れるのか -================== - -2 つのアドレスを比べてみましょう。 - -``` -/article/123 -/article/123-how-to-bake-bread -``` - -2 つめは、クリックした先に何があるのかをユーザー(そして Google)に伝えます。SEO に良く、チャットやメールの中でリンクが読みやすくなり、アドレスバーにも意味が生まれます。 - -ただしスラッグは本当の識別子ではありません。ページを決めるのは ID です。スラッグはアプリケーションがタイトルから作る飾りです。タイトルが変われば、スラッグも変わるべきです。そして誰かが URL を手で書き換えたり、古いリンクをたどったりしても、アプリケーションは正しいページを見つけられるべきです。 - - -目標 -=== - -次のすべてを扱えるルートがほしいとします。 - -``` -/article/123 → 記事 123 を開き、正規の URL にリダイレクト -/article/123-how-to-bake-bread → 記事 123 を直接開く -/article/123-anything-someone-typed → 記事 123 を開き、正規の URL にリダイレクト -/article/ → 404(ID がない) -``` - -そしてアプリケーション全体のすべての `n:href` と `link()` の呼び出しが、**テンプレートを 1 行も書き換えずに**自動的に `/article/123-how-to-bake-bread` を生むようにしたいのです。 - - -ルートのマスク -======= - -仕掛けは、角かっこを使ってマスクの中でスラッグを**省略可能**にすることです。 - -```php -$router->addRoute('article/<id [0-9]+>[-<slug>]', 'Article:detail'); -``` - -マスク `[-<slug>]` は「ID のあとにハイフンとスラッグが続くかもしれないが、必須ではない」という意味です。このルートは `/article/123` も `/article/123-anything` も受け付けます。 - -パラメータ `<slug>` について一言。既定では**スラッシュ以外の**任意の文字に一致します。まさに望みどおりです。`<slug .+>` と書くとスラッシュにも一致するので、`/article/123-something/else` が `/` を含むひとつのスラッグとして解析されてしまいます。本当に必要でない限り、既定の `<slug>` のままにしてください。 - -ここまでで URL は正しく解析されますが、生成されるリンクにはスラッグが入りません。次はスラッグを埋める方法をルートに教えます。 - - -テンプレートに触れずにスラッグを生成する -==================== - -これが決め手となるやり方です。既存の `n:href="Article:detail, $id"` の呼び出しは、アプリケーション全体でそのまま動き続けます。ルーターが自分でタイトルを調べてくれるのです。 - -そのために、空文字列のキーの下に**一般フィルタ**を置きます。すべてのパラメータを一度に見られるので、スラッグを足せます。 - -```php -use Nette\Routing\Route; -use Nette\Utils\Strings; - -$router->addRoute('article/<id [0-9]+>[-<slug>]', [ - 'presenter' => 'Article', - 'action' => 'detail', - '' => [ - Route::FilterOut => function (array $params) use ($slugProvider): array { - if (isset($params['id']) && empty($params['slug'])) { - $params['slug'] = $slugProvider->getSlug((int) $params['id']); - } - return $params; - }, - ], -]); -``` - -`FilterOut` は、ルーターが URL を**生成する**たびに走ります。スラッグが渡されていなければ、フィルタがタイトルを調べて足します。 - -たったひとつのルート定義を変えるだけで、アプリケーション全体にスラッグを導入できます。あらゆるテンプレートのあらゆるリンクが、自動的に `/article/123-how-to-bake-bread` を出すようになります。grep も、テンプレート探しも、見落としもありません。 - - -検索結果をキャッシュする -============ - -リンク 1 本につき DB のクエリが 1 回走りますが、ふつうのページにはリンクがたくさんあります。一覧、パンくず、「最近見たもの」、関連記事などです。ひとつのリクエストの中で同じ記事 ID が複数のリンクに現れることも多く、そのたびにデータベースを叩きたくはありません。 - -リクエストごとの小さなキャッシュがこれを解決します。DB の呼び出しを小さなサービスで包みます。 - -```php -final class SlugProvider -{ - /** @var array<int, string> */ - private array $cache = []; - - public function __construct( - private Nette\Database\Explorer $db, - ) { - } - - public function getSlug(int $id): string - { - return $this->cache[$id] ??= Strings::webalize(Strings::truncate( - (string) $this->db->fetchField('SELECT title FROM article WHERE id = ?', $id), - 100, '' - )); - } -} -``` - -これで十分です。リクエストごと、一意な ID ごとに DB へのアクセスは 1 回です。 - - -テンプレートからタイトルを渡す(任意の近道) -====================== - -タイトルがすでにテンプレートの手元にあるなら、DB の検索をまるごと省けます。タイトルを名前付きパラメータとして渡します。 - -```latte -<a n:href="Article:detail, $article->id, slug => $article->title">{$article->title}</a> -``` - -…そして、そのタイトルを URL に安全な文字列に変えるパラメータごとの `FilterOut` を足します。 - -```php -$router->addRoute('article/<id [0-9]+>[-<slug>]', [ - 'presenter' => 'Article', - 'action' => 'detail', - 'slug' => [ - Route::FilterOut => fn($title) => Strings::webalize(Strings::truncate($title, 100, '')), - ], - '' => [/* 上で作った検索のフォールバック */], -]); -``` - -2 つのフィルタは協調します。まず一般フィルタが走り、スラッグがすでに渡されたタイトルで埋まっているのを見て DB の検索を飛ばします。次にパラメータごとの `FilterOut` が、そのタイトルをきちんとしたスラッグに変えます。タイトルを渡さないテンプレートもそのまま動きます。一般フィルタがスラッグの空を見て、検索の経路を通るからです。 - -これは効果のある場所(1 リクエストで何百回も描かれる大きな一覧)でだけ使ってください。アプリケーションのほとんどの場所では、キャッシュ付きの検索で十分に速いです。 - - -正規化: 正しい URL へのリダイレクト -===================== - -これで `/article/123-how-to-bake-bread` を生成できるようになりましたが、ルートは相変わらず `/article/123` や `/article/123-anything-someone-wrote` も受け付けます。これは意図的です。短い URL がほしく(後述します)、古いリンクや手打ちのリンクも動き続けてほしいからです。しかし検索エンジンに、同じ記事を複数のアドレスで登録されたくはありません。 - -その答えが[正規化 |application:presenters#正規化]です。ユーザーが正規でない URL でやってきたら、アプリケーションが 301 で正しい URL にリダイレクトします。`canonicalize()` メソッドがこれを担当します。 - -```php -public function actionDetail(int $id, ?string $slug = null): void -{ - $article = $this->facade->getArticle($id); - if (!$article) { - $this->error(); - } - - // 同じ FilterOut を通して正規の URL を生成し、 - // 現在の URL と違えば HTTP 301 でリダイレクトします - $this->canonicalize('detail', ['id' => $id]); - - $this->template->article = $article; -} -``` - -`canonicalize()` は `link()` と同じやり方で正規の URL を生成し(つまり同じ `FilterOut` を通ります)、現在の URL と比べます。違えば HTTP 301 でリダイレクトします。訪問者は正しい URL にたどり着き、検索エンジンは正規の版だけを見ます。 - - -スラッグの見た目を決める場所はひとつ -================== - -`Strings::webalize(Strings::truncate(..., 100, ''))` の呼び出しがただ 1 か所、`SlugProvider`(あるいはパラメータごとの `FilterOut`)の中にあることに注目してください。同じロジックが、テンプレートのリンク、`redirect()` の URL、`canonicalize()` の正規形を生みます。 - -あとから規則を変えたくなったら(長さの上限を変える、翻字を変える、余分な文字を落とす)、1 行を変えるだけです。そうしていなければ、`redirect()` が `/article/123-how-to-bake-bread` を生む一方で `canonicalize()` は `/article/123-how-to-bake-bre` を期待し(どこかで別の `truncate` の長さが使われたせいで)、アプリケーションがリダイレクトのループに陥る危険があります。 - - -おまけ: 短い URL も動き続ける -================== - -スラッグは省略可能なので、スラッグのないアドレスも動きます。 - -``` -/article/123 -``` - -これは次のような場面で役立ちます。 -- **QR コード** - URL が短いほどコードが粗くなり、読み取りやすくなります -- **SMS やチャット** - ツイートに収まり、見た目もすっきりします -- **印刷物** - 短い URL は打ち込むのが速いです - -ユーザーがそうした URL を開くと、`canonicalize()` がスラッグ付きの完全な版に 301 でリダイレクトするので、検索エンジンは正規形だけを見ます。短さと SEO を同時に手に入れられます。 - - -まとめ -=== - -- マスク `<id>[-<slug>]` でスラッグを省略可能にします。既定の `<slug>` は `/` に一致しません。スラッグにスラッシュを入れたいときだけ `<slug .+>` を使ってください。 -- `''` のキーの下の一般 `FilterOut` が ID からタイトルを調べます。**アプリケーションのどこでもテンプレートを変える必要はありません**。 -- 検索はリクエストごとの小さなキャッシュで包みましょう。一意な ID ごとに DB のクエリ 1 回で十分です。 -- 必要ならパラメータごとの `FilterOut` で、テンプレートからタイトルを直接渡して検索を飛ばせます。 -- アクションの中の `$this->canonicalize()` が、正規でない URL を HTTP 301 で正しい URL にリダイレクトします。 -- スラッグの作り方(`webalize` + `truncate`)は 1 か所にあります。一度変えれば、どこにでも効きます。 -- ID だけの短い URL も動き続けるので、QR コードや SMS に便利です。 - -フィルタと正規化については、[ルーティング |application:routing#一般のフィルタ]と[プレゼンター |application:presenters#正規化]のドキュメントで詳しく扱っています。 diff --git a/best-practices/ja/restore-request.texy b/best-practices/ja/restore-request.texy deleted file mode 100644 index 8a0551c2ec..0000000000 --- a/best-practices/ja/restore-request.texy +++ /dev/null @@ -1,62 +0,0 @@ -前のページに戻るには? -*********** - -.[perex] -ユーザーがフォームに入力している最中にログインのセッションが切れたら、どうなるでしょうか。データを失わないよう、ログインページにリダイレクトする前に現在のリクエスト(フォームのデータを含みます)をセッションに保存できます。Nette では、これが驚くほど簡単です。 - -現在のリクエストは `storeRequest()` メソッドでセッションに保存できます。このメソッドは、保存したリクエストの一意な識別子(短い文字列)を返します。現在のプレゼンターの名前、そのビュー、パラメータが保存されます。リクエストの一部としてフォームが送信されていた場合は、フィールドに入力された値(アップロードされたファイルを除きます)も保存されます。 - -リクエストは `restoreRequest($key)` メソッドで復元します。先ほど得た識別子を渡してください。このメソッドはユーザーをもとのプレゼンターとビューに戻します。ただし保存されたリクエストにフォームの送信が含まれていた場合、`restoreRequest()` はリダイレクトの代わりに `forward()` メソッドを使います。以前に入力された値をフォームに戻し、もう一度描画できるようにします。おかげでユーザーは、入力したデータを失わずにフォームを再送信できます。 - -重要なのは、`restoreRequest()` が、新しくログインしたユーザーがもとのフォームを送信したユーザーと同じかどうかを確認する点です。別のユーザーなら、保存されたリクエストは復元されず、メソッドは何もしません。安全性が高まります。 - -例で見てみましょう。データを編集する `AdminPresenter` があるとします。その `startup()` メソッドはユーザーがログインしているかを確かめます。していなければ `SignPresenter` にリダイレクトします。同時に `storeRequest()` で現在のリクエストを保存し、そのキー(`$backlink`)を `SignPresenter` に渡します。 - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - protected function startup() - { - parent::startup(); - - if (!$this->user->isLoggedIn()) { - $this->redirect('Sign:in', ['backlink' => $this->storeRequest()]); - } - } -} -``` - -`SignPresenter` にはログインフォームのほかに、キーを保持する永続パラメータ `$backlink` があります。パラメータが永続なので、ログインフォームを送信したあとも値が保たれます。 - - -```php -use Nette\Application\Attributes\Persistent; - -class SignPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $backlink = ''; - - protected function createComponentSignInForm() - { - $form = new Nette\Application\UI\Form; - // ... フォームの項目を追加します ... - $form->onSuccess[] = $this->signInFormSuceeded(...); - return $form; - } - - private function signInFormSuceeded($form) - { - // ... ここでユーザーをログインさせます ... - - $this->restoreRequest($this->backlink); - $this->redirect('Admin:'); - } -} -``` - -保存したリクエストのキー(`$this->backlink`)を `restoreRequest()` メソッドに渡します。すると、ユーザーをもとのプレゼンターとビューにリダイレクト(または forward)します。 - -ただしキーが正しくない場合(たとえばセッションから失効している場合)、メソッドは何もしません。ですから、続く `$this->redirect('Admin:')` の呼び出しがフォールバックとして働き、`AdminPresenter` のような既定のページにリダイレクトします。 - -{{priority: -1}} diff --git a/best-practices/meta.json b/best-practices/meta.json deleted file mode 100644 index 0967ef424b..0000000000 --- a/best-practices/meta.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/best-practices/pl/@home.texy b/best-practices/pl/@home.texy deleted file mode 100644 index 9019befb4e..0000000000 --- a/best-practices/pl/@home.texy +++ /dev/null @@ -1,69 +0,0 @@ -Poradniki i dobre praktyki -************************** - -.[perex] -Poradniki, rozwiązania typowych zadań i dobre praktyki dla Nette. - - -<div class=documentation> -<div> - - -Nette Application ------------------ -- [Metody i atrybuty inject |inject-method-attribute] -- [Składanie presenterów z traitów |presenter-traits] -- [Przekazywanie ustawień do presenterów |passing-settings-to-presenters] -- [Jak przywrócić żądanie |restore-request] -- [Stronicowanie wyników z bazy danych |pagination] -- [Dynamiczne snippety |dynamic-snippets] -- [Jak używać atrybutu #Requires |attribute-requires] -- [Jak poprawnie używać odnośników POST |post-links] -- [Przyjazne adresy URL ze slugami |pretty-urls] - -</div> -<div> - - -Formularze ----------- -- [Ponowne wykorzystanie formularzy |form-reuse] -- [Formularz do tworzenia i edycji rekordów |creating-editing-form] -- [Stwórzmy formularz kontaktowy |lets-create-contact-form] -- [Zależne selectboxy |https://blog.nette.org/pl/dependent-selectboxes-elegantly-in-nette-and-pure-js] - -</div> -<div> - - -Ogólne ------- -- [Jak wczytać plik konfiguracyjny |bootstrap:] -- [Jak pisać mikrostrony |microsites] -- [Dlaczego Nette używa dla stałych notacji PascalCase? |https://blog.nette.org/pl/for-less-screaming-in-the-code] -- [Dlaczego Nette nie używa przyrostka Interface? |https://blog.nette.org/pl/prefixes-and-suffixes-do-not-belong-in-interface-names] -- [Composer: wskazówki użycia |composer] -- [Wprowadzenie do programowania obiektowego |nette:introduction-to-object-oriented-programming] - -</div> -<div> - - -Przykładowe rozwiązania ------------------------ -- [Nette examples |https://github.com/nette-examples] -- [Doctrine & Nette |https://contributte.org/nettrine/] -- [Contributte examples |https://contributte.org/examples.html] -- [Doctrine ORM Website |https://github.com/MinecordNetwork/Website] -- [Quick start |quickstart:] - -</div> -<div> - - -Wideo ------ -Setki nagrań ze spotkań Last Saturday i filmów o Nette znajdziesz w jednym miejscu na "kanale YouTube Nette Framework":https://www.youtube.com/user/NetteFramework. - -</div> -</div> diff --git a/best-practices/pl/@left-menu.texy b/best-practices/pl/@left-menu.texy deleted file mode 100644 index 7fd03c1ea8..0000000000 --- a/best-practices/pl/@left-menu.texy +++ /dev/null @@ -1,34 +0,0 @@ -Poradniki i dobre praktyki -************************** -- [Przegląd |@home] - -Nette Application -***************** -- [Metody i atrybuty inject |inject-method-attribute] -- [Składanie presenterów z traitów |presenter-traits] -- [Przekazywanie ustawień do presenterów |passing-settings-to-presenters] -- [Jak przywrócić żądanie |restore-request] -- [Stronicowanie wyników z bazy danych |pagination] -- [Dynamiczne snippety |dynamic-snippets] -- [Jak używać atrybutu #Requires |attribute-requires] -- [Jak poprawnie używać odnośników POST |post-links] -- [Przyjazne adresy URL ze slugami |pretty-urls] - -Formularze -********** -- [Ponowne wykorzystanie formularzy |form-reuse] -- [Formularz do tworzenia i edycji rekordów |creating-editing-form] -- [Stwórzmy formularz kontaktowy |lets-create-contact-form] - -Ogólne -****** -- [Jak pisać mikrostrony |microsites] -- [Composer: wskazówki użycia |composer] - - -Dalsza lektura -************** -- [Dokumentacja Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Rozwiązywanie problemów |nette:troubleshooting] diff --git a/best-practices/pl/@meta.texy b/best-practices/pl/@meta.texy deleted file mode 100644 index d812cd8006..0000000000 --- a/best-practices/pl/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Przewodniki i dobre praktyki}} diff --git a/best-practices/pl/attribute-requires.texy b/best-practices/pl/attribute-requires.texy deleted file mode 100644 index 8c854f39ab..0000000000 --- a/best-practices/pl/attribute-requires.texy +++ /dev/null @@ -1,177 +0,0 @@ -Jak używać atrybutu `#[Requires]` -********************************* - -.[perex] -Pisząc aplikację webową, często napotykasz potrzebę ograniczenia dostępu do niektórych jej części. Może chcesz, żeby pewne żądania mogły przesyłać dane tylko formularzem (czyli metodą POST) albo żeby były dostępne wyłącznie dla wywołań AJAX-owych. W Nette Framework 3.2 pojawiło się nowe narzędzie, które pozwala takie ograniczenia ustawiać elegancko i przejrzyście: atrybut `#[Requires]`. - -Atrybut to specjalny znacznik w PHP, który dodajesz przed definicją klasy albo metody. Ponieważ w istocie jest to klasa, żeby poniższe przykłady działały, musisz dodać klauzulę `use`: - -```php -use Nette\Application\Attributes\Requires; -``` - -Atrybutu `#[Requires]` możesz użyć na samej klasie presentera oraz na tych metodach: - -- `action<Action>()` -- `render<View>()` -- `handle<Signal>()` -- `createComponent<Name>()` - -Dwie ostatnie metody dotyczą także komponentów, więc możesz atrybutu używać również w nich. - -Jeśli warunki podane w atrybucie nie są spełnione, zostaje wywołany błąd HTTP 4xx. - - -Metody HTTP ------------ - -Możesz określić, które metody HTTP (jak GET, POST itd.) są dozwolone przy dostępie. Na przykład jeśli chcesz zezwolić na dostęp tylko przez wysłanie formularza, ustaw: - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST')] - public function actionDelete(int $id): void - { - } -} -``` - -Dlaczego do akcji zmieniających stan używać POST zamiast GET i jak to robić? [Przeczytaj poradnik |post-links]. - -Możesz podać jedną metodę albo tablicę metod. Szczególnym przypadkiem jest wartość `'*'`, która zezwala na wszystkie metody, czego presentery [ze względów bezpieczeństwa domyślnie nie dopuszczają |application:presenters#Kontrola metody HTTP]. - - -Wywołania AJAX --------------- - -Jeśli chcesz, żeby presenter albo metoda były dostępne tylko dla żądań AJAX-owych, użyj: - -```php -#[Requires(ajax: true)] -class AjaxPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -To samo pochodzenie -------------------- - -Dla zwiększenia bezpieczeństwa możesz wymagać, żeby żądanie pochodziło z tej samej domeny. Zapobiega to [podatności CSRF |nette:vulnerability-protection#Cross-Site Request Forgery (CSRF)]: - -```php -#[Requires(sameOrigin: true)] -class SecurePresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Dla metod `handle<Signal>()` dostęp z tej samej domeny jest wymagany automatycznie. Jeśli więc chcesz zezwolić na dostęp z dowolnej domeny, podaj: - -```php -#[Requires(sameOrigin: false)] -public function handleList(): void -{ -} -``` - - -Dostęp przez forward --------------------- - -Czasem przydaje się ograniczyć dostęp do presentera tak, żeby był dostępny wyłącznie pośrednio, na przykład metodami `forward()` albo `switch()` z innego presentera. W ten sposób chronione są na przykład error-presentery, żeby nie dało się ich wywołać z URL: - -```php -#[Requires(forward: true)] -class ForwardedPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -W praktyce często trzeba oznaczyć pewne widoki, do których można dotrzeć tylko na podstawie logiki w presenterze. Znowu po to, żeby nie dało się 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 akcje ---------------- - -Możesz też ograniczyć pewien kod, na przykład tworzenie komponentu, tak żeby był dostępny wyłącznie dla konkretnych akcji w presenterze: - -```php -class EditDeletePresenter extends Nette\Application\UI\Presenter -{ - #[Requires(actions: ['add', 'edit'])] - public function createComponentPostForm() - { - } -} -``` - -W przypadku jednej akcji nie trzeba pisać tablicy: `#[Requires(actions: 'default')]` - - -Własne atrybuty ---------------- - -Jeśli chcesz używać atrybutu `#[Requires]` wielokrotnie z tymi samymi ustawieniami, możesz utworzyć własny atrybut, który dziedziczy po `#[Requires]` i konfiguruje go zgodnie z Twoimi potrzebami. - -Na przykład `#[SingleAction]` zezwala na dostęp tylko przez 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 -{ -} -``` - -Albo `#[RestMethods]` zezwoli na dostęp wszystkimi metodami HTTP używanymi w 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 -{ -} -``` - - -Podsumowanie ------------- - -Atrybut `#[Requires]` daje Ci dużą elastyczność i kontrolę nad tym, w jaki sposób udostępniane są Twoje strony. Za pomocą prostych, a zarazem potężnych reguł możesz zwiększyć bezpieczeństwo i poprawne działanie swojej aplikacji. Jak widzisz, używanie atrybutów w Nette może nie tylko uprościć Twoją pracę, ale też ją zabezpieczyć. diff --git a/best-practices/pl/composer.texy b/best-practices/pl/composer.texy deleted file mode 100644 index 377fa24df7..0000000000 --- a/best-practices/pl/composer.texy +++ /dev/null @@ -1,282 +0,0 @@ -Composer: wskazówki użycia -************************** - -<div class=perex> - -Composer to narzędzie do zarządzania zależnościami w PHP. Pozwala zadeklarować biblioteki, od których zależy Twój projekt, a następnie sam je instaluje i aktualizuje. Dowiemy się: - -- jak zainstalować Composer -- jak używać go w nowym albo istniejącym projekcie - -</div> - - -Instalacja -========== - -Composer to wykonywalny plik `.phar`, który pobierasz i instalujesz w następujący sposób. - - -Windows -------- - -Użyj oficjalnego instalatora [Composer-Setup.exe|https://getcomposer.org/Composer-Setup.exe]. - - -Linux, macOS ------------- - -Wystarczą 4 polecenia, które możesz skopiować z [tej strony |https://getcomposer.org/download/]. - -Dodatkowo, kopiując go do katalogu znajdującego się w systemowym `PATH`, sprawisz, że Composer będzie dostępny globalnie: - -```shell -$ mv ./composer.phar ~/bin/composer # albo /usr/local/bin/composer -``` - - -Użycie w projekcie -================== - -Żeby zacząć używać Composera w swoim projekcie, wystarczy plik `composer.json`. Opisuje on zależności Twojego projektu i może zawierać także inne metadane. Najprostszy `composer.json` może wyglądać tak: - -```js -{ - "require": { - "nette/database": "^3.0" - } -} -``` - -Mówimy tu, że nasza aplikacja (albo biblioteka) wymaga pakietu `nette/database` (nazwa pakietu składa się z nazwy dostawcy i nazwy projektu) i że chce wersję odpowiadającą warunkowi `^3.0` (czyli najnowszą wersję 3). - -Mając więc plik `composer.json` w katalogu głównym projektu, uruchom: - -```shell -composer update -``` - -Composer pobierze Nette Database do katalogu `vendor/`. Utworzy też plik `composer.lock`, który zawiera informacje o tym, jakie dokładnie wersje bibliotek zainstalował. - -Composer wygeneruje plik `vendor/autoload.php`. Wystarczy go dołączyć i możesz zacząć używać klas z bibliotek bez żadnej dodatkowej pracy: - -```php -require __DIR__ . '/vendor/autoload.php'; - -$db = new Nette\Database\Connection('sqlite::memory:'); -``` - - -Aktualizacja pakietów do najnowszych wersji -=========================================== - -Do aktualizacji używanych bibliotek do najnowszych wersji zgodnych z warunkami zdefiniowanymi w `composer.json` służy polecenie `composer update`. Na przykład przy zależności `"nette/database": "^3.0"` zainstaluje najnowszą wersję 3.x.x, ale nie wersję 4. - -Żeby zaktualizować warunki w pliku `composer.json`, na przykład na `"nette/database": "^4.1"`, i pozwolić na instalację najnowszej wersji, użyj polecenia `composer require nette/database`. - -Żeby zaktualizować wszystkie używane pakiety Nette, musiałbyś wypisać je wszystkie w wierszu poleceń, np.: - -```shell -composer require nette/application nette/forms latte/latte tracy/tracy ... -``` - -To niepraktyczne. Użyj więc prostego skryptu "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff, który zrobi to za Ciebie: - -```shell -php composer-frontline.php -``` - - -Tworzenie nowego projektu -========================= - -Nowy projekt Nette utworzysz jednym poleceniem: - -```shell -composer create-project nette/web-project nazwa-projektu -``` - -Zamiast `nazwa-projektu` wpisz nazwę katalogu dla swojego projektu i wykonaj polecenie. Composer pobierze z GitHuba repozytorium `nette/web-project`, które zawiera już plik `composer.json`, a następnie zainstaluje sam Nette Framework. Pozostaje już tylko [ustawić uprawnienia do zapisu |nette:troubleshooting#Ustawienie uprawnień do katalogów] dla katalogów `temp/` i `log/` i projekt powinien ożyć. - -Jeśli wiesz, na jakiej wersji PHP projekt będzie hostowany, koniecznie ją [ustaw |#Wersja PHP]. - - -Wersja PHP -========== - -Composer zawsze instaluje wersje pakietów zgodne z wersją PHP, której aktualnie używasz (konkretnie z wersją PHP używaną w wierszu poleceń przy uruchamianiu Composera). To nie musi być ta sama wersja, której używa Twój hosting. Dlatego bardzo ważne jest, żeby dodać do pliku `composer.json` informację o wersji PHP na hostingu. Wtedy zainstalują się tylko wersje pakietów zgodne z hostingiem. - -Na przykład żeby ustawić, że projekt będzie działać na PHP 8.2.3, użyj polecenia: - -```shell -composer config platform.php 8.2.3 -``` - -Wersja zostanie zapisana w pliku `composer.json` w ten sposób: - -```js -{ - "config": { - "platform": { - "php": "8.2.3" - } - } -} -``` - -Numer wersji PHP podaje się jednak w pliku jeszcze w innym miejscu, w sekcji `require`. Podczas gdy pierwszy numer określa wersję, dla której instalowane są pakiety, drugi mówi o wersji, dla której napisana jest sama aplikacja. Na jej podstawie na przykład PhpStorm ustawia *PHP language level*. (Oczywiście nie ma sensu, żeby te wersje się różniły, więc podwójny zapis jest przeoczeniem.) Tę wersję ustawisz poleceniem: - -```shell -composer require php 8.2.3 --no-update -``` - -Albo bezpośrednio w pliku `composer.json`: - -```js -{ - "require": { - "php": "8.2.3" - } -} -``` - - -Ignorowanie wersji PHP -====================== - -Pakiety zwykle podają zarówno najniższą wersję PHP, z którą są zgodne, jak i najwyższą wersję, z którą były testowane. Jeśli zamierzasz użyć jeszcze nowszej wersji PHP, na przykład w celach testowych, Composer odmówi instalacji takiego pakietu. Rozwiązaniem jest opcja `--ignore-platform-req=php+`, która sprawia, że Composer ignoruje górne limity wymaganej wersji PHP. - - -Fałszywe komunikaty -=================== - -Przy aktualizacji pakietów albo zmianie numerów wersji czasem dochodzi do konfliktów. Jeden pakiet ma wymagania kolidujące z innym i tak dalej. Composer jednak czasem wypisuje fałszywe komunikaty. Zgłasza konflikt, który w rzeczywistości nie istnieje. W takiej sytuacji pomaga usunięcie pliku `composer.lock` i ponowna próba. - -Jeśli komunikat o błędzie nie znika, jest prawdziwy i trzeba go przeczytać, żeby zrozumieć, co i jak zmodyfikować. - - -Packagist.org - globalne repozytorium -===================================== - -[Packagist |https://packagist.org] to główne repozytorium, w którym Composer domyślnie szuka pakietów. Możesz tu również publikować własne pakiety. - - -A co, jeśli nie chcemy centralnego repozytorium ------------------------------------------------ - -Jeśli mamy w firmie wewnętrzne aplikacje albo biblioteki, których nie da się hostować publicznie, możemy utworzyć dla nich własne repozytoria. - -Więcej o repozytoriach przeczytasz w [oficjalnej dokumentacji |https://getcomposer.org/doc/05-repositories.md#repositories]. - - -Autoloading -=========== - -Kluczową cechą Composera jest to, że zapewnia autoloading wszystkich klas, które instaluje. Aktywujesz go, dołączając plik `vendor/autoload.php`. - -Composera możesz jednak użyć także do wczytywania innych klas spoza katalogu `vendor/`. Pierwsza możliwość to pozwolić Composerowi przeskanować zdefiniowane katalogi i podkatalogi, znaleźć wszystkie klasy i włączyć je do autoloadera. Osiągniesz to, ustawiając w `composer.json` `autoload > classmap`: - -```js -{ - "autoload": { - "classmap": [ - "src/", # włącza katalog src/ i jego podkatalogi - ] - } -} -``` - -Następnie po każdej zmianie musisz uruchomić polecenie `composer dumpautoload`, żeby wygenerować tablice autoloadingu na nowo. To ogromnie niewygodne. Znacznie lepiej powierzyć to zadanie [RobotLoaderowi|robot-loader:], który tę samą czynność wykonuje automatycznie w tle i o wiele szybciej. - -Druga możliwość to trzymać się [PSR-4 |https://www.php-fig.org/psr/psr-4/]. Upraszczając, to system, w którym przestrzenie nazw i nazwy klas odpowiadają strukturze katalogów i nazwom plików, np. `App\Core\RouterFactory` będzie znajdować się w pliku `/ścieżka/do/App/Core/RouterFactory.php`. Przykład konfiguracji: - -```js -{ - "autoload": { - "psr-4": { - "App\\": "app/" # przestrzeń nazw App\ jest w katalogu app/ - } - } -} -``` - -Szczegóły konfiguracji tego zachowania znajdziesz w [dokumentacji Composera |https://getcomposer.org/doc/04-schema.md#psr-4]. - - -Testowanie nowych wersji -======================== - -Chcesz przetestować nową wersję rozwojową pakietu? Oto jak. Najpierw dodaj do pliku `composer.json` tę parę opcji. Pozwoli to instalować wersje rozwojowe, ale Composer sięgnie po nie tylko wtedy, gdy żadna kombinacja wersji stabilnych nie spełni wymagań: - -```js -{ - "minimum-stability": "dev", - "prefer-stable": true, -} -``` - -Zalecamy też usunięcie pliku `composer.lock`, bo Composer czasem w niewyjaśniony sposób odmawia instalacji, a to potrafi problem rozwiązać. - -Powiedzmy, że chodzi o pakiet `nette/utils`, a nowa wersja to 4.0. Zainstalujesz ją poleceniem: - -```shell -composer require nette/utils:4.0.x-dev -``` - -Albo możesz zainstalować konkretną wersję, na przykład 4.0.0-RC2: - -```shell -composer require nette/utils:4.0.0-RC2 -``` - -Jeśli jednak od biblioteki zależy inny pakiet zablokowany na starszej wersji (np. `^3.1`), idealnym rozwiązaniem jest zaktualizowanie tego zależnego pakietu tak, żeby działał z nową wersją. Ale jeśli chcesz tylko obejść ograniczenie i zmusić Composera do zainstalowania wersji rozwojowej, udając, że to wersja starsza (np. 3.1.6), możesz użyć słowa kluczowego `as`: - -```shell -composer require nette/utils "4.0.x-dev as 3.1.6" -``` - - -Wywoływanie poleceń -=================== - -Za pośrednictwem Composera możesz wywoływać własne, wcześniej zdefiniowane polecenia i skrypty, tak jakby były natywnymi poleceniami Composera. Dla skryptów znajdujących się w katalogu `vendor/bin` nie musisz podawać tej ścieżki. - -Jako przykład zdefiniujmy w `composer.json` skrypt, który za pomocą [Nette Testera |tester:] uruchamia testy: - -```js -{ - "scripts": { - "tester": "tester tests -s" - } -} -``` - -Testy uruchomimy potem poleceniem `composer tester`. Polecenie możesz wywołać, nawet jeśli nie jesteś w katalogu głównym projektu, tylko w którymś z jego podkatalogów. - - -Wyślij podziękowanie -==================== - -Pokażemy Ci trik, którym sprawisz radość autorom open source. Możesz w prosty sposób dać gwiazdki na GitHubie bibliotekom, których używa Twój projekt. Wystarczy zainstalować bibliotekę `symfony/thanks`: - -```shell -composer global require symfony/thanks -``` - -A następnie uruchomić: - -```shell -composer thanks -``` - -Wypróbuj! - - -Konfiguracja -============ - -Composer jest ściśle zintegrowany z systemem kontroli wersji [Git |https://git-scm.com]. Jeśli nie masz zainstalowanego Gita, musisz powiedzieć Composerowi, żeby go nie używał: - -```shell -composer -g config preferred-install dist -``` diff --git a/best-practices/pl/creating-editing-form.texy b/best-practices/pl/creating-editing-form.texy deleted file mode 100644 index e5c3750b0a..0000000000 --- a/best-practices/pl/creating-editing-form.texy +++ /dev/null @@ -1,204 +0,0 @@ -Formularz do tworzenia i edycji rekordu -*************************************** - -.[perex] -Jak w Nette poprawnie zrealizować dodawanie i edycję rekordu, używając do obu tego samego formularza? - -W wielu przypadkach formularze do dodawania i edycji rekordu są takie same, różnią się może tylko etykietą przycisku. Pokażemy przykłady prostych presenterów, w których użyjemy formularza najpierw do dodania rekordu, potem do jego edycji, a na koniec połączymy oba rozwiązania. - - -Dodawanie rekordu ------------------ - -Przykład presentera służącego do dodania rekordu. Samą pracę z bazą danych zostawimy klasie `Facade`, której kod nie jest dla tego przykładu istotny. - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentRecordForm(): Form - { - $form = new Form; - - // ... dodajemy pola formularza ... - - $form->onSuccess[] = $this->recordFormSucceeded(...); - return $form; - } - - private function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // dodanie rekordu do bazy danych - $this->flashMessage('Pomyślnie dodano'); - $this->redirect('...'); - } - - public function renderAdd(): void - { - // ... - } -} -``` - - -Edycja rekordu --------------- - -Zobaczmy teraz, jak wyglądałby presenter służący do edycji rekordu: - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - private $record; - - public function __construct( - private Facade $facade, - ) { - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // weryfikujemy istnienie rekordu - || !$this->facade->isEditAllowed(/*...*/) // sprawdzamy uprawnienia - ) { - $this->error(); // błąd 404 - } - - $this->record = $record; - } - - protected function createComponentRecordForm(): Form - { - // weryfikujemy, że akcja to 'edit' - if ($this->getAction() !== 'edit') { - $this->error(); - } - - $form = new Form; - - // ... dodajemy pola formularza ... - - $form->setDefaults($this->record); // ustawiamy wartości domyślne - $form->onSuccess[] = $this->recordFormSucceeded(...); - return $form; - } - - private function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->update($this->record->id, $data); // aktualizacja rekordu - $this->flashMessage('Pomyślnie zaktualizowano'); - $this->redirect('...'); - } -} -``` - -W metodzie *action*, wywoływanej na początku [cyklu życia presentera |application:presenters#Cykl życia presentera], weryfikujemy istnienie rekordu i uprawnienia użytkownika do jego edycji. - -Rekord zapisujemy do właściwości `$record`, dzięki czemu jest dostępny w metodzie `createComponentRecordForm()` do ustawienia wartości domyślnych oraz w `recordFormSucceeded()` do odczytu ID. Alternatywnym rozwiązaniem jest ustawienie wartości domyślnych bezpośrednio w `actionEdit()` i odczytanie wartości ID (będącej częścią URL) za pomocą `getParameter('id')`: - - -```php - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - // weryfikujemy istnienie i sprawdzamy uprawnienia - ) { - $this->error(); - } - - // ustawiamy wartości domyślne formularza - $this->getComponent('recordForm') - ->setDefaults($record); - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); - // ... - } -``` - -Ale, i to powinno być **najważniejszym wnioskiem z całego kodu**, przy tworzeniu formularza musimy upewnić się, że akcja rzeczywiście jest `edit`. W przeciwnym razie weryfikacja w metodzie `actionEdit()` w ogóle by się nie odbyła! - - -Ten sam formularz do dodawania i edycji ---------------------------------------- - -Połączmy teraz oba presentery w jeden. Moglibyśmy albo rozróżniać akcję w metodzie `createComponentRecordForm()` i odpowiednio konfigurować formularz, albo możemy to zostawić bezpośrednio metodom action i pozbyć się warunku: - - -```php -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - public function actionAdd(): void - { - $form = $this->getComponent('recordForm'); - $form->onSuccess[] = $this->addingFormSucceeded(...); - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // weryfikujemy istnienie rekordu - || !$this->facade->isEditAllowed(/*...*/) // sprawdzamy uprawnienia - ) { - $this->error(); // błąd 404 - } - - $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // ustawiamy wartości domyślne - $form->onSuccess[] = $this->editingFormSucceeded(...); - } - - protected function createComponentRecordForm(): Form - { - // weryfikujemy, że akcja to 'add' albo 'edit' - if (!in_array($this->getAction(), ['add', 'edit'])) { - $this->error(); - } - - $form = new Form; - - // ... dodajemy pola formularza ... - - return $form; - } - - private function addingFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // dodanie rekordu do bazy danych - $this->flashMessage('Pomyślnie dodano'); - $this->redirect('...'); - } - - private function editingFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // aktualizacja rekordu - $this->flashMessage('Pomyślnie zaktualizowano'); - $this->redirect('...'); - } -} -``` - -{{priority: -1}} diff --git a/best-practices/pl/dynamic-snippets.texy b/best-practices/pl/dynamic-snippets.texy deleted file mode 100644 index 568db2a845..0000000000 --- a/best-practices/pl/dynamic-snippets.texy +++ /dev/null @@ -1,176 +0,0 @@ -Dynamiczne snippety -******************* - -.[perex] -Jak za pomocą AJAX-u odświeżać tylko te części strony, które faktycznie się zmieniają, na przykład poszczególne pozycje listy, przy użyciu dynamicznych snippetów Latte. - -Przy tworzeniu aplikacji dość często pojawia się potrzeba wykonywania operacji AJAX-owych na przykład na poszczególnych wierszach tabeli albo pozycjach listy. Jako przykład weźmy listę artykułów, w której zalogowany użytkownik może każdy artykuł ocenić jako "lubię to" albo "nie lubię". Kod presentera i odpowiadający mu szablon bez AJAX-u wyglądałyby mniej więcej tak (podajemy najważniejsze fragmenty; kod zakłada istnienie usługi obsługującej oceny i pobierającej artykuły, konkretna implementacja nie jest dla tego poradnika istotna): - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - $this->redirect('this'); -} - -public function handleUnlike(int $articleId): void -{ - $this->ratingService->removeLike($articleId, $this->user->id); - $this->redirect('this'); -} -``` - -Szablon: - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {if !$article->liked} - <a n:href="like!, $article->id" class=ajax>lubię to</a> - {else} - <a n:href="unlike!, $article->id" class=ajax>już tego nie lubię</a> - {/if} -</article> -``` - - -Ajaksowanie -=========== - -Dodajmy teraz do tej prostej aplikacji obsługę AJAX-u. Zmiana oceny artykułu nie jest na tyle istotna, by wymagała przekierowania całej strony, więc powinna idealnie odbywać się w tle przez AJAX. Użyjemy [skryptu obsługującego z dodatków |application:ajax#Naja] wraz z powszechną konwencją, że odnośniki AJAX-owe mają klasę CSS `ajax`. - -Ale jak konkretnie to zrealizować? Nette oferuje dwa podejścia: dynamiczne snippety i komponenty. Oba mają swoje zalety i wady, więc pokażemy sobie oba. - - -Sposób z dynamicznymi snippetami -================================ - -W terminologii Latte dynamiczny snippet oznacza szczególne użycie tagu `{snippet}`, w którym w nazwie snippetu użyta jest zmienna. Takiego snippetu nie można umieścić w szablonie gdziekolwiek: musi być otoczony statycznym (zwykłym) snippetem albo znajdować się wewnątrz `{snippetArea}`. Nasz szablon moglibyśmy zmodyfikować następująco: - - -```latte -{snippet articlesContainer} - <article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {snippet article-{$article->id}} - {if !$article->liked} - <a n:href="like!, $article->id" class=ajax>lubię to</a> - {else} - <a n:href="unlike!, $article->id" class=ajax>już tego nie lubię</a> - {/if} - {/snippet} - </article> -{/snippet} -``` - -Każdy artykuł definiuje teraz snippet, którego nazwa zawiera ID artykułu. Wszystkie te dynamiczne snippety są następnie wspólnie otoczone statycznym snippetem o nazwie `articlesContainer`. Gdybyśmy ten zewnętrzny snippet pominęli, Latte rzuciłoby wyjątek. - -Pozostaje już tylko dodać do presentera logikę przerysowywania: wystarczy przerysować statyczny wrapper. - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - if ($this->isAjax()) { - $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- niepotrzebne - } else { - $this->redirect('this'); - } -} -``` - -Analogicznie zmodyfikuj odpowiadającą metodę `handleUnlike()` i AJAX działa! - -To rozwiązanie ma jednak pewną wadę. Gdy przyjrzymy się żądaniu AJAX-owemu bliżej, odkryjemy, że choć na zewnątrz aplikacja sprawia wrażenie oszczędnej (zwraca tylko jeden snippet dla konkretnego artykułu), po stronie serwera renderuje w rzeczywistości *wszystkie* snippety. Potrzebny snippet umieszcza w payloadzie, a pozostałe odrzuca (czyli niepotrzebnie je też pobrała i wyrenderowała). - -Żeby to zoptymalizować, musimy zainterweniować tam, gdzie kolekcja `$articles` jest przekazywana do szablonu (powiedzmy w metodzie `renderDefault()`). Wykorzystamy fakt, że obsługa sygnału odbywa się przed metodami `render<Coś>`: - -```php -public function handleLike(int $articleId): void -{ - // ... - if ($this->isAjax()) { - // ... - $this->template->articles = [ - $this->db->table('articles')->get($articleId), - ]; - } else { - // ... -} - -public function renderDefault(): void -{ - if (!isset($this->template->articles)) { - $this->template->articles = $this->db->table('articles'); - } -} -``` - -Teraz przy obsłudze sygnału zamiast całej kolekcji artykułów trafia do szablonu tylko tablica z jednym istotnym artykułem: tym, który zamierzamy wyrenderować i wysłać w payloadzie do przeglądarki. Dzięki temu pętla `{foreach}` wykona się tylko raz i nie wyrenderują się żadne zbędne snippety. - - -Sposób z komponentami -===================== - -Zupełnie inne podejście omija dynamiczne snippety całkowicie. Trik polega na zamknięciu całej logiki w osobnym komponencie. Zamiast presentera obsługą oceniania zajmie się dedykowany `LikeControl`. Klasa będzie wyglądać tak (zawierałaby także metody `render`, `handleUnlike` itd.): - -```php -class LikeControl extends Nette\Application\UI\Control -{ - public function __construct( - private Article $article, - ) { - } - - public function handleLike(): void - { - $this->ratingService->saveLike($this->article->id, $this->presenter->user->id); - if ($this->presenter->isAjax()) { - $this->redrawControl(); - } else { - $this->presenter->redirect('this'); - } - } -} -``` - -Szablon komponentu: - -```latte -{snippet} - {if !$article->liked} - <a n:href="like!" class=ajax>lubię to</a> - {else} - <a n:href="unlike!" class=ajax>już tego nie lubię</a> - {/if} -{/snippet} -``` - -Naturalnie zmieni się szablon widoku i będziemy musieli dodać do presentera fabrykę. Ponieważ instancję tego komponentu utworzymy dla każdego artykułu pobranego z bazy danych, do zarządzania ich tworzeniem użyjemy klasy [Multiplier |application:Multiplier]. - -```php -protected function createComponentLikeControl() -{ - $articles = $this->db->table('articles'); - return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { - return new LikeControl($articles[$articleId]); - }); -} -``` - -Szablon widoku kurczy się do absolutnego minimum (i jest całkowicie wolny od snippetów!): - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {control "likeControl-$article->id"} -</article> -``` - -Jesteśmy prawie u celu: aplikacja będzie teraz działać z AJAX-em. I tutaj przyda się optymalizacja, bo ze względu na użycie Nette Database obsługa sygnału niepotrzebnie wczytuje z bazy wszystkie artykuły zamiast tylko tego istotnego. Zaletą jest natomiast to, że nie dochodzi do zbędnego renderowania, bo renderowana jest wyłącznie konkretna instancja komponentu. - -{{priority: -1}} diff --git a/best-practices/pl/form-reuse.texy b/best-practices/pl/form-reuse.texy deleted file mode 100644 index a723016c5d..0000000000 --- a/best-practices/pl/form-reuse.texy +++ /dev/null @@ -1,348 +0,0 @@ -Ponowne wykorzystanie formularzy w wielu miejscach -************************************************** - -.[perex] -Nette oferuje kilka sposobów, jak używać tego samego formularza w wielu miejscach bez duplikowania kodu. W tym artykule omówimy różne rozwiązania, w tym te, których powinieneś unikać. - - -Fabryka formularzy -================== - -Podstawowym podejściem do ponownego wykorzystania komponentu w wielu miejscach jest utworzenie metody albo klasy, która ten komponent tworzy. Taką metodę wywołujemy potem z różnych miejsc aplikacji. Taka metoda albo klasa nazywa się *fabryką*. Nie myl tego proszę ze wzorcem projektowym *metoda fabrykująca*, który opisuje szczególny sposób użycia fabryk i nie jest bezpośrednio związany z tym tematem. - -Jako przykład utwórzmy fabrykę budującą formularz edycyjny: - -```php -use Nette\Application\UI\Form; - -class FormFactory -{ - public function createEditForm(): Form - { - $form = new Form; - $form->addText('title', 'Tytuł:'); - // tutaj dodawane są kolejne pola formularza - $form->addSubmit('send', 'Zapisz'); - return $form; - } -} -``` - -Teraz możesz tej fabryki używać w różnych częściach aplikacji, na przykład w presenterach albo komponentach. Zrobisz to, [prosząc o nią jako o zależność |dependency-injection:passing-dependencies]. Najpierw zarejestruj klasę w pliku konfiguracyjnym: - -```neon -services: - - FormFactory -``` - -A potem użyj jej w presenterze: - - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->createEditForm(); - $form->onSuccess[] = function () { - // przetwarzanie wysłanych danych - }; - return $form; - } -} -``` - -Fabrykę formularzy możesz rozszerzyć o kolejne metody tworzące inne rodzaje formularzy, zależnie od potrzeb Twojej aplikacji. I naturalnie możemy dodać metodę tworzącą podstawowy formularz bez elementów, którą pozostałe metody potem wykorzystają: - -```php -class FormFactory -{ - public function createForm(): Form - { - $form = new Form; - return $form; - } - - public function createEditForm(): Form - { - $form = $this->createForm(); - $form->addText('title', 'Tytuł:'); - // tutaj dodawane są kolejne pola formularza - $form->addSubmit('send', 'Zapisz'); - return $form; - } -} -``` - -Metoda `createForm()` nie robi jeszcze nic szczególnie pożytecznego, ale to się wkrótce zmieni. - - -Zależności fabryki -================== - -Z czasem może się okazać, że formularze mają być wielojęzyczne. Oznacza to, że wszystkim formularzom trzeba ustawić [translator |forms:rendering#Tłumaczenie]. Żeby to osiągnąć, zmodyfikuj klasę `FormFactory` tak, by przyjmowała obiekt `Translator` jako zależność w konstruktorze i przekazywała go tworzonemu formularzowi: - -```php -use Nette\Localization\Translator; - -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function createForm(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } - - // ... -} -``` - -Ponieważ metodę `createForm()` wywołują także pozostałe metody tworzące konkretne formularze, wystarczy ustawić translator właśnie tutaj. I gotowe. Nie trzeba modyfikować kodu żadnego presentera ani komponentu, co jest znakomite. - - -Więcej klas fabryk -================== - -Alternatywnie możesz utworzyć osobne klasy fabryk dla każdego formularza, którego zamierzasz używać w aplikacji. To podejście może poprawić czytelność kodu i uprościć zarządzanie formularzami. Niech pierwotna `FormFactory` tworzy tylko podstawowy formularz z podstawową konfiguracją (na przykład wsparciem dla tłumaczeń), a dla formularza edycyjnego utwórz nową fabrykę `EditFormFactory`. - -```php -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function create(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } -} - - -// ✅ użycie kompozycji -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - // tutaj dodawane są kolejne pola formularza - $form->addSubmit('send', 'Zapisz'); - return $form; - } -} -``` - -Bardzo ważne jest, żeby relacja między klasami `FormFactory` i `EditFormFactory` była zrealizowana przez [kompozycję |nette:introduction-to-object-oriented-programming#Kompozycja], a nie przez [dziedziczenie obiektowe |nette:introduction-to-object-oriented-programming#Dziedziczenie]: - -```php -// ⛔ NIE! DZIEDZICZENIE TU NIE PASUJE -class EditFormFactory extends FormFactory -{ - public function create(): Form - { - $form = parent::create(); - $form->addText('title', 'Tytuł:'); - // tutaj dodawane są kolejne pola formularza - $form->addSubmit('send', 'Zapisz'); - return $form; - } -} -``` - -Użycie dziedziczenia byłoby tu całkowicie kontrproduktywne. Bardzo szybko napotkałbyś problemy. Na przykład gdybyś chciał dodać do metody `create()` parametry, PHP zgłosiłoby błąd, bo jej sygnatura różniłaby się od sygnatury rodzica. Albo przy przekazywaniu zależności do klasy `EditFormFactory` przez konstruktor. Doszłoby do sytuacji, którą nazywamy [constructor hell |dependency-injection:passing-dependencies#Piekło konstruktorów]. - -Generalnie lepiej preferować [kompozycję nad dziedziczeniem |dependency-injection:faq#Dlaczego kompozycja jest preferowana nad dziedziczeniem?]. - - -Obsługa formularza -================== - -Handler formularza wywoływany po udanym wysłaniu też może być częścią klasy fabryki. Działa tak, że przekazuje wysłane dane do przetworzenia warstwie modelu. Ewentualne błędy przetwarzania przekazuje [z powrotem |forms:validation#Błędy przy przetwarzaniu] do formularza. W poniższym przykładzie model reprezentuje klasa `Facade`: - -```php -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - private Facade $facade, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - $form->addText('title', 'Tytuł:'); - // tutaj dodawane są kolejne pola formularza - $form->addSubmit('send', 'Zapisz'); - $form->onSuccess[] = $this->processForm(...); - return $form; - } - - private function processForm(Form $form, array $data): void - { - try { - // przetwarzanie wysłanych danych - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - } - } -} -``` - -Samo przekierowanie zostawmy jednak presenterowi. Ten doda do zdarzenia `onSuccess` kolejny handler, który przekierowanie wykona. Dzięki temu formularza będzie można używać w różnych presenterach, a każdy z nich po sukcesie przekieruje gdzie indziej. - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditFormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->create(); - $form->onSuccess[] = function () { - $this->flashMessage('Rekord został zapisany'); - $this->redirect('Homepage:'); - }; - return $form; - } -} -``` - -To rozwiązanie wykorzystuje właściwość formularzy, zgodnie z którą jeśli na formularzu albo którymś z jego elementów zostanie wywołane `addError()`, kolejne handlery `onSuccess` nie zostaną wywołane. - - -Dziedziczenie po klasie Form -============================ - -Zbudowany formularz nie powinien być potomkiem klasy `Form`. Innymi słowy, unikaj takiego podejścia: - -```php -// ⛔ NIE! DZIEDZICZENIE TU NIE PASUJE -class EditForm extends Form -{ - public function __construct(Translator $translator) - { - parent::__construct(); - $this->addText('title', 'Tytuł:'); - // tutaj dodawane są kolejne pola formularza - $this->addSubmit('send', 'Zapisz'); - $this->setTranslator($translator); - } -} -``` - -Zamiast budować formularz w konstruktorze, użyj fabryki. - -Ważne jest, żeby zdać sobie sprawę, że klasa `Form` to przede wszystkim narzędzie do budowania formularzy, czyli *form builder*. Zbudowany formularz można uznać za jej produkt. Produkt nie jest jednak szczególnym rodzajem buildera; nie zachodzi między nimi relacja *is a*, która jest podstawą dziedziczenia. - - -Komponent z formularzem -======================= - -Zupełnie inne podejście polega na utworzeniu [komponentu |application:components], który zawiera w sobie formularz. Otwiera to nowe możliwości, na przykład renderowanie formularza w szczególny sposób, bo komponent ma własny szablon. Albo można wykorzystać sygnały do komunikacji AJAX-owej i dynamicznego wczytywania informacji do formularza, na przykład dla podpowiedzi itd. - - -```php -use Nette\Application\UI\Form; - -class EditControl extends Nette\Application\UI\Control -{ - public array $onSave = []; - - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentForm(): Form - { - $form = new Form; - $form->addText('title', 'Tytuł:'); - // tutaj dodawane są kolejne pola formularza - $form->addSubmit('send', 'Zapisz'); - $form->onSuccess[] = $this->processForm(...); - - return $form; - } - - private function processForm(Form $form, array $data): void - { - try { - // przetwarzanie wysłanych danych - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - return; - } - - // wywołanie zdarzenia - $this->onSave($this, $data); - } -} -``` - -Utwórzmy jeszcze fabrykę, która ten komponent będzie produkować. Wystarczy [zdefiniować jej interfejs |application:components#Komponenty z zależnościami]: - -```php -interface EditControlFactory -{ - function create(): EditControl; -} -``` - -I dodać go do pliku konfiguracyjnego: - -```neon -services: - - EditControlFactory -``` - -Teraz możemy poprosić o fabrykę i użyć jej w presenterze: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditControlFactory $controlFactory, - ) { - } - - protected function createComponentEditForm(): EditControl - { - $control = $this->controlFactory->create(); - - $control->onSave[] = function (EditControl $control, $data) { - $this->redirect('this'); - // albo przekierowanie na wynik edycji, np.: - // $this->redirect('detail', ['id' => $data->id]); - }; - - return $control; - } -} -``` diff --git a/best-practices/pl/inject-method-attribute.texy b/best-practices/pl/inject-method-attribute.texy deleted file mode 100644 index b21bb41ab2..0000000000 --- a/best-practices/pl/inject-method-attribute.texy +++ /dev/null @@ -1,61 +0,0 @@ -Metody i atrybuty inject -************************ - -.[perex] -Ten artykuł skupia się na różnych sposobach przekazywania zależności do presenterów we frameworku Nette. Porównamy preferowany sposób, czyli wstrzykiwanie przez konstruktor, z alternatywami w postaci metod i atrybutów `inject`. - -W przypadku presenterów, podobnie jak w innych klasach, preferowanym sposobem przekazywania zależności jest [konstruktor |dependency-injection:passing-dependencies#Przekazywanie przez konstruktor]. Jeśli jednak tworzysz wspólnego przodka, po którym dziedziczą pozostałe presentery (np. `BasePresenter`), i ten przodek również potrzebuje zależności, może pojawić się problem zwany [constructor hell |dependency-injection:passing-dependencies#Piekło konstruktorów]. Da się go obejść za pomocą alternatywnych sposobów, czyli metod i atrybutów inject (dawniej adnotacji). - - -Metody `inject*()` -================== - -Jest to forma przekazywania zależności przez [settery |dependency-injection:passing-dependencies#Przekazywanie przez setter]. Nazwy tych setterów muszą zaczynać się od przedrostka `inject`. Nette DI wywołuje tak nazwane metody automatycznie zaraz po utworzeniu instancji presentera i przekazuje im wszystkie potrzebne zależności. Dlatego muszą być zadeklarowane jako public. - -Metody `inject*()` można traktować jako rozszerzenie konstruktora rozbite na kilka metod. Dzięki temu `BasePresenter` może przyjąć zależności osobną metodą i pozostawić konstruktor do dyspozycji swoim potomkom: - -```php -abstract class BasePresenter extends Nette\Application\UI\Presenter -{ - private Foo $foo; - - public function injectBase(Foo $foo): void - { - $this->foo = $foo; - } -} - -class MyPresenter extends BasePresenter -{ - private Bar $bar; - - public function __construct(Bar $bar) - { - $this->bar = $bar; - } -} -``` - -Presenter może mieć dowolną liczbę metod `inject*()`, a każda z nich może przyjmować dowolną liczbę parametrów. Ten sposób świetnie sprawdza się także wtedy, gdy presenter jest [składany z traitów |presenter-traits], a każdy trait wymaga własnych zależności. - - -Atrybuty `Inject` -================= - -Jest to forma [wstrzykiwania do właściwości |dependency-injection:passing-dependencies#Przekazywanie przez właściwość]. Wystarczy oznaczyć właściwości, które mają zostać wstrzyknięte, a Nette DI przekaże zależności automatycznie zaraz po utworzeniu instancji presentera. Aby wstrzyknięcie było możliwe, właściwości te muszą być zadeklarowane jako public. - -Właściwości oznaczamy atrybutem: (wcześniej używano adnotacji `/** @inject */`) - -```php -use Nette\DI\Attributes\Inject; // ta linia jest ważna - -class MyPresenter extends Nette\Application\UI\Presenter -{ - #[Inject] - public Cache $cache; -} -``` - -Zaletą tego sposobu przekazywania zależności była bardzo zwięzła składnia. Odkąd jednak istnieje [constructor property promotion |https://blog.nette.org/pl/php-8-0-complete-overview-of-news#toc-constructor-property-promotion], użycie konstruktora często wydaje się prostsze. - -Z drugiej strony ten sposób cierpi na te same wady co wstrzykiwanie do właściwości w ogóle: nie mamy kontroli nad zmianami zmiennej, a sama zmienna staje się częścią publicznego interfejsu klasy, co jest generalnie niepożądane. diff --git a/best-practices/pl/lets-create-contact-form.texy b/best-practices/pl/lets-create-contact-form.texy deleted file mode 100644 index a3e08a1aa3..0000000000 --- a/best-practices/pl/lets-create-contact-form.texy +++ /dev/null @@ -1,218 +0,0 @@ -Stwórzmy formularz kontaktowy -***************************** - -.[perex] -Zobaczmy, jak w Nette utworzyć formularz kontaktowy wraz z wysyłaniem wpisanych danych e-mailem. No to do dzieła! - -Najpierw musimy utworzyć nowy projekt. Jak to zrobić, wyjaśnia strona [Pierwsze kroki |nette:installation]. Potem możemy zabrać się za tworzenie formularza. - -Najprostszym podejściem jest utworzenie [formularza bezpośrednio w presenterze |forms:in-presenter]. Możemy wykorzystać przygotowany `HomePresenter`. Dodamy do niego komponent `contactForm` reprezentujący nasz formularz. Zrobimy to, dopisując do kodu presentera metodę fabrykującą `createComponentContactForm()`, która ten komponent utworzy: - -```php -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - protected function createComponentContactForm(): Form - { - $form = new Form; - $form->addText('name', 'Imię:') - ->setRequired('Podaj swoje imię'); - $form->addEmail('email', 'E-mail:') - ->setRequired('Podaj swój e-mail'); - $form->addTextArea('message', 'Wiadomość:') - ->setRequired('Wpisz wiadomość'); - $form->addSubmit('send', 'Wyślij'); - $form->onSuccess[] = $this->contactFormSucceeded(...); - return $form; - } - - private function contactFormSucceeded(Form $form, $data): void - { - // wysłanie e-maila - } -} -``` - -Jak widzisz, utworzyliśmy dwie metody. Pierwsza z nich, `createComponentContactForm()`, tworzy nową instancję formularza. Zawiera pola na imię, e-mail i wiadomość, dodane odpowiednio metodami `addText()`, `addEmail()` i `addTextArea()`. Dodaliśmy też przycisk wysyłający. A co, jeśli użytkownik zostawi któreś pole puste? W takim razie powinniśmy go poinformować, że pole jest wymagane. Osiągnęliśmy to metodą `setRequired()`. Na koniec podpięliśmy jeszcze handler [zdarzenia |nette:glossary#Zdarzenia] `onSuccess`, które wywołuje się po udanym wysłaniu formularza. W naszym przypadku wywołuje on metodę `contactFormSucceeded`, która zajmie się przetworzeniem wysłanych danych. Za chwilę ją uzupełnimy. - -Komponent `contactForm` wyrenderujmy w szablonie `Home/default.latte`: - -```latte -{block content} -<h1>Formularz kontaktowy</h1> -{control contactForm} -``` - -Do samego wysyłania e-maila utworzymy nową klasę o nazwie `ContactFacade` i umieścimy ją w pliku `app/Model/ContactFacade.php`: - -```php -namespace App\Model; - -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $mail = new Message; - $mail->addTo('admin@example.com') // Twój e-mail - ->setFrom($email, $name) - ->setSubject('Wiadomość z formularza kontaktowego') - ->setBody($message); - - $this->mailer->send($mail); - } -} -``` - -Metoda `sendMessage()` tworzy i wysyła e-mail. Wykorzystuje do tego usługę mailera, którą otrzymuje jako zależność przez konstruktor. Przeczytaj więcej o [wysyłaniu e-maili |mail:]. - -Wróćmy teraz do presentera i uzupełnijmy metodę `contactFormSucceeded()`. Wywoła ona metodę `sendMessage()` klasy `ContactFacade` i przekaże jej dane wysłane formularzem. A skąd weźmiemy obiekt `ContactFacade`? Poprosimy o niego w konstruktorze za pomocą wstrzykiwania zależności: - -```php -use App\Model\ContactFacade; -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - public function __construct( - private ContactFacade $facade, - ) { - } - - protected function createComponentContactForm(): Form - { - // ... - } - - public function contactFormSucceeded(stdClass $data): void - { - $this->facade->sendMessage($data->email, $data->name, $data->message); - $this->flashMessage('Wiadomość została wysłana'); - $this->redirect('this'); - } -} -``` - -Po wysłaniu e-maila wyświetlimy użytkownikowi [wiadomość flash |application:components#Wiadomości flash] potwierdzającą wysłanie. A następnie przekierujemy, żeby formularz nie dało się wysłać ponownie odświeżeniem strony w przeglądarce. - - -No i jeśli wszystko jest ustawione poprawnie, powinieneś już móc wysłać e-mail ze swojego formularza kontaktowego. Gratulacje! - - -Szablon e-maila w HTML ----------------------- - -Na razie wysyła się zwykły e-mail tekstowy zawierający tylko wiadomość wysłaną formularzem. W e-mailu możemy jednak użyć HTML-a i uatrakcyjnić jego wygląd. Utworzymy dla niego szablon w Latte i zapiszemy go jako `app/Model/contactEmail.latte`: - -```latte -<html> - <title>Wiadomość z formularza kontaktowego - - -

    Imię: {$name}

    -

    E-mail: {$email}

    -

    Wiadomość: {$message}

    - - -``` - -Pozostaje zmodyfikować `ContactFacade` tak, żeby ten szablon wykorzystywała. W konstruktorze poprosimy o klasę `LatteFactory`, która potrafi utworzyć obiekt `Latte\Engine`, czyli [renderer szablonów Latte |latte:develop#Jak wyrenderować szablon]. Metodą `renderToString()` wyrenderujemy szablon do ciągu znaków. Pierwszym parametrem jest ścieżka do pliku szablonu, drugim tablica zmiennych, które mu przekazujemy. - -```php -namespace App\Model; - -use Nette\Bridges\ApplicationLatte\LatteFactory; -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $latte = $this->latteFactory->create(); - $body = $latte->renderToString(__DIR__ . '/contactEmail.latte', [ - 'email' => $email, - 'name' => $name, - 'message' => $message, - ]); - - $mail = new Message; - $mail->addTo('admin@example.com') // Twój e-mail - ->setFrom($email, $name) - ->setHtmlBody($body); - - $this->mailer->send($mail); - } -} -``` - -Wygenerowaną treść e-maila w HTML przekazujemy potem metodzie `setHtmlBody()` zamiast pierwotnej `setBody()`. Nie musimy też podawać tematu e-maila metodą `setSubject()`, bo biblioteka pobiera go automatycznie z elementu `` w szablonie. - - -Konfigurowanie --------------- - -W kodzie klasy `ContactFacade` nadal mamy zapisany na sztywno e-mail administratora `admin@example.com`. Lepiej byłoby przenieść go do pliku konfiguracyjnego. Jak to zrobić? - -Najpierw zmodyfikujemy klasę `ContactFacade` i zamiast zapisanego na sztywno ciągu z e-mailem użyjemy zmiennej przekazanej przez konstruktor: - -```php -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - private string $adminEmail, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - // ... - $mail = new Message; - $mail->addTo($this->adminEmail) - ->setFrom($email, $name) - ->setHtmlBody($body); - // ... - } -} -``` - -Drugim krokiem jest podanie wartości tej zmiennej w konfiguracji. Do pliku `app/config/services.neon` dopiszemy: - -```neon -services: - - App\Model\ContactFacade(adminEmail: admin@example.com) -``` - -I to wszystko. Jeśli pozycji w sekcji `services` jest dużo i masz wrażenie, że adres e-mail się wśród nich gubi, możemy zrobić z niego parametr. Zmodyfikujemy wpis tak: - -```neon -services: - - App\Model\ContactFacade(adminEmail: %adminEmail%) -``` - -A ten parametr zdefiniujemy w pliku `app/config/common.neon`: - -```neon -parameters: - adminEmail: admin@example.com -``` - -I gotowe! diff --git a/best-practices/pl/microsites.texy b/best-practices/pl/microsites.texy deleted file mode 100644 index e88b04fc25..0000000000 --- a/best-practices/pl/microsites.texy +++ /dev/null @@ -1,63 +0,0 @@ -Jak pisać mikrostrony -********************* - -Wyobraź sobie, że potrzebujesz szybko stworzyć małą stronę na zbliżające się wydarzenie w Twojej firmie. Ma być prosta, szybka i bez zbędnych komplikacji. Możesz pomyśleć, że do tak małego projektu nie jest potrzebny rozbudowany framework. A co, jeśli użycie Nette Framework mogłoby ten proces uprościć i przyspieszyć? - -Nawet przy tworzeniu prostych stron nie chcesz rezygnować z wygody. Nie chcesz wymyślać na nowo tego, co już zostało rozwiązane. Śmiało bądź leniwy i pozwól się rozpieszczać. Nette Framework świetnie nadaje się także do użycia jako mikroframework. - -Jak może wyglądać taka mikrostrona? Na przykład cały kod strony może znajdować się w jednym pliku `index.php` w katalogu publicznym: - -```php -<?php - -require __DIR__ . '/../vendor/autoload.php'; - -$configurator = new Nette\Bootstrap\Configurator; -$configurator->enableTracy(__DIR__ . '/../log'); -$configurator->setTempDirectory(__DIR__ . '/../temp'); - -// tworzymy kontener DI na podstawie konfiguracji w config.neon -$configurator->addConfig(__DIR__ . '/../app/config.neon'); -$container = $configurator->createContainer(); - -// ustawiamy routing -$router = new Nette\Application\Routers\RouteList; -$container->addService('router', $router); - -// trasa dla URL https://example.com/ -$router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { - // wykrywamy język przeglądarki i przekierowujemy na URL /en lub /de itd. - $supportedLangs = ['en', 'de', 'cs']; - $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); - return $presenter->redirectUrl("/$lang"); -}); - -// trasa dla URL https://example.com/cs lub https://example.com/en -$router->addRoute('<lang cs|en|de>', function ($presenter, string $lang) { - // wyświetlamy odpowiedni szablon, na przykład ../templates/en.latte - $template = $presenter->createTemplate() - ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); - return $template; -}); - -// uruchamiamy aplikację! -$container->getByType(Nette\Application\Application::class)->run(); -``` - -Wszystko pozostałe to będą szablony zapisane w nadrzędnym katalogu `/templates`. - -Kod PHP w `index.php` najpierw [przygotowuje środowisko |bootstrap:], potem definiuje [trasy |application:routing#Routing dynamiczny z callbackami] i na koniec uruchamia aplikację. Zaletą jest to, że drugim parametrem funkcji `addRoute()` może być callable, który wykona się po otwarciu odpowiedniej strony. - - -Dlaczego używać Nette do mikrostron? ------------------------------------- - -- Programiści, którzy kiedyś spróbowali [Tracy|tracy:], nie wyobrażają sobie już dziś programowania bez niej. -- Przede wszystkim skorzystasz z systemu szablonów [Latte|latte:], bo już przy dwóch stronach zechcesz oddzielić [layout od treści|latte:template-inheritance]. -- I na pewno chcesz polegać na [automatycznym escapowaniu |latte:safety-first], żeby nie powstała podatność XSS. -- Nette zadba też o to, żeby w razie błędu nigdy nie wyświetliły się surowe komunikaty PHP, tylko przyjazna dla użytkownika strona. -- Jeśli chcesz zbierać opinie użytkowników, na przykład przez formularz kontaktowy, łatwo dodasz wsparcie dla [formularzy|forms:] i [bazy danych|database:]. -- Wypełnione formularze możesz też bez trudu [wysyłać e-mailem|mail:]. -- Czasem może się przydać [cache|caching:], na przykład przy pobieraniu i wyświetlaniu kanałów. - -W dzisiejszym szybkim świecie, gdzie kluczowe są szybkość i efektywność, niezbędne jest posiadanie narzędzi, które pozwalają osiągać rezultaty bez zbędnych opóźnień. Nette Framework oferuje dokładnie to: szybki rozwój, bezpieczeństwo i szeroką gamę narzędzi takich jak Tracy i Latte, które usprawniają pracę. Wystarczy zainstalować kilka pakietów Nette i zbudowanie takiej mikrostrony staje się niesamowicie proste. A możesz mieć pewność, że nie kryją się w niej żadne luki bezpieczeństwa. diff --git a/best-practices/pl/pagination.texy b/best-practices/pl/pagination.texy deleted file mode 100644 index 54b4856b8b..0000000000 --- a/best-practices/pl/pagination.texy +++ /dev/null @@ -1,273 +0,0 @@ -Stronicowanie wyników z bazy danych -*********************************** - -.[perex] -Przy tworzeniu aplikacji webowych bardzo często napotkasz wymaganie, żeby ograniczyć liczbę wypisywanych pozycji na stronie. Technika ta nazywa się stronicowaniem. - -Wyjdziemy od stanu, w którym wypisujemy wszystkie dane bez stronicowania. Do pobierania danych z bazy mamy klasę `ArticleRepository`. Oprócz konstruktora zawiera metodę `findPublishedArticles`, która zwraca wszystkie opublikowane artykuły posortowane malejąco według daty publikacji. - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC', - new \DateTime, - ); - } -} -``` - -W presenterze wstrzykniemy potem tę klasę modelu. W metodzie render pobierzemy opublikowane artykuły i przekażemy je do szablonu: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(): void - { - $this->template->articles = $this->articleRepository->findPublishedArticles(); - } -} -``` - -Szablon `default.latte` zajmie się już wypisaniem artykułów: - -```latte -{block content} -<h1>Artykuły</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> -``` - - -W ten sposób potrafimy wypisać wszystkie artykuły, co jednak zaczyna być problematyczne, gdy artykułów przybywa. W tym momencie przydaje się wdrożenie mechanizmu stronicowania. - -Mechanizm ten dzieli wszystkie artykuły na kilka stron i wyświetla tylko artykuły należące do aktualnie wybranej strony. Łączną liczbę stron i podział artykułów wylicza narzędzie [Paginator |utils:Paginator] na podstawie łącznej liczby artykułów i pożądanej liczby artykułów na stronie. - -W pierwszym kroku zmodyfikujemy w klasie repozytorium metodę pobierającą artykuły tak, żeby potrafiła zwrócić artykuły tylko dla jednej strony. Dodamy też metodę zwracającą łączną liczbę artykułów w bazie danych, potrzebną do skonfigurowania Paginatora: - -```php -namespace App\Model; - -use Nette; - - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(int $limit, int $offset): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC - LIMIT ? - OFFSET ?', - new \DateTime, $limit, $offset, - ); - } - - /** - * Zwraca łączną liczbę opublikowanych artykułów - */ - public function getPublishedArticlesCount(): int - { - return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime); - } -} -``` - -Następnie zmodyfikujemy presenter. Do metody `renderDefault` przekażemy numer aktualnie wyświetlanej strony. Jeśli numer ten nie będzie częścią URL, ustawimy wartość domyślną 1 (pierwsza strona). - -Rozszerzymy też metodę render o utworzenie i skonfigurowanie instancji Paginatora oraz o wybranie odpowiednich artykułów do wyświetlenia w szablonie. Zmodyfikowany `HomePresenter` będzie wyglądać tak: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // pobieramy łączną liczbę opublikowanych artykułów - $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - - // tworzymy i konfigurujemy instancję Paginatora - $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // łączna liczba pozycji - $paginator->setItemsPerPage(10); // liczba pozycji na stronie - $paginator->setPage($page); // numer aktualnej strony - - // pobieramy z bazy ograniczony zbiór artykułów według wyliczenia Paginatora - $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - - // przekazujemy je do szablonu - $this->template->articles = $articles; - // a także sam Paginator do wyświetlenia opcji stronicowania - $this->template->paginator = $paginator; - } -} -``` - -Szablon iteruje teraz tylko po artykułach z aktualnej strony. Musimy jeszcze dodać odnośniki stronicowania: - -```latte -{block content} -<h1>Artykuły</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if !$paginator->isFirst()} - <a n:href="default, 1">Pierwsza</a> -  |  - <a n:href="default, $paginator->getPage() - 1">Poprzednia</a> -  |  - {/if} - - Strona {$paginator->getPage()} z {$paginator->getPageCount()} - - {if !$paginator->isLast()} -  |  - <a n:href="default, $paginator->getPage() + 1">Następna</a> -  |  - <a n:href="default, $paginator->getPageCount()">Ostatnia</a> - {/if} -</div> -``` - - -W ten sposób uzupełniliśmy stronę o stronicowanie za pomocą Paginatora. Jeśli zamiast [Nette Database Core |database:sql-way] używasz jako warstwy bazodanowej [Nette Database Explorer |database:explorer], potrafisz zrealizować stronicowanie nawet bez bezpośredniego użycia Paginatora. Klasa `Nette\Database\Table\Selection` zawiera metodę [page() |api:Nette\Database\Table\Selection::page()], która zamyka w sobie logikę stronicowania. - -Przy takim podejściu repozytorium będzie wyglądać tak: - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Explorer $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\Table\Selection - { - return $this->database->table('articles') - ->where('created_at < ', new \DateTime) - ->order('created_at DESC'); - } -} -``` - -W presenterze nie musimy tworzyć instancji Paginatora. Zamiast tego użyjemy metody `page()` udostępnianej przez obiekt `Selection` zwrócony z repozytorium: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // pobieramy opublikowane artykuły - $articles = $this->articleRepository->findPublishedArticles(); - - // i przekazujemy do szablonu tylko ich część ograniczoną wyliczeniem metody page - $lastPage = 0; - $this->template->articles = $articles->page($page, 10, $lastPage); - - // a także dane potrzebne do wyświetlenia opcji stronicowania - $this->template->page = $page; - $this->template->lastPage = $lastPage; - } -} -``` - -Ponieważ do szablonu nie przekazujemy już obiektu Paginatora, musimy dostosować część wyświetlającą odnośniki stronicowania: - -```latte -{block content} -<h1>Artykuły</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if $page > 1} - <a n:href="default, 1">Pierwsza</a> -  |  - <a n:href="default, $page - 1">Poprzednia</a> -  |  - {/if} - - Strona {$page} z {$lastPage} - - {if $page < $lastPage} -  |  - <a n:href="default, $page + 1">Następna</a> -  |  - <a n:href="default, $lastPage">Ostatnia</a> - {/if} -</div> -``` - -W ten sposób zrealizowaliśmy mechanizm stronicowania bez jawnego użycia Paginatora. - -{{priority: -1}} diff --git a/best-practices/pl/passing-settings-to-presenters.texy b/best-practices/pl/passing-settings-to-presenters.texy deleted file mode 100644 index 722fc6b7f8..0000000000 --- a/best-practices/pl/passing-settings-to-presenters.texy +++ /dev/null @@ -1,49 +0,0 @@ -Przekazywanie ustawień do presenterów -************************************* - -.[perex] -Potrzebujesz przekazać do presenterów argumenty nieobiektowe (jak flaga wskazująca tryb debug, ścieżki do katalogów itd.), których nie da się przekazać automatycznie przez autowiring? Rozwiązaniem jest zamknięcie ich w dedykowanym obiekcie `Settings`. - -Usługa `Settings` daje bardzo prosty, a zarazem skuteczny sposób dostarczania presenterom informacji o działającej aplikacji. Jej konkretna struktura zależy wyłącznie od Twoich potrzeb. Przykład: - -```php -namespace App; - -class Settings -{ - public function __construct( - // od PHP 8.1 można używać readonly - public bool $debugMode, - public string $appDir, - // i tak dalej - ) {} -} -``` - -Przykład rejestracji w konfiguracji: - -```neon -services: - - App\Settings( - %debugMode%, - %appDir%, - ) -``` - -Gdy presenter potrzebuje informacji dostarczanych przez tę usługę, po prostu prosi o nią w swoim konstruktorze: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private App\Settings $settings, - ) {} - - public function renderDefault() - { - if ($this->settings->debugMode) { - // ... - } - } -} -``` diff --git a/best-practices/pl/post-links.texy b/best-practices/pl/post-links.texy deleted file mode 100644 index 3a5bd48092..0000000000 --- a/best-practices/pl/post-links.texy +++ /dev/null @@ -1,56 +0,0 @@ -Jak poprawnie używać odnośników POST -************************************ - -.[perex] -W aplikacjach webowych, zwłaszcza w interfejsach administracyjnych, podstawową zasadą powinno być, że akcje zmieniające stan serwera nie są wykonywane metodą HTTP GET. Jak sama nazwa wskazuje, GET służy tylko do pobierania danych, nie do ich zmiany. Do akcji takich jak usuwanie rekordów bardziej odpowiednia jest metoda POST. Idealna byłaby metoda DELETE, ale nie da się jej wywołać bez JavaScriptu, dlatego historycznie używa się do takich akcji POST. - -Jak to zrealizować w praktyce? Wykorzystaj prosty trik. Na początku szablonu layoutu utwórz pomocniczy formularz o ID `postForm`. Będziesz go potem używać dla akcji takich jak przyciski usuwania: - -```latte .{file:@layout.latte} -<form method="post" id="postForm"></form> -``` - -Dzięki temu formularzowi zamiast zwykłego odnośnika `<a>` możesz użyć `<button>`. Przycisk da się ostylować tak, by wyglądał jak zwykły odnośnik. Na przykład framework CSS Bootstrap oferuje klasy `btn btn-link`, dzięki którym przycisk staje się wizualnie nie do odróżnienia od pozostałych odnośników. Za pomocą atrybutu `form="postForm"` powiążesz przycisk z przygotowanym formularzem pomocniczym: - -```latte .{file:admin.latte} -<table> - <tr n:foreach="$posts as $post"> - <td>{$post->title}</td> - <td> - <button class="btn btn-link" form="postForm" formaction="{link delete, $post->id}">usuń</button> - <!-- zamiast <a n:href="delete, $post->id">usuń</a> --> - </td> - </tr> -</table> -``` - -Kliknięcie tego przycisku wywoła teraz akcję `delete`. Aby żądania były przyjmowane tylko metodą POST i pochodziły z tej samej domeny (skuteczna obrona przed atakami CSRF), użyj atrybutu `#[Requires]`: - -```php .{file:AdminPresenter.php} -use Nette\Application\Attributes\Requires; - -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST', sameOrigin: true)] - public function actionDelete(int $id): void - { - $this->facade->deletePost($id); // hipotetyczny kod usuwający rekord - $this->redirect('default'); - } -} -``` - -Atrybut ten jest dostępny od Nette Application 3.2. O jego możliwościach dowiesz się więcej na stronie [Jak używać atrybutu #Requires |attribute-requires]. - -Gdybyś zamiast akcji `actionDelete()` używał sygnału `handleDelete()`, podawanie `sameOrigin: true` jest zbędne, bo sygnały mają tę ochronę włączoną domyślnie: - -```php .{file:AdminPresenter.php} -#[Requires(methods: 'POST')] -public function handleDelete(int $id): void -{ - $this->facade->deletePost($id); - $this->redirect('this'); -} -``` - -Takie podejście nie tylko zwiększa bezpieczeństwo Twojej aplikacji, ale też sprzyja przestrzeganiu właściwych standardów i praktyk webowych. Używanie metod POST do akcji zmieniających stan prowadzi do solidniejszej i bezpieczniejszej aplikacji. diff --git a/best-practices/pl/presenter-traits.texy b/best-practices/pl/presenter-traits.texy deleted file mode 100644 index 6a21bf1726..0000000000 --- a/best-practices/pl/presenter-traits.texy +++ /dev/null @@ -1,47 +0,0 @@ -Składanie presenterów z traitów -******************************* - -.[perex] -Jeśli potrzebujesz zaimplementować tę samą funkcjonalność w wielu presenterach (np. weryfikację zalogowania użytkownika), typowym podejściem jest umieszczenie kodu we wspólnym przodku. Inną możliwością jest utworzenie jednozadaniowych [traitów |nette:introduction-to-object-oriented-programming#Traity]. - -Zaletą traitów jest to, że każdy presenter może włączyć tylko te traity, których faktycznie potrzebuje, zwłaszcza że PHP nie obsługuje dziedziczenia wielokrotnego. - -Traity te mogą wykorzystać fakt, że przy tworzeniu instancji presentera wywoływane są kolejno wszystkie [metody inject |inject-method-attribute#Metody inject*()]. Musisz tylko zadbać o to, aby nazwa każdej metody inject była unikalna we wszystkich użytych traitach i w samym presenterze. - -Traity mogą podpiąć kod inicjalizacyjny do zdarzeń [onStartup albo onRender |application:presenters#Zdarzenia]. - -Przykłady: - -```php -trait RequireLoggedUser -{ - public function injectRequireLoggedUser(): void - { - $this->onStartup[] = function () { - if (!$this->getUser()->isLoggedIn()) { - $this->redirect('Sign:in', $this->storeRequest()); - } - }; - } -} - -trait StandardTemplateFilters -{ - public function injectStandardTemplateFilters(TemplateBuilder $builder): void - { - $this->onRender[] = function () use ($builder) { - $builder->setupTemplate($this->template); - }; - } -} -``` - -Presenter po prostu używa potem tych traitów: - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - use StandardTemplateFilters; - use RequireLoggedUser; -} -``` diff --git a/best-practices/pl/pretty-urls.texy b/best-practices/pl/pretty-urls.texy deleted file mode 100644 index 174fd176c6..0000000000 --- a/best-practices/pl/pretty-urls.texy +++ /dev/null @@ -1,204 +0,0 @@ -Przyjazne adresy URL ze slugami -******************************* - -.[perex] -URL takie jak `/artykul/123-jak-upiec-chleb` wygląda lepiej niż `/artykul/123` i pomaga zarówno użytkownikom, jak i wyszukiwarkom zrozumieć, co czeka na stronie. Ten poradnik pokazuje, jak generować je wyłącznie w routerze, bez ingerencji w ani jeden szablon, i jak zadbać o to, żeby każdy odwiedzający wylądował na kanonicznym URL. - - -Dlaczego slug w URL -=================== - -Porównaj te dwa adresy: - -``` -/artykul/123 -/artykul/123-jak-upiec-chleb -``` - -Drugi zdradza użytkownikowi (i Google'owi), co czeka go po kliknięciu. To dobre dla SEO, sprawia, że odnośniki są czytelne w czacie albo e-mailu, i nadaje sens także paskowi adresu. - -Slug nie jest jednak prawdziwym identyfikatorem. Stronę określa ID. Slug to tylko dekoracja, którą aplikacja generuje z tytułu. Gdy tytuł się zmieni, slug też powinien się zmienić. A gdy ktoś ręcznie zmodyfikuje URL albo trafi ze starego odnośnika, aplikacja i tak powinna znaleźć właściwą stronę. - - -Cel -=== - -Chcemy trasę, która obsłuży to wszystko: - -``` -/artykul/123 → otwiera artykuł 123, przekierowuje na kanoniczny URL -/artykul/123-jak-upiec-chleb → otwiera artykuł 123 bezpośrednio -/artykul/123-cokolwiek-ktos-wpisal → otwiera artykuł 123, przekierowuje na kanoniczny URL -/artykul/ → 404 (brak ID) -``` - -I chcemy, żeby każde `n:href` i każde wywołanie `link()` w całej aplikacji automatycznie tworzyło `/artykul/123-jak-upiec-chleb`, **bez przepisywania ani jednego szablonu**. - - -Maska trasy -=========== - -Sztuczka polega na oznaczeniu sluga w masce jako **opcjonalnego** za pomocą nawiasów kwadratowych: - -```php -$router->addRoute('artykul/<id [0-9]+>[-<slug>]', 'Article:detail'); -``` - -Maska `[-<slug>]` mówi: po ID może być myślnik i slug, ale nie musi. Trasa akceptuje zarówno `/artykul/123`, jak i `/artykul/123-cokolwiek`. - -Uwaga do parametru `<slug>`: domyślnie dopasowuje dowolne znaki **z wyjątkiem ukośnika**, czyli dokładnie to, czego chcemy. Jeśli napiszesz `<slug .+>`, parametr będzie dopasowywał także ukośniki, więc `/artykul/123-cos/jeszcze` zostanie sparsowane jako jeden slug zawierający `/`. Zostań przy domyślnym `<slug>`, chyba że naprawdę tego potrzebujesz. - -Na razie URL jest parsowany poprawnie, ale generowane odnośniki nie będą zawierać sluga. Kolejnym krokiem jest nauczenie trasy, jak sluga uzupełnić. - - -Generowanie sluga bez ingerencji w szablony -=========================================== - -To jest ten zabójczy wariant. Istniejące wywołania `n:href="Article:detail, $id"` działają dalej bez zmian w całej aplikacji, bo router sam wyszuka tytuł. - -Zrobimy to za pomocą **ogólnego filtra** pod kluczem pustego ciągu: widzi on wszystkie parametry naraz i może dodać sluga: - -```php -use Nette\Routing\Route; -use Nette\Utils\Strings; - -$router->addRoute('artykul/<id [0-9]+>[-<slug>]', [ - 'presenter' => 'Article', - 'action' => 'detail', - '' => [ - Route::FilterOut => function (array $params) use ($slugProvider): array { - if (isset($params['id']) && empty($params['slug'])) { - $params['slug'] = $slugProvider->getSlug((int) $params['id']); - } - return $params; - }, - ], -]); -``` - -`FilterOut` uruchamia się za każdym razem, gdy router **generuje** URL. Jeśli slug nie został przekazany, filtr wyszuka tytuł i go doda. - -Slugi możesz wdrożyć w całej aplikacji jedną zmianą: wystarczy jedna definicja trasy. Każdy odnośnik w każdym szablonie zacznie automatycznie tworzyć `/artykul/123-jak-upiec-chleb`. Bez grepowania, bez przeszukiwania szablonów, bez przeoczonych zakamarków. - - -Zapamiętaj wynik wyszukiwania -============================= - -Jeden odnośnik generuje jedno zapytanie do bazy, a typowa strona ma ich wiele: listingi, okruszki, "ostatnio oglądane", artykuły powiązane. To samo ID artykułu pojawia się często w kilku odnośnikach w ramach jednego żądania, a nie chcesz odpytywać bazy za każdym razem. - -Rozwiązuje to maleńki cache w obrębie żądania. Zamknij wywołanie bazy w małej usłudze: - -```php -final class SlugProvider -{ - /** @var array<int, string> */ - private array $cache = []; - - public function __construct( - private Nette\Database\Explorer $db, - ) { - } - - public function getSlug(int $id): string - { - return $this->cache[$id] ??= Strings::webalize(Strings::truncate( - (string) $this->db->fetchField('SELECT title FROM article WHERE id = ?', $id), - 100, '' - )); - } -} -``` - -To wystarczy: jedno odpytanie bazy na unikalne ID w ramach żądania. - - -Przekazanie tytułu z szablonu (opcjonalna szybka ścieżka) -========================================================= - -Gdy tytuł jest w szablonie już pod ręką, możesz wyszukiwanie w bazie całkowicie pominąć. Przekaż tytuł jako parametr nazwany: - -```latte -<a n:href="Article:detail, $article->id, slug => $article->title">{$article->title}</a> -``` - -…i dodaj `FilterOut` dla pojedynczego parametru, który zamieni tytuł na ciąg bezpieczny dla URL: - -```php -$router->addRoute('artykul/<id [0-9]+>[-<slug>]', [ - 'presenter' => 'Article', - 'action' => 'detail', - 'slug' => [ - Route::FilterOut => fn($title) => Strings::webalize(Strings::truncate($title, 100, '')), - ], - '' => [/* fallback z wyszukiwaniem z góry */], -]); -``` - -Oba filtry współpracują. Najpierw uruchamia się filtr ogólny; widząc, że slug jest już wypełniony przekazanym tytułem, pomija odpytanie bazy. Filtr `FilterOut` przy parametrze zamienia potem ten tytuł na właściwego sluga. Szablony, które tytułu nie przekazują, działają dalej: filtr ogólny znajdzie pustego sluga i pójdzie ścieżką z wyszukiwaniem. - -Używaj tego tylko tam, gdzie ma to znaczenie (duże listingi renderowane setki razy na żądanie). Dla większości aplikacji wyszukiwanie z cache jest wystarczająco szybkie. - - -Kanonizacja: przekierowanie na właściwy URL -=========================================== - -Potrafimy już generować `/artykul/123-jak-upiec-chleb`, ale trasa nadal akceptuje `/artykul/123` oraz `/artykul/123-cokolwiek-ktos-napisal`. To zamierzone: chcemy krótkich URL-i (więcej o tym niżej) i chcemy, żeby stare albo ręcznie wpisane odnośniki dalej działały. Nie chcemy jednak, żeby wyszukiwarki indeksowały ten sam artykuł pod kilkoma adresami. - -Rozwiązaniem jest [kanonizacja |application:presenters#Kanonizacja]: gdy użytkownik przyjdzie przez niekanoniczny URL, aplikacja przekieruje go kodem 301 na właściwy. Zajmuje się tym metoda `canonicalize()`: - -```php -public function actionDetail(int $id, ?string $slug = null): void -{ - $article = $this->facade->getArticle($id); - if (!$article) { - $this->error(); - } - - // generuje kanoniczny URL przez ten sam FilterOut - // i przekierowuje z HTTP 301, jeśli różni się od bieżącego URL - $this->canonicalize('detail', ['id' => $id]); - - $this->template->article = $article; -} -``` - -`canonicalize()` generuje kanoniczny URL tak samo, jak zrobiłoby to `link()` (czyli przechodzi przez ten sam `FilterOut`), i porównuje go z bieżącym URL. Jeśli się różnią, przekierowuje z HTTP 301. Odwiedzający lądują na właściwym URL, a wyszukiwarki widzą tylko jedną kanoniczną wersję. - - -Jedno miejsce decydujące o wyglądzie sluga -========================================== - -Zauważ, że wywołanie `Strings::webalize(Strings::truncate(..., 100, ''))` żyje w jednym miejscu: wewnątrz `SlugProvider` (albo w `FilterOut` przy parametrze). Ta sama logika tworzy odnośnik w szablonie, URL w `redirect()` i postać kanoniczną w `canonicalize()`. - -Jeśli zechcesz później zmienić reguły (inny limit długości, inna transliteracja, usuwanie dodatkowych znaków), zmieniasz jedną linię. Bez tego ryzykowałbyś, że `redirect()` wygeneruje `/artykul/123-jak-upiec-chleb`, podczas gdy `canonicalize()` oczekuje `/artykul/123-jak-upiec-chl` (bo ktoś gdzie indziej zastosował inną długość `truncate`), a aplikacja przekierowywałaby w kółko. - - -Bonus: krótkie URL-e nadal działają -=================================== - -Ponieważ slug jest opcjonalny, adresy bez niego dalej działają: - -``` -/artykul/123 -``` - -Przydaje się to przy: -- **kodach QR** - krótszy URL oznacza mniej gęsty, łatwiejszy do zeskanowania kod -- **SMS-ach i czacie** - mieści się w tweecie, wygląda schludnie -- **materiałach drukowanych** - krótki URL szybciej się przepisuje - -Gdy użytkownik otworzy taki URL, `canonicalize()` przekieruje go kodem 301 na pełną wersję ze slugiem, więc wyszukiwarki i tak zobaczą tylko postać kanoniczną. Możesz mieć jednocześnie zwięzłość i SEO. - - -Podsumowanie -============ - -- Maska `<id>[-<slug>]` czyni sluga opcjonalnym. Domyślne `<slug>` nie dopasowuje `/`; użyj `<slug .+>` tylko wtedy, gdy naprawdę chcesz ukośników w slugu. -- Ogólny `FilterOut` pod kluczem `''` wyszukuje tytuł po ID - **bez zmian w szablonach w całej aplikacji**. -- Zamknij wyszukiwanie w maleńkim cache w obrębie żądania; jedno zapytanie do bazy na unikalne ID w zupełności wystarczy. -- Opcjonalnie `FilterOut` przy parametrze pozwala szablonom przekazać tytuł bezpośrednio i pominąć wyszukiwanie. -- `$this->canonicalize()` w akcji przekierowuje niekanoniczne URL-e na właściwy z HTTP 301. -- Wzór na sluga (`webalize` + `truncate`) żyje w jednym miejscu - zmienisz go raz, zadziała wszędzie. -- Krótkie URL-e z samym ID działają dalej, co przydaje się przy kodach QR i SMS-ach. - -Więcej o filtrach i kanonizacji znajdziesz w dokumentacji [routingu |application:routing#Filtry ogólne] i [presenterów |application:presenters#Kanonizacja]. diff --git a/best-practices/pl/restore-request.texy b/best-practices/pl/restore-request.texy deleted file mode 100644 index 97875b6670..0000000000 --- a/best-practices/pl/restore-request.texy +++ /dev/null @@ -1,62 +0,0 @@ -Jak wrócić do wcześniejszej strony? -*********************************** - -.[perex] -Co się stanie, jeśli użytkownik wypełnia formularz, a w międzyczasie wygaśnie mu sesja logowania? Żeby nie stracił danych, możemy przed przekierowaniem na stronę logowania zapisać bieżące żądanie (łącznie z danymi formularza) do sesji. W Nette jest to zaskakująco proste. - -Bieżące żądanie zapiszemy do sesji metodą `storeRequest()`. Metoda zwraca unikalny identyfikator (krótki ciąg znaków) zapisanego żądania. Zapisuje nazwę bieżącego presentera, jego widok oraz parametry. Jeśli w ramach żądania został wysłany formularz, zapisywane są także wartości wpisane w polach (z wyjątkiem wysłanych plików). - -Żądanie przywracamy metodą `restoreRequest($key)`, której przekazujemy wcześniej otrzymany identyfikator. Metoda przekieruje użytkownika z powrotem do pierwotnego presentera i widoku. Jeśli jednak zapisane żądanie zawierało wysłanie formularza, `restoreRequest()` zamiast przekierowania użyje metody `forward()`. Wcześniej wypełnione wartości przekaże z powrotem do formularza i pozwoli go ponownie wyrenderować. Dzięki temu użytkownik może wysłać formularz jeszcze raz, nie tracąc żadnych wpisanych danych. - -Co ważne, `restoreRequest()` sprawdza, czy nowo zalogowany użytkownik to ten sam, który pierwotnie wysłał formularz. Jeśli to ktoś inny, zapisane żądanie nie zostanie przywrócone, a metoda nic nie zrobi, co zwiększa bezpieczeństwo. - -Pokażmy to na przykładzie. Mamy `AdminPresenter`, w którym edytuje się dane. Jego metoda `startup()` sprawdza, czy użytkownik jest zalogowany. Jeśli nie, przekierowuje go do `SignPresenter`. Jednocześnie zapisujemy bieżące żądanie metodą `storeRequest()` i przekazujemy jego klucz (`$backlink`) do `SignPresenter`. - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - protected function startup() - { - parent::startup(); - - if (!$this->user->isLoggedIn()) { - $this->redirect('Sign:in', ['backlink' => $this->storeRequest()]); - } - } -} -``` - -`SignPresenter` będzie zawierał oprócz formularza logowania także trwały parametr `$backlink`, w którym przechowywany jest klucz. Ponieważ parametr jest trwały, jego wartość zostanie zachowana także po wysłaniu formularza logowania. - - -```php -use Nette\Application\Attributes\Persistent; - -class SignPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $backlink = ''; - - protected function createComponentSignInForm() - { - $form = new Nette\Application\UI\Form; - // ... dodajemy pola formularza ... - $form->onSuccess[] = $this->signInFormSuceeded(...); - return $form; - } - - private function signInFormSuceeded($form) - { - // ... tutaj logujemy użytkownika ... - - $this->restoreRequest($this->backlink); - $this->redirect('Admin:'); - } -} -``` - -Metodzie `restoreRequest()` przekazujemy klucz (`$this->backlink`) zapisanego żądania. Ta następnie przekieruje (lub przeforwarduje) użytkownika z powrotem do pierwotnego presentera i widoku. - -Jeśli jednak klucz jest nieprawidłowy (np. wygasł już z sesji), metoda nic nie zrobi. Kolejne wywołanie `$this->redirect('Admin:')` działa więc jako zabezpieczenie i przekieruje na stronę domyślną, na przykład `AdminPresenter`. - -{{priority: -1}} diff --git a/best-practices/ru/@home.texy b/best-practices/ru/@home.texy deleted file mode 100644 index 623cb64fa2..0000000000 --- a/best-practices/ru/@home.texy +++ /dev/null @@ -1,69 +0,0 @@ -Руководства и лучшие практики -***************************** - -.[perex] -Руководства, решения типичных задач и лучшие практики для Nette. - - -<div class=documentation> -<div> - - -Nette Application ------------------ -- [Методы и атрибуты inject |inject-method-attribute] -- [Составление презентеров из трейтов |presenter-traits] -- [Передача настроек в презентеры |passing-settings-to-presenters] -- [Как восстановить запрос |restore-request] -- [Постраничный вывод из базы данных |pagination] -- [Динамические сниппеты |dynamic-snippets] -- [Как использовать атрибут #Requires |attribute-requires] -- [Как правильно использовать POST-ссылки |post-links] -- [Красивые URL со слагами |pretty-urls] - -</div> -<div> - - -Формы ------ -- [Повторное использование форм |form-reuse] -- [Форма для создания и редактирования записей |creating-editing-form] -- [Создадим форму обратной связи |lets-create-contact-form] -- [Зависимые выпадающие списки |https://blog.nette.org/ru/dependent-selectboxes-elegantly-in-nette-and-pure-js] - -</div> -<div> - - -Общее ------ -- [Как загрузить конфигурационный файл |bootstrap:] -- [Как писать микросайты |microsites] -- [Почему Nette записывает константы в PascalCase? |https://blog.nette.org/ru/for-less-screaming-in-the-code] -- [Почему Nette не использует суффикс Interface? |https://blog.nette.org/ru/prefixes-and-suffixes-do-not-belong-in-interface-names] -- [Composer: советы по использованию |composer] -- [Введение в объектно-ориентированное программирование |nette:introduction-to-object-oriented-programming] - -</div> -<div> - - -Примеры решений ---------------- -- [Примеры Nette |https://github.com/nette-examples] -- [Doctrine & Nette |https://contributte.org/nettrine/] -- [Примеры Contributte |https://contributte.org/examples.html] -- [Сайт на Doctrine ORM |https://github.com/MinecordNetwork/Website] -- [Быстрый старт |quickstart:] - -</div> -<div> - - -Видео ------ -Сотни записей со встреч Last Saturday и видео о Nette собраны в одном месте на "YouTube-канале Nette Framework":https://www.youtube.com/user/NetteFramework. - -</div> -</div> diff --git a/best-practices/ru/@left-menu.texy b/best-practices/ru/@left-menu.texy deleted file mode 100644 index 5f0b47ac07..0000000000 --- a/best-practices/ru/@left-menu.texy +++ /dev/null @@ -1,34 +0,0 @@ -Руководства и лучшие практики -***************************** -- [Обзор |@home] - -Nette Application -***************** -- [Методы и атрибуты inject |inject-method-attribute] -- [Составление презентеров из трейтов |presenter-traits] -- [Передача настроек в презентеры |passing-settings-to-presenters] -- [Как восстановить запрос |restore-request] -- [Постраничный вывод из базы данных |pagination] -- [Динамические сниппеты |dynamic-snippets] -- [Как использовать атрибут #Requires |attribute-requires] -- [Как правильно использовать POST-ссылки |post-links] -- [Красивые URL со слагами |pretty-urls] - -Формы -***** -- [Повторное использование форм |form-reuse] -- [Форма для создания и редактирования записей |creating-editing-form] -- [Создадим форму обратной связи |lets-create-contact-form] - -Общее -***** -- [Как писать микросайты |microsites] -- [Composer: советы по использованию |composer] - - -Дополнительные материалы -************************ -- [Документация Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Устранение неполадок |nette:troubleshooting] diff --git a/best-practices/ru/@meta.texy b/best-practices/ru/@meta.texy deleted file mode 100644 index cbad61a84f..0000000000 --- a/best-practices/ru/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Руководства и лучшие практики}} diff --git a/best-practices/ru/attribute-requires.texy b/best-practices/ru/attribute-requires.texy deleted file mode 100644 index c7f07757f3..0000000000 --- a/best-practices/ru/attribute-requires.texy +++ /dev/null @@ -1,177 +0,0 @@ -Как использовать атрибут `#[Requires]` -************************************** - -.[perex] -Разрабатывая веб-приложение, вы часто сталкиваетесь с необходимостью ограничить доступ к некоторым его частям. Возможно, вы хотите, чтобы часть запросов могла отправлять данные только через форму (то есть методом POST) или была доступна только для AJAX-вызовов. В Nette Framework 3.2 появился новый инструмент, позволяющий задавать такие ограничения изящно и наглядно: атрибут `#[Requires]`. - -Атрибут - это особая пометка в PHP, которую вы добавляете перед определением класса или метода. Поскольку по сути это класс, для работы следующих примеров нужно подключить конструкцию `use`: - -```php -use Nette\Application\Attributes\Requires; -``` - -Атрибут `#[Requires]` можно использовать с самим классом презентера и с этими методами: - -- `action<Action>()` -- `render<View>()` -- `handle<Signal>()` -- `createComponent<Name>()` - -Последние два метода относятся и к компонентам, поэтому атрибут можно использовать и с ними. - -Если условия, заданные атрибутом, не выполнены, вызывается HTTP-ошибка 4xx. - - -HTTP-методы ------------ - -Вы можете указать, какие HTTP-методы (такие как GET, POST и так далее) разрешены для доступа. Например, если вы хотите разрешить доступ только через отправку формы, задайте: - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST')] - public function actionDelete(int $id): void - { - } -} -``` - -Почему для действий, меняющих состояние, стоит использовать POST вместо GET и как это сделать? [Прочитайте руководство |post-links]. - -Можно указать один метод или массив методов. Особый случай - значение `'*'`, разрешающее все методы, чего презентеры [по соображениям безопасности по умолчанию не допускают |application:presenters#Проверка HTTP-метода]. - - -AJAX-вызовы ------------ - -Если вы хотите, чтобы презентер или метод были доступны только для AJAX-запросов, используйте: - -```php -#[Requires(ajax: true)] -class AjaxPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Тот же источник ---------------- - -Ради повышения безопасности вы можете потребовать, чтобы запрос приходил с того же домена. Это предотвращает [уязвимость CSRF |nette:vulnerability-protection#Cross-Site Request Forgery (CSRF)]: - -```php -#[Requires(sameOrigin: true)] -class SecurePresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Для методов `handle<Signal>()` доступ с того же домена требуется автоматически. Поэтому если вы хотите разрешить доступ с любого домена, укажите: - -```php -#[Requires(sameOrigin: false)] -public function handleList(): void -{ -} -``` - - -Доступ через 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 - { - } -} -``` - - -Конкретные действия -------------------- - -Вы можете также ограничить какой-то код, например создание компонента, чтобы он был доступен только для определённых действий презентера: - -```php -class EditDeletePresenter extends Nette\Application\UI\Presenter -{ - #[Requires(actions: ['add', 'edit'])] - public function createComponentPostForm() - { - } -} -``` - -Для одного действия массив писать не нужно: `#[Requires(actions: 'default')]` - - -Собственные атрибуты --------------------- - -Если вы хотите многократно использовать атрибут `#[Requires]` с одними и теми же настройками, вы можете создать собственный атрибут, который наследует `#[Requires]` и настраивает его под ваши нужды. - -Например, `#[SingleAction]` разрешает доступ только через действие `default`: - -```php -#[\Attribute] -class SingleAction extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(actions: 'default'); - } -} - -#[SingleAction] -class SingleActionPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -А `#[RestMethods]` разрешит доступ всеми HTTP-методами, используемыми для REST API: - -```php -#[\Attribute] -class RestMethods extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']); - } -} - -#[RestMethods] -class ApiPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Заключение ----------- - -Атрибут `#[Requires]` даёт вам большую гибкость и контроль над тем, как обращаются к вашим страницам. С помощью простых, но мощных правил вы можете повысить безопасность и правильность работы приложения. Как видите, использование атрибутов в Nette может не только упростить вашу работу, но и защитить её. diff --git a/best-practices/ru/composer.texy b/best-practices/ru/composer.texy deleted file mode 100644 index 67f90286aa..0000000000 --- a/best-practices/ru/composer.texy +++ /dev/null @@ -1,282 +0,0 @@ -Composer: советы по использованию -********************************* - -<div class=perex> - -Composer - инструмент управления зависимостями в PHP. Он позволяет объявить библиотеки, от которых зависит ваш проект, и сам их установит и обновит. Мы узнаем: - -- как установить Composer -- как использовать его в новом или существующем проекте - -</div> - - -Установка -========= - -Composer - исполняемый файл `.phar`, который вы скачиваете и устанавливаете следующим образом. - - -Windows -------- - -Воспользуйтесь официальным установщиком [Composer-Setup.exe|https://getcomposer.org/Composer-Setup.exe]. - - -Linux, macOS ------------- - -Вам понадобятся всего 4 команды, которые можно скопировать с [этой страницы |https://getcomposer.org/download/]. - -Кроме того, скопировав его в папку, входящую в системный `PATH`, вы сделаете Composer доступным глобально: - -```shell -$ mv ./composer.phar ~/bin/composer # или /usr/local/bin/composer -``` - - -Использование в проекте -======================= - -Чтобы начать использовать Composer в своём проекте, вам нужен только файл `composer.json`. Этот файл описывает зависимости вашего проекта и может содержать другие метаданные. Простейший `composer.json` может выглядеть так: - -```js -{ - "require": { - "nette/database": "^3.0" - } -} -``` - -Здесь мы говорим, что нашему приложению (или библиотеке) нужен пакет `nette/database` (имя пакета состоит из имени поставщика и имени проекта) и что нужна версия, отвечающая ограничению `^3.0` (то есть последняя версия 3). - -Итак, имея файл `composer.json` в корне проекта, выполните: - -```shell -composer update -``` - -Composer скачает Nette Database в каталог `vendor/`. Он также создаст файл `composer.lock`, в котором записано, какие именно версии библиотек были установлены. - -Composer порождает файл `vendor/autoload.php`. Вы можете просто подключить этот файл и начать использовать классы библиотек без каких-либо дополнительных действий: - -```php -require __DIR__ . '/vendor/autoload.php'; - -$db = new Nette\Database\Connection('sqlite::memory:'); -``` - - -Обновление пакетов до последних версий -====================================== - -Чтобы обновить используемые библиотеки до последних версий в рамках ограничений, заданных в `composer.json`, используйте команду `composer update`. Например, при зависимости `"nette/database": "^3.0"` он установит последнюю версию 3.x.x, но не версию 4. - -Чтобы обновить сами ограничения в файле `composer.json`, например до `"nette/database": "^4.1"`, разрешив установку последней версии, используйте команду `composer require nette/database`. - -Чтобы обновить все используемые пакеты Nette, вам пришлось бы перечислить их все в командной строке, например: - -```shell -composer require nette/application nette/forms latte/latte tracy/tracy ... -``` - -Это непрактично. Поэтому воспользуйтесь простым скриптом "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff, который сделает это за вас: - -```shell -php composer-frontline.php -``` - - -Создание нового проекта -======================= - -Новый проект на Nette можно создать одной командой: - -```shell -composer create-project nette/web-project name-of-the-project -``` - -Замените `name-of-the-project` на имя каталога вашего проекта и выполните команду. Composer скачает с GitHub репозиторий `nette/web-project`, в котором уже есть файл `composer.json`, а затем установит сам Nette Framework. Останется только [настроить права каталогов |nette:troubleshooting#Задание прав на каталоги] `temp/` и `log/`, и проект должен заработать. - -Если вы знаете, на какой версии PHP будет размещён ваш проект, обязательно [задайте её |#Версия PHP]. - - -Версия PHP -========== - -Composer всегда устанавливает версии пакетов, совместимые с той версией PHP, которую вы сейчас используете (точнее, с версией PHP в командной строке, где запускается Composer). Она может отличаться от версии, которая используется на вашем хостинге. Поэтому принципиально важно добавить в файл `composer.json` сведения о версии PHP на хостинге. Тогда будут устанавливаться только версии пакетов, совместимые с хостингом. - -Например, чтобы указать, что проект будет работать на PHP 8.2.3, используйте команду: - -```shell -composer config platform.php 8.2.3 -``` - -Версия запишется в файл `composer.json` вот так: - -```js -{ - "config": { - "platform": { - "php": "8.2.3" - } - } -} -``` - -Однако номер версии PHP указывается в файле и в другом месте, в секции `require`. Первое число определяет версию, под которую устанавливаются пакеты, а второе - версию, под которую написано само приложение. Например, PhpStorm по нему выставляет *PHP language level*. (Разумеется, различаться этим версиям смысла нет, так что двойная запись - недосмотр.) Задайте эту версию командой: - -```shell -composer require php 8.2.3 --no-update -``` - -Или прямо в файле `composer.json`: - -```js -{ - "require": { - "php": "8.2.3" - } -} -``` - - -Игнорирование версии PHP -======================== - -Пакеты обычно указывают и самую низкую версию PHP, с которой они совместимы, и самую высокую, на которой они были протестированы. Если вы собираетесь использовать ещё более новую версию PHP, скажем ради тестирования, Composer откажется устанавливать такой пакет. Решение - параметр `--ignore-platform-req=php+`, который заставляет Composer игнорировать верхние границы требуемой версии PHP. - - -Ложные сообщения -================ - -При обновлении пакетов или изменении номеров версий иногда возникают конфликты. У одного пакета требования конфликтуют с другим и так далее. Однако иногда Composer выдаёт ложные сообщения. Он сообщает о конфликте, которого на самом деле нет. В таких случаях может помочь удаление файла `composer.lock` и повторная попытка. - -Если сообщение об ошибке не исчезает, значит оно настоящее, и вам нужно его прочитать, чтобы понять, что и как изменить. - - -Packagist.org - глобальный репозиторий -====================================== - -[Packagist |https://packagist.org] - главный репозиторий, в котором Composer по умолчанию ищет пакеты. Здесь вы можете публиковать и собственные пакеты. - - -А если нам не нужен центральный репозиторий -------------------------------------------- - -Если внутри компании у нас есть приложения или библиотеки, которые нельзя размещать публично, мы можем создать для них собственные репозитории. - -Подробнее о репозиториях читайте в [официальной документации |https://getcomposer.org/doc/05-repositories.md#repositories]. - - -Автозагрузка -============ - -Ключевая возможность Composer в том, что он обеспечивает автозагрузку всех устанавливаемых им классов. Вы включаете её подключением файла `vendor/autoload.php`. - -Однако Composer можно использовать и для загрузки других классов вне каталога `vendor/`. Первый вариант - позволить Composer просмотреть заданные каталоги и подкаталоги, найти все классы и включить их в автозагрузчик. Для этого задайте в `composer.json` секцию `autoload > classmap`: - -```js -{ - "autoload": { - "classmap": [ - "src/", # включает каталог src/ и его подкаталоги - ] - } -} -``` - -После этого вам придётся после каждого изменения выполнять команду `composer dumpautoload`, чтобы перегенерировать таблицы автозагрузки. Это крайне неудобно. Куда лучше поручить эту задачу [RobotLoader|robot-loader:], который делает то же самое автоматически в фоне и намного быстрее. - -Второй вариант - придерживаться [PSR-4 |https://www.php-fig.org/psr/psr-4/]. Проще говоря, это система, в которой пространства имён и имена классов соответствуют структуре каталогов и именам файлов, например `App\Core\RouterFactory` будет находиться в файле `/path/to/App/Core/RouterFactory.php`. Пример настройки: - -```js -{ - "autoload": { - "psr-4": { - "App\\": "app/" # пространство имён App\ находится в каталоге app/ - } - } -} -``` - -Подробности о настройке этого поведения см. в [документации Composer |https://getcomposer.org/doc/04-schema.md#psr-4]. - - -Тестирование новых версий -========================= - -Хотите протестировать новую разрабатываемую версию пакета? Вот как это сделать. Сначала добавьте в файл `composer.json` эту пару параметров. Она разрешает установку разрабатываемых версий, но Composer прибегнет к ним только тогда, когда ни одно сочетание стабильных версий не отвечает требованиям: - -```js -{ - "minimum-stability": "dev", - "prefer-stable": true, -} -``` - -Мы также рекомендуем удалить файл `composer.lock`, потому что Composer иногда необъяснимо отказывается от установки, а это может решить проблему. - -Допустим, пакет - `nette/utils`, а новая версия - 4.0. Установите её командой: - -```shell -composer require nette/utils:4.0.x-dev -``` - -Или вы можете установить конкретную версию, например 4.0.0-RC2: - -```shell -composer require nette/utils:4.0.0-RC2 -``` - -Однако если от библиотеки зависит другой пакет и он привязан к более старой версии (например, `^3.1`), идеальным решением будет обновить этот зависимый пакет так, чтобы он работал с новой версией. Но если вы просто хотите обойти ограничение и заставить Composer установить разрабатываемую версию, притворившись, что это более старая версия (например, 3.1.6), вы можете использовать ключевое слово `as`: - -```shell -composer require nette/utils "4.0.x-dev as 3.1.6" -``` - - -Вызов команд -============ - -Вы можете вызывать через Composer собственные заранее заданные команды и скрипты так, будто это его родные команды. Для скриптов, лежащих в каталоге `vendor/bin`, указывать этот путь не нужно. - -В качестве примера определим в `composer.json` скрипт, который использует [Nette Tester |tester:] для запуска тестов: - -```js -{ - "scripts": { - "tester": "tester tests -s" - } -} -``` - -Затем мы запускаем тесты командой `composer tester`. Команду можно вызвать, даже если вы находитесь не в корневом каталоге проекта, а в одном из его подкаталогов. - - -Скажите спасибо -=============== - -Покажем приём, который порадует авторов открытого кода. Вы можете легко поставить на GitHub звёзды библиотекам, которые использует ваш проект. Достаточно установить библиотеку `symfony/thanks`: - -```shell -composer global require symfony/thanks -``` - -А затем выполнить: - -```shell -composer thanks -``` - -Попробуйте! - - -Настройка -========= - -Composer тесно интегрирован с системой контроля версий [Git |https://git-scm.com]. Если Git у вас не установлен, нужно сказать Composer, чтобы он его не использовал: - -```shell -composer -g config preferred-install dist -``` diff --git a/best-practices/ru/creating-editing-form.texy b/best-practices/ru/creating-editing-form.texy deleted file mode 100644 index e059c579eb..0000000000 --- a/best-practices/ru/creating-editing-form.texy +++ /dev/null @@ -1,204 +0,0 @@ -Форма для создания и редактирования записи -****************************************** - -.[perex] -Как правильно реализовать добавление и редактирование записи в Nette, используя для обоих случаев одну и ту же форму? - -Во многих случаях формы для добавления и редактирования записи одинаковы и различаются разве что подписью кнопки. Мы покажем примеры простых презентеров, где сначала используем форму для добавления записи, затем для её редактирования, а в конце объединим оба решения. - - -Добавление записи ------------------ - -Пример презентера для добавления записи. Саму работу с базой данных оставим классу `Facade`, код которого для этого примера не важен. - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentRecordForm(): Form - { - $form = new Form; - - // ... добавляем поля формы ... - - $form->onSuccess[] = $this->recordFormSucceeded(...); - return $form; - } - - private function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // добавляем запись в базу данных - $this->flashMessage('Successfully added'); - $this->redirect('...'); - } - - public function renderAdd(): void - { - // ... - } -} -``` - - -Редактирование записи ---------------------- - -Теперь посмотрим, как выглядел бы презентер для редактирования записи: - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - private $record; - - public function __construct( - private Facade $facade, - ) { - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // проверяем существование записи - || !$this->facade->isEditAllowed(/*...*/) // проверяем права - ) { - $this->error(); // ошибка 404 - } - - $this->record = $record; - } - - protected function createComponentRecordForm(): Form - { - // проверяем, что действие - 'edit' - if ($this->getAction() !== 'edit') { - $this->error(); - } - - $form = new Form; - - // ... добавляем поля формы ... - - $form->setDefaults($this->record); // задаём значения по умолчанию - $form->onSuccess[] = $this->recordFormSucceeded(...); - return $form; - } - - private function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->update($this->record->id, $data); // обновляем запись - $this->flashMessage('Successfully updated'); - $this->redirect('...'); - } -} -``` - -В методе *action*, вызываемом в начале [жизненного цикла презентера |application:presenters#Жизненный цикл презентера], мы проверяем существование записи и право пользователя её редактировать. - -Мы сохраняем запись в свойстве `$record`, благодаря чему она доступна в методе `createComponentRecordForm()` для задания значений по умолчанию и в `recordFormSucceeded()` для доступа к идентификатору. Альтернативное решение - задать значения по умолчанию прямо в `actionEdit()`, а значение ID (часть URL) получить через `getParameter('id')`: - - -```php - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - // проверяем существование и права - ) { - $this->error(); - } - - // задаём значения формы по умолчанию - $this->getComponent('recordForm') - ->setDefaults($record); - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); - // ... - } -``` - -Однако, и это должно стать **самым важным выводом из всего кода**, мы обязаны при создании формы убедиться, что действие действительно `edit`. Иначе проверка в методе `actionEdit()` вообще не выполнится! - - -Одна форма для добавления и редактирования ------------------------------------------- - -Теперь объединим оба презентера в один. Мы могли бы либо различать действие внутри метода `createComponentRecordForm()` и настраивать форму соответственно, либо переложить это на сами методы действий и избавиться от условной проверки: - - -```php -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - public function actionAdd(): void - { - $form = $this->getComponent('recordForm'); - $form->onSuccess[] = $this->addingFormSucceeded(...); - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // проверяем существование записи - || !$this->facade->isEditAllowed(/*...*/) // проверяем права - ) { - $this->error(); // ошибка 404 - } - - $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // задаём значения по умолчанию - $form->onSuccess[] = $this->editingFormSucceeded(...); - } - - protected function createComponentRecordForm(): Form - { - // проверяем, что действие - 'add' или 'edit' - if (!in_array($this->getAction(), ['add', 'edit'])) { - $this->error(); - } - - $form = new Form; - - // ... добавляем поля формы ... - - return $form; - } - - private function addingFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // добавляем запись в базу данных - $this->flashMessage('Successfully added'); - $this->redirect('...'); - } - - private function editingFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // обновляем запись - $this->flashMessage('Successfully updated'); - $this->redirect('...'); - } -} -``` - -{{priority: -1}} diff --git a/best-practices/ru/dynamic-snippets.texy b/best-practices/ru/dynamic-snippets.texy deleted file mode 100644 index 518a2838e2..0000000000 --- a/best-practices/ru/dynamic-snippets.texy +++ /dev/null @@ -1,176 +0,0 @@ -Динамические сниппеты -********************* - -.[perex] -Как с помощью AJAX обновлять только те части страницы, которые действительно меняются, например отдельные элементы списка, используя динамические сниппеты Latte. - -Довольно часто при разработке приложения возникает необходимость выполнять AJAX-операции, например над отдельными строками таблицы или элементами списка. Для примера возьмём вывод статей, где вошедшие пользователи могут оценить каждую статью как "нравится" или "не нравится". Код презентера и соответствующий шаблон без AJAX выглядели бы примерно так (приводим самые важные части; код предполагает, что существует сервис для обработки оценок и получения статей - его конкретная реализация для этого руководства не важна): - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - $this->redirect('this'); -} - -public function handleUnlike(int $articleId): void -{ - $this->ratingService->removeLike($articleId, $this->user->id); - $this->redirect('this'); -} -``` - -Шаблон: - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {if !$article->liked} - <a n:href="like!, $article->id" class=ajax>I like it</a> - {else} - <a n:href="unlike!, $article->id" class=ajax>I don't like it anymore</a> - {/if} -</article> -``` - - -Добавление AJAX -=============== - -Теперь добавим в это простое приложение функциональность AJAX. Изменение оценки статьи не настолько важно, чтобы ради него перезагружать страницу целиком, поэтому в идеале оно должно происходить в фоне через AJAX. Мы воспользуемся [скриптом-обработчиком из дополнений |application:ajax#Naja] с общепринятым соглашением, что у AJAX-ссылок есть CSS-класс `ajax`. - -Но как именно это реализовать? Nette предлагает два подхода: динамические сниппеты и компоненты. У каждого есть свои плюсы и минусы, поэтому покажем оба. - - -Способ с динамическими сниппетами -================================= - -В терминологии Latte динамический сниппет - это особый случай использования тега `{snippet}`, когда в имени сниппета используется переменная. Такой сниппет нельзя поставить где угодно в шаблоне: он должен быть обёрнут статическим (обычным) сниппетом или находиться внутри `{snippetArea}`. Мы могли бы изменить наш шаблон так: - - -```latte -{snippet articlesContainer} - <article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {snippet article-{$article->id}} - {if !$article->liked} - <a n:href="like!, $article->id" class=ajax>I like it</a> - {else} - <a n:href="unlike!, $article->id" class=ajax>I don't like it anymore</a> - {/if} - {/snippet} - </article> -{/snippet} -``` - -Каждая статья теперь задаёт сниппет, в имя которого входит идентификатор статьи. Все эти динамические сниппеты вместе обёрнуты статическим сниппетом с именем `articlesContainer`. Если бы мы этот внешний сниппет опустили, Latte выбросил бы исключение. - -Остаётся добавить в презентер логику перерисовки - достаточно перерисовать статическую обёртку. - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - if ($this->isAjax()) { - $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- не нужно - } else { - $this->redirect('this'); - } -} -``` - -Измените похожим образом и соответствующий метод `handleUnlike()`, и AJAX работает! - -Однако у этого решения есть недостаток. Если присмотреться к AJAX-запросу, окажется, что снаружи приложение выглядит эффективным (возвращает только один сниппет конкретной статьи), но на стороне сервера отрисовывает *все* сниппеты. Оно кладёт нужный сниппет в payload, а остальные отбрасывает (то есть зря их получило и отрисовало). - -Чтобы это оптимизировать, нужно вмешаться там, где в шаблон передаётся коллекция `$articles` (скажем, в методе `renderDefault()`). Мы воспользуемся тем, что обработка сигнала происходит до методов `render<Something>`: - -```php -public function handleLike(int $articleId): void -{ - // ... - if ($this->isAjax()) { - // ... - $this->template->articles = [ - $this->db->table('articles')->get($articleId), - ]; - } else { - // ... -} - -public function renderDefault(): void -{ - if (!isset($this->template->articles)) { - $this->template->articles = $this->db->table('articles'); - } -} -``` - -Теперь при обработке сигнала вместо всей коллекции статей в шаблон передаётся массив всего с одной нужной статьёй - той, которую мы собираемся отрисовать и отправить в payload в браузер. В результате цикл `{foreach}` выполняется только один раз, и лишние сниппеты не отрисовываются. - - -Способ с компонентом -==================== - -Совершенно другой подход обходится без динамических сниппетов вообще. Хитрость в том, чтобы упаковать всю логику в отдельный компонент. Вместо того чтобы оценкой занимался презентер, ею будет управлять специальный `LikeControl`. Класс будет выглядеть так (в нём были бы также методы `render`, `handleUnlike` и другие): - -```php -class LikeControl extends Nette\Application\UI\Control -{ - public function __construct( - private Article $article, - ) { - } - - public function handleLike(): void - { - $this->ratingService->saveLike($this->article->id, $this->presenter->user->id); - if ($this->presenter->isAjax()) { - $this->redrawControl(); - } else { - $this->presenter->redirect('this'); - } - } -} -``` - -Шаблон компонента: - -```latte -{snippet} - {if !$article->liked} - <a n:href="like!" class=ajax>I like it</a> - {else} - <a n:href="unlike!" class=ajax>I don't like it anymore</a> - {/if} -{/snippet} -``` - -Разумеется, шаблон представления изменится, и нам понадобится добавить в презентер фабрику. Поскольку мы будем создавать экземпляр этого компонента для каждой статьи, полученной из базы данных, для управления их созданием мы используем класс [Multiplier |application:Multiplier]. - -```php -protected function createComponentLikeControl() -{ - $articles = $this->db->table('articles'); - return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { - return new LikeControl($articles[$articleId]); - }); -} -``` - -Шаблон представления сокращается до минимума (и полностью обходится без сниппетов!): - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {control "likeControl-$article->id"} -</article> -``` - -Мы почти закончили: теперь приложение работает с AJAX. И здесь тоже нужна оптимизация, потому что из-за использования Nette Database обработка сигнала зря загружает из базы все статьи вместо одной нужной. Преимущество, однако, в том, что лишней отрисовки не происходит: отрисовывается только конкретный экземпляр компонента. - -{{priority: -1}} diff --git a/best-practices/ru/form-reuse.texy b/best-practices/ru/form-reuse.texy deleted file mode 100644 index 74be343918..0000000000 --- a/best-practices/ru/form-reuse.texy +++ /dev/null @@ -1,348 +0,0 @@ -Повторное использование форм в нескольких местах -************************************************ - -.[perex] -Nette предлагает несколько способов использовать одну и ту же форму в нескольких местах, не дублируя код. Эта статья разбирает разные решения, в том числе те, которых стоит избегать. - - -Фабрика форм -============ - -Основной подход к повторному использованию компонента в нескольких местах - создать метод или класс, который его порождает. Этот метод затем вызывается из разных мест приложения. Такой метод или класс называется *фабрикой*. Не путайте это с шаблоном проектирования *factory method*, который описывает конкретный способ использования фабрик и с этой темой напрямую не связан. - -Для примера создадим фабрику, собирающую форму редактирования: - -```php -use Nette\Application\UI\Form; - -class FormFactory -{ - public function createEditForm(): Form - { - $form = new Form; - $form->addText('title', 'Title:'); - // здесь добавляются остальные поля формы - $form->addSubmit('send', 'Save'); - return $form; - } -} -``` - -Теперь вы можете использовать эту фабрику в разных частях приложения, например в презентерах или компонентах. Делается это через [запрос её как зависимости |dependency-injection:passing-dependencies]. Сначала зарегистрируйте класс в конфигурационном файле: - -```neon -services: - - FormFactory -``` - -Затем используйте её в презентере: - - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->createEditForm(); - $form->onSuccess[] = function () { - // обработка отправленных данных - }; - return $form; - } -} -``` - -Вы можете расширять фабрику форм новыми методами, создающими другие виды форм по потребностям вашего приложения. И, разумеется, мы можем добавить метод, создающий базовую форму без элементов, которым затем воспользуются остальные методы: - -```php -class FormFactory -{ - public function createForm(): Form - { - $form = new Form; - return $form; - } - - public function createEditForm(): Form - { - $form = $this->createForm(); - $form->addText('title', 'Title:'); - // здесь добавляются остальные поля формы - $form->addSubmit('send', 'Save'); - return $form; - } -} -``` - -Метод `createForm()` пока не делает ничего полезного, но скоро это изменится. - - -Зависимости фабрики -=================== - -Со временем может понадобиться, чтобы формы стали многоязычными. Это значит, что всем формам нужно задать [переводчик |forms:rendering#Перевод]. Чтобы этого добиться, измените класс `FormFactory` так, чтобы он принимал объект `Translator` как зависимость в конструкторе и передавал его создаваемой форме: - -```php -use Nette\Localization\Translator; - -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function createForm(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } - - // ... -} -``` - -Поскольку метод `createForm()` вызывается и другими методами, создающими конкретные формы, задать переводчик здесь достаточно. И готово. Менять код какого-либо презентера или компонента не нужно, и это прекрасно. - - -Несколько классов-фабрик -======================== - -Как вариант, вы можете создать отдельный класс-фабрику для каждой формы, которую собираетесь использовать в приложении. Такой подход может улучшить читаемость кода и упростить управление формами. Пусть исходная `FormFactory` создаёт только базовую форму с основными настройками (например, с поддержкой перевода), а для формы редактирования создадим новую фабрику `EditFormFactory`. - -```php -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function create(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } -} - - -// ✅ используем композицию -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - // здесь добавляются остальные поля формы - $form->addSubmit('send', 'Save'); - return $form; - } -} -``` - -Принципиально важно, что связь между классами `FormFactory` и `EditFormFactory` реализована через [композицию |nette:introduction-to-object-oriented-programming#Композиция], а не через [наследование объектов |nette:introduction-to-object-oriented-programming#Наследование]: - -```php -// ⛔ НЕТ! НАСЛЕДОВАНИЮ ЗДЕСЬ НЕ МЕСТО -class EditFormFactory extends FormFactory -{ - public function create(): Form - { - $form = parent::create(); - $form->addText('title', 'Title:'); - // здесь добавляются остальные поля формы - $form->addSubmit('send', 'Save'); - return $form; - } -} -``` - -Наследование здесь было бы совершенно контрпродуктивно. Проблемы возникли бы очень быстро. Например, если бы вы захотели добавить параметры в метод `create()`, PHP выдал бы ошибку, потому что его сигнатура отличалась бы от родительской. Или при передаче зависимостей в класс `EditFormFactory` через конструктор. Это привело бы к тому, что известно как [ад конструкторов |dependency-injection:passing-dependencies#Ад конструкторов]. - -Вообще лучше предпочитать [композицию наследованию |dependency-injection:faq#Почему композиция предпочтительнее наследования?]. - - -Обработка формы -=============== - -Обработчик формы, вызываемый при успешной отправке, тоже может быть частью класса-фабрики. Он работает так, что передаёт отправленные данные на обработку в слой модели. Любые ошибки обработки передаются [обратно |forms:validation#Обработка ошибок] в форму. В следующем примере модель представлена классом `Facade`: - -```php -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - private Facade $facade, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - $form->addText('title', 'Title:'); - // здесь добавляются остальные поля формы - $form->addSubmit('send', 'Save'); - $form->onSuccess[] = $this->processForm(...); - return $form; - } - - private function processForm(Form $form, array $data): void - { - try { - // обработка отправленных данных - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - } - } -} -``` - -А вот перенаправление пусть выполняет сам презентер. Он добавляет в событие `onSuccess` ещё один обработчик, который и перенаправляет. Благодаря этому форму можно использовать в разных презентерах, каждый из которых при успехе перенаправляет в своё место. - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditFormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->create(); - $form->onSuccess[] = function () { - $this->flashMessage('Record was saved'); - $this->redirect('Homepage:'); - }; - return $form; - } -} -``` - -Это решение опирается на особенность форм: если у формы или у одного из её элементов вызван `addError()`, последующие обработчики `onSuccess` не вызываются. - - -Наследование от класса Form -=========================== - -Собранная форма не должна быть потомком класса `Form`. Иными словами, избегайте такого подхода: - -```php -// ⛔ НЕТ! НАСЛЕДОВАНИЮ ЗДЕСЬ НЕ МЕСТО -class EditForm extends Form -{ - public function __construct(Translator $translator) - { - parent::__construct(); - $this->addText('title', 'Title:'); - // здесь добавляются остальные поля формы - $this->addSubmit('send', 'Save'); - $this->setTranslator($translator); - } -} -``` - -Вместо сборки формы в конструкторе используйте фабрику. - -Важно осознавать, что класс `Form` - это прежде всего инструмент для сборки форм, то есть *построитель форм*. Собранную форму можно считать его продуктом. Однако продукт не является разновидностью построителя; между ними нет отношения *является*, которое лежит в основе наследования. - - -Компонент с формой -================== - -Совершенно другой подход состоит в создании [компонента |application:components], который скрывает в себе форму. Это открывает новые возможности, например отрисовку формы особым образом, потому что у компонента есть собственный шаблон. Кроме того, можно использовать сигналы для AJAX-обмена и динамической подгрузки сведений в форму, например для подсказок и подобного. - - -```php -use Nette\Application\UI\Form; - -class EditControl extends Nette\Application\UI\Control -{ - public array $onSave = []; - - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentForm(): Form - { - $form = new Form; - $form->addText('title', 'Title:'); - // здесь добавляются остальные поля формы - $form->addSubmit('send', 'Save'); - $form->onSuccess[] = $this->processForm(...); - - return $form; - } - - private function processForm(Form $form, array $data): void - { - try { - // обработка отправленных данных - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - return; - } - - // вызов события - $this->onSave($this, $data); - } -} -``` - -Далее создадим фабрику, которая будет порождать этот компонент. Достаточно [определить её интерфейс |application:components#Компоненты с зависимостями]: - -```php -interface EditControlFactory -{ - function create(): EditControl; -} -``` - -И добавить его в конфигурационный файл: - -```neon -services: - - EditControlFactory -``` - -Теперь мы можем запросить фабрику и использовать её в презентере: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditControlFactory $controlFactory, - ) { - } - - protected function createComponentEditForm(): EditControl - { - $control = $this->controlFactory->create(); - - $control->onSave[] = function (EditControl $control, $data) { - $this->redirect('this'); - // или перенаправление на результат редактирования, например: - // $this->redirect('detail', ['id' => $data->id]); - }; - - return $control; - } -} -``` diff --git a/best-practices/ru/inject-method-attribute.texy b/best-practices/ru/inject-method-attribute.texy deleted file mode 100644 index c09a70b160..0000000000 --- a/best-practices/ru/inject-method-attribute.texy +++ /dev/null @@ -1,61 +0,0 @@ -Методы и атрибуты inject -************************ - -.[perex] -Эта статья посвящена разным способам передачи зависимостей в презентеры фреймворка Nette. Мы сравним предпочтительный способ, внедрение через конструктор, с альтернативами в виде методов и атрибутов `inject`. - -Для презентеров, как и для других классов, предпочтительным подходом является передача зависимостей через [конструктор |dependency-injection:passing-dependencies#Внедрение через конструктор]. Однако если вы создаёте общего предка, от которого наследуются другие презентеры (например, `BasePresenter`), и этому предку тоже нужны зависимости, может возникнуть проблема, известная как [ад конструкторов |dependency-injection:passing-dependencies#Ад конструкторов]. Её можно обойти альтернативными способами, а именно методами и атрибутами inject (раньше аннотациями). - - -Методы `inject*()` -================== - -Это разновидность передачи зависимостей через [сеттеры |dependency-injection:passing-dependencies#Внедрение через сеттер]. Имена таких сеттеров должны начинаться с префикса `inject`. Nette DI автоматически вызывает методы с такими именами сразу после создания экземпляра презентера и передаёт им все нужные зависимости. Поэтому они должны быть объявлены как public. - -Методы `inject*()` можно воспринимать как расширение конструктора, разбитое на несколько методов. Это позволяет `BasePresenter` получать свои зависимости отдельным методом, оставляя конструктор свободным для его потомков: - -```php -abstract class BasePresenter extends Nette\Application\UI\Presenter -{ - private Foo $foo; - - public function injectBase(Foo $foo): void - { - $this->foo = $foo; - } -} - -class MyPresenter extends BasePresenter -{ - private Bar $bar; - - public function __construct(Bar $bar) - { - $this->bar = $bar; - } -} -``` - -У презентера может быть сколько угодно методов `inject*()`, и каждый может принимать сколько угодно параметров. Такой подход хорошо подходит и для случаев, когда презентер [составлен из трейтов |presenter-traits] и каждому трейту нужны свои зависимости. - - -Атрибуты `Inject` -================= - -Это разновидность [внедрения в свойства |dependency-injection:passing-dependencies#Внедрение в свойство]. Достаточно пометить свойства, в которые нужно внедрить зависимости, и Nette DI автоматически передаст их сразу после создания экземпляра презентера. Чтобы внедрение было возможно, эти свойства должны быть объявлены как public. - -Свойства помечаются атрибутом (раньше использовалась аннотация `/** @inject */`): - -```php -use Nette\DI\Attributes\Inject; // эта строка важна - -class MyPresenter extends Nette\Application\UI\Presenter -{ - #[Inject] - public Cache $cache; -} -``` - -Преимуществом этого способа передачи зависимостей была очень краткая запись. Однако с появлением [продвижения свойств конструктора |https://blog.nette.org/ru/php-8-0-complete-overview-of-news#toc-constructor-property-promotion] использование конструктора часто выглядит проще. - -С другой стороны, этот способ страдает теми же недостатками, что и внедрение в свойства вообще: у нас нет контроля над изменениями переменной, а сама переменная становится частью публичного интерфейса класса, что обычно нежелательно. diff --git a/best-practices/ru/lets-create-contact-form.texy b/best-practices/ru/lets-create-contact-form.texy deleted file mode 100644 index f94b8b5113..0000000000 --- a/best-practices/ru/lets-create-contact-form.texy +++ /dev/null @@ -1,218 +0,0 @@ -Создадим форму обратной связи -***************************** - -.[perex] -Разберём, как создать в Nette форму обратной связи, включая отправку введённых данных по электронной почте. Приступим! - -Сначала нужно создать новый проект. Как это сделать, объясняет страница [Первые шаги |nette:installation]. Затем можно приступить к созданию формы. - -Проще всего создать [форму прямо в презентере |forms:in-presenter]. Мы можем воспользоваться уже существующим `HomePresenter`. Мы добавим компонент с именем `contactForm`, представляющий нашу форму. Для этого добавим в код презентера фабричный метод `createComponentContactForm()`, который создаст компонент: - -```php -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - protected function createComponentContactForm(): Form - { - $form = new Form; - $form->addText('name', 'Имя:') - ->setRequired('Введите, пожалуйста, своё имя'); - $form->addEmail('email', 'E-mail:') - ->setRequired('Введите, пожалуйста, свой e-mail'); - $form->addTextArea('message', 'Сообщение:') - ->setRequired('Введите, пожалуйста, сообщение'); - $form->addSubmit('send', 'Отправить'); - $form->onSuccess[] = $this->contactFormSucceeded(...); - return $form; - } - - private function contactFormSucceeded(Form $form, $data): void - { - // отправка письма - } -} -``` - -Как видите, мы создали два метода. Первый, `createComponentContactForm()`, создаёт новый экземпляр формы. В нём есть поля для имени, e-mail и сообщения, добавленные соответственно методами `addText()`, `addEmail()` и `addTextArea()`. Мы также добавили кнопку отправки. А что, если пользователь оставит поле пустым? В этом случае нам следует сообщить ему, что поле обязательно. Этого мы добились методом `setRequired()`. Наконец, мы привязали обработчик [события |nette:glossary#События] к `onSuccess`, который срабатывает при успешной отправке формы. В нашем случае он вызывает метод `contactFormSucceeded`, который займётся обработкой отправленных данных. Реализуем его чуть позже. - -Отрисуем компонент `contactForm` в шаблоне `Home/default.latte`: - -```latte -{block content} -<h1>Форма обратной связи</h1> -{control contactForm} -``` - -Для самой отправки письма создадим новый класс с именем `ContactFacade` и поместим его в файл `app/Model/ContactFacade.php`: - -```php -namespace App\Model; - -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $mail = new Message; - $mail->addTo('admin@example.com') // ваш e-mail - ->setFrom($email, $name) - ->setSubject('Сообщение из формы обратной связи') - ->setBody($message); - - $this->mailer->send($mail); - } -} -``` - -Метод `sendMessage()` создаёт и отправляет письмо. Для этого он использует сервис отправки почты, который получает как зависимость через конструктор. Подробнее об [отправке писем |mail:]. - -Теперь вернёмся в презентер и допишем метод `contactFormSucceeded()`. Он вызовет метод `sendMessage()` класса `ContactFacade` и передаст данные, отправленные через форму. А как получить объект `ContactFacade`? Мы запросим его через конструктор с помощью внедрения зависимостей: - -```php -use App\Model\ContactFacade; -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - public function __construct( - private ContactFacade $facade, - ) { - } - - protected function createComponentContactForm(): Form - { - // ... - } - - public function contactFormSucceeded(stdClass $data): void - { - $this->facade->sendMessage($data->email, $data->name, $data->message); - $this->flashMessage('Сообщение отправлено'); - $this->redirect('this'); - } -} -``` - -После отправки письма мы показываем пользователю [flash-сообщение |application:components#Flash-сообщения], подтверждающее отправку. Затем перенаправляем, чтобы форму нельзя было отправить повторно обновлением страницы в браузере. - - -Итак, если всё настроено правильно, теперь вы должны иметь возможность отправить письмо из своей формы обратной связи. Поздравляем! - - -HTML-шаблон письма ------------------- - -Сейчас отправляется обычное текстовое письмо, содержащее только сообщение, отправленное через форму. Однако мы можем использовать в письме HTML, чтобы его вид стал привлекательнее. Создадим для него шаблон на Latte и сохраним как `app/Model/contactEmail.latte`: - -```latte -<html> - <title>Сообщение из формы обратной связи - - -

    Имя: {$name}

    -

    E-mail: {$email}

    -

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

    - - -``` - -Остаётся изменить `ContactFacade`, чтобы он использовал этот шаблон. В конструкторе мы запросим класс `LatteFactory`, который умеет создавать объект `Latte\Engine`, [отрисовщик шаблонов Latte |latte:develop#Как отрисовать шаблон]. Методом `renderToString()` мы отрисуем шаблон в строку. Первый параметр - путь к файлу шаблона, второй - массив переменных, которые в него передаются. - -```php -namespace App\Model; - -use Nette\Bridges\ApplicationLatte\LatteFactory; -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $latte = $this->latteFactory->create(); - $body = $latte->renderToString(__DIR__ . '/contactEmail.latte', [ - 'email' => $email, - 'name' => $name, - 'message' => $message, - ]); - - $mail = new Message; - $mail->addTo('admin@example.com') // ваш e-mail - ->setFrom($email, $name) - ->setHtmlBody($body); - - $this->mailer->send($mail); - } -} -``` - -Получившееся HTML-содержимое письма мы затем передаём в метод `setHtmlBody()` вместо исходного `setBody()`. Тему письма через `setSubject()` указывать тоже не нужно, потому что библиотека автоматически берёт её из элемента `` в шаблоне. - - -Настройка ---------- - -В коде класса `ContactFacade` наш e-mail администратора `admin@example.com` по-прежнему прописан жёстко. Лучше было бы перенести его в конфигурационный файл. Как это сделать? - -Сначала изменим класс `ContactFacade`, заменив жёстко прописанную строку с e-mail переменной, передаваемой через конструктор: - -```php -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - private string $adminEmail, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - // ... - $mail = new Message; - $mail->addTo($this->adminEmail) - ->setFrom($email, $name) - ->setHtmlBody($body); - // ... - } -} -``` - -Второй шаг - задать значение этой переменной в конфигурации. В файл `app/config/services.neon` добавьте: - -```neon -services: - - App\Model\ContactFacade(adminEmail: admin@example.com) -``` - -Вот и всё. Если в секции `services` много записей и вам кажется, что адрес электронной почты в них теряется, его можно превратить в параметр. Измените запись так: - -```neon -services: - - App\Model\ContactFacade(adminEmail: %adminEmail%) -``` - -А сам параметр определите в файле `app/config/common.neon`: - -```neon -parameters: - adminEmail: admin@example.com -``` - -И готово! diff --git a/best-practices/ru/microsites.texy b/best-practices/ru/microsites.texy deleted file mode 100644 index d7bb6f89af..0000000000 --- a/best-practices/ru/microsites.texy +++ /dev/null @@ -1,63 +0,0 @@ -Как писать микросайты -********************* - -Представьте, что вам нужно быстро сделать небольшой сайт к предстоящему мероприятию вашей компании. Он должен быть простым, быстрым и без лишних усложнений. Вы можете подумать, что для такого маленького проекта мощный фреймворк не нужен. Но что, если использование Nette Framework на самом деле упростит и ускорит этот процесс? - -Даже создавая простые сайты, вы не хотите жертвовать удобством. Вы не хотите заново изобретать то, что уже решено. Спокойно ленитесь и позвольте себя побаловать. 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); - return $presenter->redirectUrl("/$lang"); -}); - -// маршрут для URL https://example.com/cs или https://example.com/en -$router->addRoute('<lang cs|en|de>', function ($presenter, string $lang) { - // выводим подходящий шаблон, например ../templates/en.latte - $template = $presenter->createTemplate() - ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); - return $template; -}); - -// запускаем приложение! -$container->getByType(Nette\Application\Application::class)->run(); -``` - -Всё остальное будет шаблонами, лежащими в родительском каталоге `/templates`. - -PHP-код в `index.php` сначала [настраивает окружение |bootstrap:], затем задаёт [маршруты |application:routing#Динамическая маршрутизация с callback-функциями] и наконец запускает приложение. Преимущество в том, что вторым параметром функции `addRoute()` может быть callable, который выполняется при обращении к соответствующей странице. - - -Зачем использовать Nette для микросайтов? ------------------------------------------ - -- Программисты, попробовавшие [Tracy|tracy:], сегодня едва ли представляют себе разработку без неё. -- Прежде всего вам пригодится система шаблонов [Latte|latte:], потому что даже при двух страницах вам захочется разделить [макет и содержимое|latte:template-inheritance]. -- И вы наверняка захотите положиться на [автоматическое экранирование |latte:safety-first], чтобы предотвратить XSS-уязвимости. -- Nette также следит за тем, чтобы при ошибке никогда не отображались сырые сообщения об ошибках PHP: вместо них показывается дружелюбная страница. -- Если вы хотите собирать отзывы пользователей, скажем через форму обратной связи, вы легко добавите поддержку [форм|forms:] и [базы данных|database:]. -- Заполненные формы можно легко [отправлять по электронной почте|mail:]. -- Иногда может пригодиться [кеширование|caching:], например при загрузке и выводе лент. - -В сегодняшнем быстром мире, где скорость и эффективность решают, жизненно важно иметь инструменты, позволяющие получать результат без лишних задержек. Nette Framework предлагает именно это - быструю разработку, безопасность и широкий набор инструментов вроде Tracy и Latte, которые упрощают процесс. Достаточно установить несколько пакетов Nette, и создание такого микросайта становится невероятно простым. И вы можете быть уверены, что скрытых уязвимостей в нём нет. diff --git a/best-practices/ru/pagination.texy b/best-practices/ru/pagination.texy deleted file mode 100644 index 8929e32ffa..0000000000 --- a/best-practices/ru/pagination.texy +++ /dev/null @@ -1,273 +0,0 @@ -Постраничный вывод результатов из базы данных -********************************************* - -.[perex] -Разрабатывая веб-приложения, вы часто сталкиваетесь с требованием ограничить количество выводимых элементов на странице - приёмом, известным как пагинация. - -Начнём с состояния, когда мы выводим все данные без пагинации. Для выборки данных из базы у нас есть класс `ArticleRepository`. Помимо конструктора в нём есть метод `findPublishedArticles`, возвращающий все опубликованные статьи, отсортированные по убыванию даты публикации. - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC', - new \DateTime, - ); - } -} -``` - -Затем мы внедряем этот класс модели в презентер. В методе render получаем опубликованные статьи и передаём их в шаблон: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(): void - { - $this->template->articles = $this->articleRepository->findPublishedArticles(); - } -} -``` - -Шаблон `default.latte` затем позаботится о выводе статей: - -```latte -{block content} -<h1>Статьи</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> -``` - - -Так мы можем вывести все статьи, но это становится проблемой по мере роста их количества. Тогда и пригождается механизм пагинации. - -Этот механизм делит все статьи на несколько страниц, и мы показываем только те статьи, которые относятся к выбранной странице. Общее число страниц и распределение статей вычисляет вспомогательный класс [Paginator |utils:Paginator] на основе общего числа статей и желаемого числа статей на странице. - -На первом шаге изменим в классе репозитория метод получения статей так, чтобы он мог возвращать статьи только для одной страницы. Добавим также метод получения общего числа статей в базе, который нужен для настройки Paginator: - -```php -namespace App\Model; - -use Nette; - - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(int $limit, int $offset): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC - LIMIT ? - OFFSET ?', - new \DateTime, $limit, $offset, - ); - } - - /** - * Возвращает общее число опубликованных статей - */ - public function getPublishedArticlesCount(): int - { - return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime); - } -} -``` - -Далее изменим презентер. Мы передадим в метод `renderDefault` номер текущей страницы. Если этого номера нет в URL, зададим значение по умолчанию 1 (первая страница). - -Мы также расширим метод render, чтобы создать и настроить экземпляр Paginator и выбрать подходящие статьи для вывода в шаблоне. Изменённый `HomePresenter` будет выглядеть так: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Получаем общее число опубликованных статей - $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - - // Создаём и настраиваем экземпляр Paginator - $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // общее число элементов - $paginator->setItemsPerPage(10); // число элементов на странице - $paginator->setPage($page); // номер текущей страницы - - // Получаем из базы ограниченный набор статей по расчёту Paginator - $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - - // передаём их в шаблон - $this->template->articles = $articles; - // а также сам Paginator для вывода элементов пагинации - $this->template->paginator = $paginator; - } -} -``` - -Шаблон теперь обходит только статьи текущей страницы. Нам остаётся добавить ссылки пагинации: - -```latte -{block content} -<h1>Статьи</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if !$paginator->isFirst()} - <a n:href="default, 1">Первая</a> -  |  - <a n:href="default, $paginator->getPage() - 1">Предыдущая</a> -  |  - {/if} - - Страница {$paginator->getPage()} из {$paginator->getPageCount()} - - {if !$paginator->isLast()} -  |  - <a n:href="default, $paginator->getPage() + 1">Следующая</a> -  |  - <a n:href="default, $paginator->getPageCount()">Последняя</a> - {/if} -</div> -``` - - -На этом реализация пагинации с помощью Paginator закончена. Если в качестве слоя работы с базой данных вы используете [Nette Database Explorer |database:explorer] вместо [Nette Database Core |database:sql-way], вы можете реализовать пагинацию и без прямого использования Paginator. В классе `Nette\Database\Table\Selection` есть метод [page() |api:Nette\Database\Table\Selection::page()], который скрывает в себе логику пагинации. - -При таком подходе репозиторий будет выглядеть так: - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Explorer $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\Table\Selection - { - return $this->database->table('articles') - ->where('created_at < ', new \DateTime) - ->order('created_at DESC'); - } -} -``` - -В презентере нам не нужно создавать экземпляр Paginator. Вместо этого мы используем метод `page()` объекта `Selection`, возвращённого из репозитория: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Получаем опубликованные статьи - $articles = $this->articleRepository->findPublishedArticles(); - - // и передаём в шаблон только их часть, ограниченную расчётом метода page - $lastPage = 0; - $this->template->articles = $articles->page($page, 10, $lastPage); - - // а также данные, нужные для вывода элементов пагинации - $this->template->page = $page; - $this->template->lastPage = $lastPage; - } -} -``` - -Поскольку объект Paginator в шаблон мы больше не передаём, нужно изменить часть, выводящую ссылки пагинации: - -```latte -{block content} -<h1>Статьи</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if $page > 1} - <a n:href="default, 1">Первая</a> -  |  - <a n:href="default, $page - 1">Предыдущая</a> -  |  - {/if} - - Страница {$page} из {$lastPage} - - {if $page < $lastPage} -  |  - <a n:href="default, $page + 1">Следующая</a> -  |  - <a n:href="default, $lastPage">Последняя</a> - {/if} -</div> -``` - -Так мы реализовали механизм пагинации без явного использования вспомогательного класса Paginator. - -{{priority: -1}} diff --git a/best-practices/ru/passing-settings-to-presenters.texy b/best-practices/ru/passing-settings-to-presenters.texy deleted file mode 100644 index 7e2dd4013f..0000000000 --- a/best-practices/ru/passing-settings-to-presenters.texy +++ /dev/null @@ -1,49 +0,0 @@ -Передача настроек в презентеры -****************************** - -.[perex] -Нужно передать в презентеры не объектные аргументы (например, флаг режима отладки, пути к каталогам и подобное), которые нельзя передать автоматически через autowiring? Решение - упаковать их в отдельный объект `Settings`. - -Сервис `Settings` даёт очень простой, но действенный способ снабдить презентеры сведениями о работающем приложении. Его конкретная структура полностью зависит от ваших нужд. Пример: - -```php -namespace App; - -class Settings -{ - public function __construct( - // начиная с PHP 8.1 можно использовать readonly - public bool $debugMode, - public string $appDir, - // и так далее - ) {} -} -``` - -Пример регистрации в конфигурации: - -```neon -services: - - App\Settings( - %debugMode%, - %appDir%, - ) -``` - -Когда презентеру нужны сведения, предоставляемые этим сервисом, он просто запрашивает его в своём конструкторе: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private App\Settings $settings, - ) {} - - public function renderDefault() - { - if ($this->settings->debugMode) { - // ... - } - } -} -``` diff --git a/best-practices/ru/post-links.texy b/best-practices/ru/post-links.texy deleted file mode 100644 index fd92f9326f..0000000000 --- a/best-practices/ru/post-links.texy +++ /dev/null @@ -1,56 +0,0 @@ -Как правильно использовать POST-ссылки -************************************** - -.[perex] -В веб-приложениях, особенно в административных интерфейсах, основным правилом должно быть то, что действия, изменяющие состояние на сервере, не выполняются HTTP-методом GET. Как следует из названия, GET должен служить только для получения данных, а не для их изменения. Для действий вроде удаления записей уместнее метод POST. Идеальным был бы метод DELETE, но вызвать его без JavaScript нельзя, поэтому исторически для таких действий используется POST. - -Как реализовать это на практике? Воспользуйтесь простым приёмом. В начале шаблона макета создайте вспомогательную форму с идентификатором `postForm`. Затем вы будете использовать её для действий вроде кнопок удаления: - -```latte .{file:@layout.latte} -<form method="post" id="postForm"></form> -``` - -Благодаря этой форме вместо обычной ссылки `<a>` вы можете использовать `<button>`. Эту кнопку можно оформить так, чтобы она выглядела как обычная ссылка. Например, CSS-фреймворк Bootstrap предлагает классы `btn btn-link`, благодаря которым кнопка визуально не отличается от других ссылок. Атрибутом `form="postForm"` привяжите кнопку к подготовленной вспомогательной форме: - -```latte .{file:admin.latte} -<table> - <tr n:foreach="$posts as $post"> - <td>{$post->title}</td> - <td> - <button class="btn btn-link" form="postForm" formaction="{link delete, $post->id}">delete</button> - <!-- вместо <a n:href="delete, $post->id">delete</a> --> - </td> - </tr> -</table> -``` - -Нажатие на эту кнопку теперь вызывает действие `delete`. Чтобы запросы принимались только методом POST и только с того же домена (действенная защита от CSRF-атак), используйте атрибут `#[Requires]`: - -```php .{file:AdminPresenter.php} -use Nette\Application\Attributes\Requires; - -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST', sameOrigin: true)] - public function actionDelete(int $id): void - { - $this->facade->deletePost($id); // гипотетический код удаления записи - $this->redirect('default'); - } -} -``` - -Этот атрибут доступен начиная с Nette Application 3.2. Подробнее о его возможностях можно узнать на странице [Как использовать атрибут #Requires |attribute-requires]. - -Если вместо действия `actionDelete()` вы использовали сигнал `handleDelete()`, указывать `sameOrigin: true` не нужно, потому что у сигналов эта защита включена по умолчанию: - -```php .{file:AdminPresenter.php} -#[Requires(methods: 'POST')] -public function handleDelete(int $id): void -{ - $this->facade->deletePost($id); - $this->redirect('this'); -} -``` - -Такой подход не только повышает безопасность вашего приложения, но и способствует соблюдению правильных веб-стандартов и практик. Использование методов POST для действий, меняющих состояние, делает приложение надёжнее и безопаснее. diff --git a/best-practices/ru/presenter-traits.texy b/best-practices/ru/presenter-traits.texy deleted file mode 100644 index 180271a230..0000000000 --- a/best-practices/ru/presenter-traits.texy +++ /dev/null @@ -1,47 +0,0 @@ -Составление презентеров из трейтов -********************************** - -.[perex] -Если вам нужно реализовать одну и ту же функциональность в нескольких презентерах (например, проверку входа пользователя), обычным подходом будет разместить код в общем предке. Другой вариант - создать узконаправленные [трейты |nette:introduction-to-object-oriented-programming#Трейты]. - -Преимущество трейтов в том, что каждый презентер может подключить только те трейты, которые ему действительно нужны, тем более что множественное наследование в PHP не поддерживается. - -Эти трейты могут пользоваться тем, что все [методы inject |inject-method-attribute#Методы inject*()] вызываются друг за другом при создании экземпляра презентера. Нужно лишь позаботиться о том, чтобы имя каждого метода inject было уникальным среди всех используемых трейтов и самого презентера. - -Трейты могут привязать код инициализации к событиям [onStartup или onRender |application:presenters#События]. - -Примеры: - -```php -trait RequireLoggedUser -{ - public function injectRequireLoggedUser(): void - { - $this->onStartup[] = function () { - if (!$this->getUser()->isLoggedIn()) { - $this->redirect('Sign:in', $this->storeRequest()); - } - }; - } -} - -trait StandardTemplateFilters -{ - public function injectStandardTemplateFilters(TemplateBuilder $builder): void - { - $this->onRender[] = function () use ($builder) { - $builder->setupTemplate($this->template); - }; - } -} -``` - -Презентер затем просто использует эти трейты: - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - use StandardTemplateFilters; - use RequireLoggedUser; -} -``` diff --git a/best-practices/ru/pretty-urls.texy b/best-practices/ru/pretty-urls.texy deleted file mode 100644 index 4f0f0b0e73..0000000000 --- a/best-practices/ru/pretty-urls.texy +++ /dev/null @@ -1,204 +0,0 @@ -Красивые URL со слагами -*********************** - -.[perex] -URL вида `/article/123-how-to-bake-bread` выглядят лучше, чем `/article/123`, и помогают и пользователям, и поисковым системам понять, что на странице. Это руководство показывает, как формировать их целиком в маршрутизаторе, не трогая ни одного шаблона, и как позаботиться о том, чтобы каждый посетитель попадал на канонический URL. - - -Зачем слаги в URL -================= - -Сравните два адреса: - -``` -/article/123 -/article/123-how-to-bake-bread -``` - -Второй сообщает пользователю (и Google), что ждёт его после щелчка. Это хорошо для SEO, делает ссылки читаемыми в чате или письме и придаёт адресной строке смысл. - -При этом слаг не является настоящим идентификатором. Страницу определяет ID. Слаг - украшение, которое приложение формирует из заголовка. Если заголовок меняется, должен меняться и слаг. А если кто-то отредактирует URL вручную или перейдёт по старой ссылке, приложение всё равно должно найти нужную страницу. - - -Цель -==== - -Мы хотим маршрут, который справится со всем этим: - -``` -/article/123 → открывает статью 123, перенаправляет на канонический URL -/article/123-how-to-bake-bread → открывает статью 123 напрямую -/article/123-anything-someone-typed → открывает статью 123, перенаправляет на канонический URL -/article/ → 404 (нет ID) -``` - -И мы хотим, чтобы каждый вызов `n:href` и `link()` во всём приложении автоматически порождал `/article/123-how-to-bake-bread` - **без переписывания хотя бы одного шаблона**. - - -Маска маршрута -============== - -Хитрость в том, чтобы пометить слаг в маске как **необязательный** с помощью квадратных скобок: - -```php -$router->addRoute('article/<id [0-9]+>[-<slug>]', 'Article:detail'); -``` - -Маска `[-<slug>]` говорит: после ID может быть дефис и слаг, но это не обязательно. Маршрут принимает и `/article/123`, и `/article/123-anything`. - -Замечание о параметре `<slug>`: по умолчанию он совпадает с любыми символами, **кроме слеша**, - именно то, что нам нужно. Если вы напишете `<slug .+>`, параметр будет совпадать и со слешами, поэтому `/article/123-something/else` разберётся как один слаг, содержащий `/`. Оставайтесь при значении по умолчанию `<slug>`, если вам это действительно не нужно. - -Пока URL разбирается правильно, но в порождаемых ссылках слага не будет. Следующий шаг - научить маршрут его подставлять. - - -Формирование слага без правки шаблонов -====================================== - -Это самый выигрышный вариант. Существующие вызовы `n:href="Article:detail, $id"` продолжают работать без изменений во всём приложении: маршрутизатор сам находит заголовок. - -Мы делаем это **общим фильтром** под ключом-пустой строкой: он видит все параметры сразу и может добавить слаг: - -```php -use Nette\Routing\Route; -use Nette\Utils\Strings; - -$router->addRoute('article/<id [0-9]+>[-<slug>]', [ - 'presenter' => 'Article', - 'action' => 'detail', - '' => [ - Route::FilterOut => function (array $params) use ($slugProvider): array { - if (isset($params['id']) && empty($params['slug'])) { - $params['slug'] = $slugProvider->getSlug((int) $params['id']); - } - return $params; - }, - ], -]); -``` - -`FilterOut` выполняется каждый раз, когда маршрутизатор **порождает** URL. Если слаг не был передан, фильтр находит заголовок и добавляет его. - -Слаги можно развернуть по всему приложению одним изменением - в одном определении маршрута. Каждая ссылка в каждом шаблоне начнёт автоматически выдавать `/article/123-how-to-bake-bread`. Никакого grep, никакой охоты по шаблонам, никаких пропущенных углов. - - -Кешируйте поиск -=============== - -Одна ссылка порождает один запрос к базе, но на типичной странице их много: списки, хлебные крошки, "последние просмотренные", похожие статьи. Один и тот же идентификатор статьи часто встречается в нескольких ссылках в пределах одного запроса, и обращаться к базе каждый раз не хочется. - -Небольшой кеш в пределах запроса решает задачу. Оберните обращение к базе в маленький сервис: - -```php -final class SlugProvider -{ - /** @var array<int, string> */ - private array $cache = []; - - public function __construct( - private Nette\Database\Explorer $db, - ) { - } - - public function getSlug(int $id): string - { - return $this->cache[$id] ??= Strings::webalize(Strings::truncate( - (string) $this->db->fetchField('SELECT title FROM article WHERE id = ?', $id), - 100, '' - )); - } -} -``` - -Этого достаточно - одно обращение к базе на каждый уникальный ID в пределах запроса. - - -Передача заголовка из шаблона (необязательный быстрый путь) -=========================================================== - -Когда заголовок уже под рукой в шаблоне, обращение к базе можно вообще пропустить. Передайте заголовок именованным параметром: - -```latte -<a n:href="Article:detail, $article->id, slug => $article->title">{$article->title}</a> -``` - -…и добавьте `FilterOut` для отдельного параметра, который превращает заголовок в строку, пригодную для URL: - -```php -$router->addRoute('article/<id [0-9]+>[-<slug>]', [ - 'presenter' => 'Article', - 'action' => 'detail', - 'slug' => [ - Route::FilterOut => fn($title) => Strings::webalize(Strings::truncate($title, 100, '')), - ], - '' => [/* запасной поиск из примера выше */], -]); -``` - -Два фильтра работают вместе. Общий фильтр выполняется первым; видя, что слаг уже заполнен переданным заголовком, он пропускает обращение к базе. Затем `FilterOut` отдельного параметра превращает этот заголовок в полноценный слаг. Шаблоны, которые заголовок не передают, тоже работают: общий фильтр видит пустой слаг и идёт путём поиска. - -Используйте это только там, где это важно (большие списки, отрисовываемые сотнями за запрос). Для большей части приложения кешированного поиска достаточно. - - -Канонизация: перенаправление на правильный URL -============================================== - -Теперь мы умеем порождать `/article/123-how-to-bake-bread`, но маршрут по-прежнему принимает `/article/123` и `/article/123-anything-someone-wrote`. Это сделано намеренно: нам нужны короткие URL (подробнее ниже) и нужно, чтобы старые или набранные вручную ссылки продолжали работать. Но нам не нужно, чтобы поисковые системы индексировали одну и ту же статью по нескольким адресам. - -Решение - [канонизация |application:presenters#Канонизация]: когда пользователь приходит по неканоническому URL, приложение перенаправляет его на правильный с кодом 301. Этим занимается метод `canonicalize()`: - -```php -public function actionDetail(int $id, ?string $slug = null): void -{ - $article = $this->facade->getArticle($id); - if (!$article) { - $this->error(); - } - - // порождает канонический URL через тот же FilterOut - // и перенаправляет с HTTP 301, если он отличается от текущего URL - $this->canonicalize('detail', ['id' => $id]); - - $this->template->article = $article; -} -``` - -`canonicalize()` порождает канонический URL так же, как это сделал бы `link()` (то есть проходит через тот же `FilterOut`), и сравнивает его с текущим URL. Если они различаются, происходит перенаправление с HTTP 301. Посетители попадают на правильный URL, а поисковые системы видят только одну каноническую версию. - - -Одно место, определяющее вид слага -================================== - -Обратите внимание, что вызов `Strings::webalize(Strings::truncate(..., 100, ''))` живёт в одном месте - внутри `SlugProvider` (или в `FilterOut` отдельного параметра). Одна и та же логика порождает ссылку в шаблоне, URL в `redirect()` и каноническую форму в `canonicalize()`. - -Если позже вы захотите изменить правила (другой предел длины, другая транслитерация, удаление лишних символов), вы поменяете одну строку. Иначе вы рисковали бы тем, что `redirect()` породит `/article/123-how-to-bake-bread`, а `canonicalize()` будет ожидать `/article/123-how-to-bake-bre` (потому что где-то ещё кто-то применил другую длину `truncate`), и приложение зациклилось бы на перенаправлениях. - - -Бонус: короткие URL по-прежнему работают -======================================== - -Поскольку слаг необязателен, адреса без него по-прежнему работают: - -``` -/article/123 -``` - -Это полезно для: -- **QR-кодов** - более короткий URL означает менее плотный и лучше считываемый код -- **SMS и чатов** - помещается в твит, выглядит опрятно -- **печатных материалов** - короткий URL быстрее набрать - -Когда пользователь открывает такой URL, `canonicalize()` перенаправляет его с кодом 301 на полную версию со слагом, так что поисковые системы всё равно видят только каноническую форму. Краткость и SEO можно получить одновременно. - - -Итоги -===== - -- Маска `<id>[-<slug>]` делает слаг необязательным. Значение по умолчанию `<slug>` не совпадает с `/`; используйте `<slug .+>`, только если вам действительно нужны слеши в слаге. -- Общий `FilterOut` под ключом `''` находит заголовок по ID - **никаких изменений в шаблонах во всём приложении**. -- Оберните поиск в маленький кеш в пределах запроса; одного запроса к базе на уникальный ID вполне достаточно. -- При желании `FilterOut` отдельного параметра позволяет шаблонам передавать заголовок напрямую и пропускать поиск. -- `$this->canonicalize()` в действии перенаправляет неканонические URL на правильный с кодом HTTP 301. -- Формула слага (`webalize` + `truncate`) живёт в одном месте: измените её однажды, и это подействует везде. -- Короткие URL только с ID продолжают работать, что удобно для QR-кодов и SMS. - -Подробнее о фильтрах и канонизации вы найдёте в документации по [маршрутизации |application:routing#Общие фильтры] и [презентерам |application:presenters#Канонизация]. diff --git a/best-practices/ru/restore-request.texy b/best-practices/ru/restore-request.texy deleted file mode 100644 index 39d3b0a835..0000000000 --- a/best-practices/ru/restore-request.texy +++ /dev/null @@ -1,62 +0,0 @@ -Как вернуться на предыдущую страницу? -************************************* - -.[perex] -Что произойдёт, если пользователь заполняет форму, а его сессия входа истечёт? Чтобы не потерять данные, мы можем сохранить текущий запрос (включая данные формы) в сессию перед перенаправлением на страницу входа. В Nette это на удивление просто. - -Текущий запрос можно сохранить в сессию методом `storeRequest()`. Этот метод возвращает уникальный идентификатор (короткую строку) сохранённого запроса. Метод сохраняет имя текущего презентера, его представление и параметры. Если в составе запроса была отправлена форма, сохраняются и значения, введённые в поля (кроме загруженных файлов). - -Запрос восстанавливается методом `restoreRequest($key)`, которому вы передаёте полученный ранее идентификатор. Этот метод перенаправляет пользователя обратно в исходные презентер и представление. Однако если сохранённый запрос содержал отправку формы, `restoreRequest()` вместо перенаправления использует метод `forward()`. Он передаёт в форму ранее заполненные значения и позволяет отрисовать её снова. Благодаря этому пользователь может отправить форму заново, не потеряв введённые данные. - -Принципиально важно, что `restoreRequest()` проверяет, является ли только что вошедший пользователь тем же самым, который изначально отправил форму. Если пользователь другой, сохранённый запрос не восстанавливается, и метод ничего не делает, что повышает безопасность. - -Проиллюстрируем это примером. Представьте `AdminPresenter`, где редактируются данные. Его метод `startup()` проверяет, вошёл ли пользователь. Если нет, пользователь перенаправляется в `SignPresenter`. Одновременно мы сохраняем текущий запрос через `storeRequest()` и передаём его ключ (`$backlink`) в `SignPresenter`. - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - protected function startup() - { - parent::startup(); - - if (!$this->user->isLoggedIn()) { - $this->redirect('Sign:in', ['backlink' => $this->storeRequest()]); - } - } -} -``` - -`SignPresenter` будет содержать, помимо формы входа, постоянный параметр `$backlink`, в котором хранится ключ. Поскольку параметр постоянный, его значение сохраняется и после отправки формы входа. - - -```php -use Nette\Application\Attributes\Persistent; - -class SignPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $backlink = ''; - - protected function createComponentSignInForm() - { - $form = new Nette\Application\UI\Form; - // ... добавляем поля формы ... - $form->onSuccess[] = $this->signInFormSuceeded(...); - return $form; - } - - private function signInFormSuceeded($form) - { - // ... здесь выполняем вход пользователя ... - - $this->restoreRequest($this->backlink); - $this->redirect('Admin:'); - } -} -``` - -Мы передаём ключ (`$this->backlink`) сохранённого запроса в метод `restoreRequest()`. Он затем перенаправляет (или перебрасывает) пользователя обратно в исходные презентер и представление. - -Однако если ключ недействителен (например, истёк в сессии), метод ничего не делает. Поэтому последующий вызов `$this->redirect('Admin:')` служит запасным вариантом и перенаправляет на страницу по умолчанию, например в `AdminPresenter`. - -{{priority: -1}} diff --git a/best-practices/tr/@home.texy b/best-practices/tr/@home.texy deleted file mode 100644 index 8c1c1f139d..0000000000 --- a/best-practices/tr/@home.texy +++ /dev/null @@ -1,69 +0,0 @@ -Kılavuzlar ve En İyi Uygulamalar -******************************** - -.[perex] -Nette için kılavuzlar, sık karşılaşılan görevlerin çözümleri ve en iyi uygulamalar. - - -<div class=documentation> -<div> - - -Nette Application ------------------ -- [Inject metotları ve attribute'ları |inject-method-attribute] -- [Presenter'ları trait'lerden kurma |presenter-traits] -- [Presenter'lara ayar aktarma |passing-settings-to-presenters] -- [İstek nasıl geri yüklenir |restore-request] -- [Veritabanı sonuçlarını sayfalama |pagination] -- [Dinamik snippet'ler |dynamic-snippets] -- [#Requires attribute'u nasıl kullanılır |attribute-requires] -- [POST bağlantıları doğru şekilde nasıl kullanılır |post-links] -- [Slug'larla güzel URL'ler |pretty-urls] - -</div> -<div> - - -Formlar -------- -- [Formları yeniden kullanma |form-reuse] -- [Kayıt oluşturma ve düzenleme formu |creating-editing-form] -- [Bir iletişim formu oluşturalım |lets-create-contact-form] -- [Birbirine bağlı seçim kutuları |https://blog.nette.org/tr/dependent-selectboxes-elegantly-in-nette-and-pure-js] - -</div> -<div> - - -Genel ------ -- [Yapılandırma dosyası nasıl yüklenir |bootstrap:] -- [Mikro siteler nasıl yazılır |microsites] -- [Nette sabitlerde neden PascalCase yazımını kullanıyor? |https://blog.nette.org/tr/for-less-screaming-in-the-code] -- [Nette neden Interface son ekini kullanmıyor? |https://blog.nette.org/tr/prefixes-and-suffixes-do-not-belong-in-interface-names] -- [Composer: kullanım ipuçları |composer] -- [Nesne yönelimli programlamaya giriş |nette:introduction-to-object-oriented-programming] - -</div> -<div> - - -Örnek Çözümler --------------- -- [Nette örnekleri |https://github.com/nette-examples] -- [Doctrine & Nette |https://contributte.org/nettrine/] -- [Contributte örnekleri |https://contributte.org/examples.html] -- [Doctrine ORM web sitesi |https://github.com/MinecordNetwork/Website] -- [Hızlı başlangıç |quickstart:] - -</div> -<div> - - -Videolar --------- -Last Saturday buluşmalarından yüzlerce kayıt ve Nette hakkında videolar, hepsi tek bir yerde: "Nette Framework YouTube kanalı":https://www.youtube.com/user/NetteFramework. - -</div> -</div> diff --git a/best-practices/tr/@left-menu.texy b/best-practices/tr/@left-menu.texy deleted file mode 100644 index 2cbf528cde..0000000000 --- a/best-practices/tr/@left-menu.texy +++ /dev/null @@ -1,34 +0,0 @@ -Kılavuzlar ve En İyi Uygulamalar -******************************** -- [Genel bakış |@home] - -Nette Application -***************** -- [Inject metotları ve attribute'ları |inject-method-attribute] -- [Presenter'ları trait'lerden kurma |presenter-traits] -- [Presenter'lara ayar aktarma |passing-settings-to-presenters] -- [İstek nasıl geri yüklenir |restore-request] -- [Veritabanı sonuçlarını sayfalama |pagination] -- [Dinamik snippet'ler |dynamic-snippets] -- [#Requires attribute'u nasıl kullanılır |attribute-requires] -- [POST bağlantıları doğru şekilde nasıl kullanılır |post-links] -- [Slug'larla güzel URL'ler |pretty-urls] - -Formlar -******* -- [Formları yeniden kullanma |form-reuse] -- [Kayıt oluşturma ve düzenleme formu |creating-editing-form] -- [Bir iletişim formu oluşturalım |lets-create-contact-form] - -Genel -***** -- [Mikro siteler nasıl yazılır |microsites] -- [Composer: kullanım ipuçları |composer] - - -Daha Fazla Okuma -**************** -- [Nette dokümantasyonu |nette:] -- [Nette Application |application:how-it-works] -- [Yardımcı araçlar |utils:] -- [Sorun giderme |nette:troubleshooting] diff --git a/best-practices/tr/@meta.texy b/best-practices/tr/@meta.texy deleted file mode 100644 index e2ab9e0d40..0000000000 --- a/best-practices/tr/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Kılavuzlar ve En İyi Uygulamalar}} diff --git a/best-practices/tr/attribute-requires.texy b/best-practices/tr/attribute-requires.texy deleted file mode 100644 index 26f6caed3e..0000000000 --- a/best-practices/tr/attribute-requires.texy +++ /dev/null @@ -1,177 +0,0 @@ -`#[Requires]` Attribute'u Nasıl Kullanılır -****************************************** - -.[perex] -Bir web uygulaması yazarken, uygulamanızın belirli bölümlerine erişimi kısıtlama ihtiyacıyla sık karşılaşırsınız. Belki bazı isteklerin yalnızca bir form üzerinden veri gönderebilmesini (yani POST metodunu kullanmasını) ya da yalnızca AJAX çağrılarına açık olmasını istersiniz. Nette Framework 3.2'de, bu tür kısıtlamaları şık ve anlaşılır biçimde koymanızı sağlayan yeni bir araç geldi: `#[Requires]` attribute'u. - -Attribute, PHP'de bir sınıf ya da metot tanımından önce eklediğiniz özel bir işarettir. Özünde bir sınıf olduğundan, aşağıdaki örneklerin çalışması için `use` deyimini eklemeniz gerekir: - -```php -use Nette\Application\Attributes\Requires; -``` - -`#[Requires]` attribute'unu presenter sınıfının kendisinde ve şu metotlarda kullanabilirsiniz: - -- `action<Action>()` -- `render<View>()` -- `handle<Signal>()` -- `createComponent<Name>()` - -Son iki metot bileşenler için de geçerlidir, dolayısıyla attribute'u onlarla da kullanabilirsiniz. - -Attribute'un belirttiği koşullar sağlanmazsa bir HTTP 4xx hatası tetiklenir. - - -HTTP Metotları --------------- - -Erişim için hangi HTTP metotlarına (GET, POST vb.) izin verildiğini belirtebilirsiniz. Örneğin erişime yalnızca bir form gönderilerek 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ştiren eylemlerde neden GET yerine POST kullanmalısınız ve bunu nasıl yaparsınız? [Kılavuzu okuyun |post-links]. - -Bir metot ya da metot dizisi belirtebilirsiniz. Özel bir durum, tüm metotlara izin veren `'*'` değeridir; presenter'lar bunu [güvenlik nedeniyle varsayılan olarak yapmaz |application:presenters#HTTP metodunun denetimi]. - - -AJAX Çağrıları --------------- - -Bir presenter'ın ya da metodun yalnızca AJAX istekleriyle erişilebilir olmasını istiyorsanız şunu kullanın: - -```php -#[Requires(ajax: true)] -class AjaxPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Aynı Kaynak ------------ - -Güvenliği artırmak için isteğin aynı alan adından yapılmasını zorunlu kılabilirsiniz. Bu, [CSRF açığını |nette:vulnerability-protection#Cross-Site Request Forgery (CSRF)] önler: - -```php -#[Requires(sameOrigin: true)] -class SecurePresenter extends Nette\Application\UI\Presenter -{ -} -``` - -`handle<Signal>()` metotlarında aynı alan adından erişim otomatik olarak zorunludur. Bu yüzden herhangi bir alan adından erişime izin vermek istiyorsanız şunu belirtin: - -```php -#[Requires(sameOrigin: false)] -public function handleList(): void -{ -} -``` - - -Forward ile Erişim ------------------- - -Bazen bir presenter'a erişimi, yalnızca dolaylı olarak, örneğin başka bir presenter'dan `forward()` ya da `switch()` metotlarıyla ulaşılabilecek şekilde kısıtlamak yararlıdır. Örneğin hata presenter'ları, URL'den tetiklenmelerini önlemek için böyle korunur: - -```php -#[Requires(forward: true)] -class ForwardedPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Pratikte, yalnızca presenter'daki mantığa göre erişilebilecek belirli view'ları işaretlemek çoğu zaman gerekir. Yine, doğrudan açılamamaları için: - -```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 - { - } -} -``` - - -Belirli Eylemler ----------------- - -Bileşen oluşturmak gibi belirli bir kodun yalnızca presenter'daki belirli eylemlerde 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 söz konusuysa dizi yazmaya gerek yoktur: `#[Requires(actions: 'default')]` - - -Özel Attribute'lar ------------------- - -`#[Requires]` attribute'unu aynı ayarlarla defalarca kullanmak istiyorsanız, `#[Requires]` attribute'undan türeyen ve onu ihtiyacınıza göre yapılandıran kendi attribute'unuzu oluşturabilirsiniz. - -Örneğin `#[SingleAction]`, erişime yalnızca `default` eylemi üzerinden 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'de kullanılan tüm HTTP metotlarıyla erişime izin verir: - -```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ç ------ - -`#[Requires]` attribute'u, web sayfalarınıza nasıl erişildiği konusunda size büyük esneklik ve denetim verir. Basit ama güçlü kurallarla uygulamanızın güvenliğini ve düzgün çalışmasını artırabilirsiniz. Gördüğünüz gibi, Nette'de attribute kullanmak işinizi yalnızca kolaylaştırmakla kalmaz, aynı zamanda güvenli hâle getirir. diff --git a/best-practices/tr/composer.texy b/best-practices/tr/composer.texy deleted file mode 100644 index 798a79ef77..0000000000 --- a/best-practices/tr/composer.texy +++ /dev/null @@ -1,282 +0,0 @@ -Composer Kullanım İpuçları -************************** - -<div class=perex> - -Composer, PHP'de bağımlılık yönetimi için bir araçtır. Projenizin bağımlı olduğu kütüphaneleri bildirmenizi sağlar, onları sizin için kurar ve günceller. Şunları öğreneceğiz: - -- Composer nasıl kurulur -- yeni ya da var olan bir projede nasıl kullanılır - -</div> - - -Kurulum -======= - -Composer, indirip aşağıdaki gibi kuracağınız çalıştırılabilir bir `.phar` dosyasıdır. - - -Windows -------- - -Resmi kurulum programını kullanın: [Composer-Setup.exe|https://getcomposer.org/Composer-Setup.exe]. - - -Linux, macOS ------------- - -Tek gereken, [bu sayfadan |https://getcomposer.org/download/] kopyalayabileceğiniz 4 komut. - -Ayrıca, sistemin `PATH` değişkeninde bulunan bir klasöre kopyalayarak Composer'ı her yerden erişilebilir kılabilirsiniz: - -```shell -$ mv ./composer.phar ~/bin/composer # ya da /usr/local/bin/composer -``` - - -Projede Kullanım -================ - -Projenizde Composer kullanmaya başlamak için tek gereken bir `composer.json` dosyasıdır. Bu dosya projenizin bağımlılıklarını anlatır ve başka meta veriler de içerebilir. En basit `composer.json` şöyle görünebilir: - -```js -{ - "require": { - "nette/database": "^3.0" - } -} -``` - -Burada, uygulamamızın (ya da kütüphanemizin) `nette/database` paketini gerektirdiğini (paket adı bir üretici adı ile projenin adından oluşur) ve `^3.0` sürüm kısıtına uyan bir sürüm istediğini (yani en son 3 sürümünü) söylüyoruz. - -`composer.json` dosyası proje kökündeyken şunu çalıştırın: - -```shell -composer update -``` - -Composer, Nette Database paketini `vendor/` dizinine indirir. Ayrıca, tam olarak hangi kütüphane sürümlerini kurduğuna dair bilgi içeren bir `composer.lock` dosyası oluşturur. - -Composer bir `vendor/autoload.php` dosyası üretir. Bu dosyayı basitçe dahil edip kütüphanelerin sınıflarını fazladan hiçbir iş yapmadan kullanmaya başlayabilirsiniz: - -```php -require __DIR__ . '/vendor/autoload.php'; - -$db = new Nette\Database\Connection('sqlite::memory:'); -``` - - -Paketleri En Son Sürümlere Güncelleme -===================================== - -Kullanılan kütüphaneleri, `composer.json` içinde tanımlı kısıtlara göre en son sürümlere güncellemek için `composer update` komutunu kullanın. Örneğin `"nette/database": "^3.0"` bağımlılığıyla en son 3.x.x sürümünü kurar, ama 4 sürümünü kurmaz. - -`composer.json` dosyasındaki kısıtları, en son sürümün kurulmasına izin verecek şekilde örneğin `"nette/database": "^4.1"` olarak güncellemek için `composer require nette/database` komutunu kullanın. - -Kullanılan tüm Nette paketlerini güncellemek için hepsini komut satırında saymanız gerekirdi, örneğin: - -```shell -composer require nette/application nette/forms latte/latte tracy/tracy ... -``` - -Bu pratik değil. Bu yüzden bunu sizin için yapan basit "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff betiğini kullanın: - -```shell -php composer-frontline.php -``` - - -Yeni Proje Oluşturma -==================== - -Tek bir komutla yeni bir Nette projesi oluşturabilirsiniz: - -```shell -composer create-project nette/web-project projenin-adi -``` - -`projenin-adi` yerine projeniz için kullanacağınız dizin adını yazın ve komutu çalıştırın. Composer, GitHub'dan `nette/web-project` deposunu indirir; bu depo zaten bir `composer.json` dosyası içerir. Ardından Nette Framework'ün kendisini kurar. Geriye yalnızca `temp/` ve `log/` dizinleri için [dizin izinlerini ayarlamak |nette:troubleshooting#Dizin İzinlerini Ayarlama] kalır ve proje çalışır durumda olur. - -Projenizin hangi PHP sürümünde barındırılacağını biliyorsanız, bunu mutlaka [ayarlayın |#PHP Sürümü]. - - -PHP Sürümü -========== - -Composer her zaman, o an kullandığınız PHP sürümüyle (daha kesin olarak, Composer çalıştırılırken komut satırında kullanılan PHP sürümüyle) uyumlu paket sürümlerini kurar. Bu, web barındırıcınızın kullandığı sürümle aynı olmayabilir. Bu yüzden barındırmanızdaki PHP sürümü bilgisini `composer.json` dosyasına eklemek çok önemlidir. Böylece yalnızca barındırmayla uyumlu paket sürümleri kurulur. - -Örneğin projenin PHP 8.2.3 üzerinde çalışacağını belirtmek için şu komutu kullanın: - -```shell -composer config platform.php 8.2.3 -``` - -Sürüm `composer.json` dosyasına şöyle yazılır: - -```js -{ - "config": { - "platform": { - "php": "8.2.3" - } - } -} -``` - -Ancak PHP sürüm numarası dosyanın başka bir yerinde, `require` bölümünde de belirtilir. İlk numara paketlerin hangi sürüme göre kurulacağını belirlerken, ikinci numara uygulamanın kendisinin hangi sürüm için yazıldığını gösterir. Örneğin PhpStorm bunu *PHP language level* ayarını belirlemek için kullanır. (Elbette bu sürümlerin farklı olması anlamlı değildir, dolayısıyla çift kayıt bir gözden kaçmadır.) Bu sürümü şu komutla ayarlayın: - -```shell -composer require php 8.2.3 --no-update -``` - -Ya da doğrudan `composer.json` dosyasında: - -```js -{ - "require": { - "php": "8.2.3" - } -} -``` - - -PHP Sürümünü Yok Sayma -====================== - -Paketler genellikle hem uyumlu oldukları en düşük PHP sürümünü hem de karşı test edildikleri en yüksek sürümü belirtir. Belki test amacıyla daha da yeni bir PHP sürümü kullanmak isterseniz, Composer böyle bir paketi kurmayı reddeder. Çözüm, Composer'ın gereken PHP sürümünün üst sınırlarını yok saymasını sağlayan `--ignore-platform-req=php+` seçeneğidir. - - -Yanlış Bildirimler -================== - -Paketleri yükseltirken ya da sürüm numaralarını değiştirirken bazen çakışmalar olur. Bir paketin gereksinimleri bir başkasıyla çakışır vb. Ancak Composer bazen yanlış bildirimler verir. Aslında var olmayan bir çakışmayı bildirir. Böyle durumlarda `composer.lock` dosyasını silip yeniden denemek işe yarayabilir. - -Hata mesajı yine de sürerse, gerçektir; ne değiştirmeniz gerektiğini ve nasıl yapacağınızı anlamak için okumalısınız. - - -Packagist.org - Genel Depo -========================== - -[Packagist |https://packagist.org], Composer'ın paketleri varsayılan olarak aradığı ana depodur. Kendi paketlerinizi de burada yayımlayabilirsiniz. - - -Ya Merkezi Depoyu İstemiyorsak ------------------------------- - -Şirketimiz içinde herkese açık barındırılamayacak iç uygulamalarımız ya da kütüphanelerimiz varsa, onlar için kendi depolarımızı oluşturabiliriz. - -Depolar hakkında daha fazlasını [resmi belgelerde |https://getcomposer.org/doc/05-repositories.md#repositories] okuyun. - - -Autoloading -=========== - -Composer'ın temel özelliklerinden biri, kurduğu tüm sınıflar için autoloading sağlamasıdır. Bunu `vendor/autoload.php` dosyasını dahil ederek etkinleştirirsiniz. - -Ancak Composer'ı, `vendor/` dizininin dışındaki başka sınıfları yüklemek için de kullanabilirsiniz. İlk seçenek, Composer'ın tanımlanan dizinleri ve alt dizinlerini taramasını, tüm sınıfları bulup autoloader'a katmasını sağlamaktır. Bunun için `composer.json` içinde `autoload > classmap` ayarını yapın: - -```js -{ - "autoload": { - "classmap": [ - "src/", # src/ dizinini ve alt dizinlerini kapsar - ] - } -} -``` - -Sonrasında, autoloading tablolarını yeniden üretmek için her değişiklikten sonra `composer dumpautoload` komutunu çalıştırmanız gerekir. Bu son derece elverişsizdir. Bu işi, aynı işi arka planda otomatik olarak ve çok daha hızlı yapan [RobotLoader|robot-loader:] aracına bırakmak çok daha iyidir. - -İkinci seçenek, [PSR-4 |https://www.php-fig.org/psr/psr-4/] standardına uymaktır. Basitçe söylemek gerekirse, isim 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; örneğin `App\Core\RouterFactory` sınıfı `/path/to/App/Core/RouterFactory.php` dosyasında bulunur. Yapılandırma örneği: - -```js -{ - "autoload": { - "psr-4": { - "App\\": "app/" # App\ isim alanı app/ dizinindedir - } - } -} -``` - -Bu davranışı nasıl yapılandıracağınızın ayrıntıları için [Composer belgelerine |https://getcomposer.org/doc/04-schema.md#psr-4] bakın. - - -Yeni Sürümleri Deneme -===================== - -Bir paketin yeni geliştirme sürümünü denemek mi istiyorsunuz? İşte nasıl yapılacağı. Önce `composer.json` dosyanıza şu iki seçeneği ekleyin. Bu, geliştirme sürümlerinin kurulmasına izin verir; ancak Composer bunlara yalnızca kararlı sürümlerden hiçbir bileşim gereksinimleri karşılamıyorsa başvurur: - -```js -{ - "minimum-stability": "dev", - "prefer-stable": true, -} -``` - -Ayrıca `composer.lock` dosyasını silmenizi öneririz; çünkü Composer bazen anlaşılmaz biçimde kurulumu reddeder ve bu, sorunu çözebilir. - -Diyelim ki paket `nette/utils` ve yeni sürüm 4.0. Şu komutla kurun: - -```shell -composer require nette/utils:4.0.x-dev -``` - -Ya da belirli bir sürümü, örneğin 4.0.0-RC2 sürümünü kurabilirsiniz: - -```shell -composer require nette/utils:4.0.0-RC2 -``` - -Ancak başka bir paket bu kütüphaneye bağımlıysa ve daha eski bir sürüme kilitliyse (örneğin `^3.1`), ideal çözüm o bağımlı paketi yeni sürümle çalışacak şekilde güncellemektir. Yalnızca kısıtı aşmak ve Composer'ı, geliştirme sürümünü daha eski bir sürümmüş gibi (örneğin 3.1.6) göstererek kurmaya zorlamak istiyorsanız `as` anahtar sözcüğünü kullanabilirsiniz: - -```shell -composer require nette/utils "4.0.x-dev as 3.1.6" -``` - - -Komut Çağırma -============= - -Kendi önceden tanımlanmış komutlarınızı ve betiklerinizi, Composer'ın yerleşik komutlarıymış gibi Composer üzerinden çağırabilirsiniz. `vendor/bin` dizinindeki betikler için bu yolu belirtmeniz gerekmez. - -Örnek olarak, `composer.json` içinde testleri çalıştırmak üzere [Nette Tester |tester:] kullanan bir betik tanımlayalım: - -```js -{ - "scripts": { - "tester": "tester tests -s" - } -} -``` - -Testleri sonra `composer tester` ile çalıştırırız. Komutu, projenin kök dizininde değil de alt dizinlerinden birindeyken bile çağırabilirsiniz. - - -Teşekkür Gönderin -================= - -Açık kaynak yazarlarını sevindirecek bir numara gösterelim. Projenizin kullandığı kütüphanelere GitHub'da kolayca yıldız verebilirsiniz. Yalnızca `symfony/thanks` kütüphanesini kurun: - -```shell -composer global require symfony/thanks -``` - -Ve sonra çalıştırın: - -```shell -composer thanks -``` - -Deneyin! - - -Yapılandırma -============ - -Composer, sürüm denetimi aracı [Git |https://git-scm.com] ile sıkı sıkıya bütünleşiktir. Git kurulu değilse, Composer'a onu kullanmamasını söylemeniz gerekir: - -```shell -composer -g config preferred-install dist -``` diff --git a/best-practices/tr/creating-editing-form.texy b/best-practices/tr/creating-editing-form.texy deleted file mode 100644 index 6c96ad2d41..0000000000 --- a/best-practices/tr/creating-editing-form.texy +++ /dev/null @@ -1,204 +0,0 @@ -Kayıt Oluşturma ve Düzenleme Formu -********************************** - -.[perex] -Nette'de bir kaydın eklenmesi ve düzenlenmesi, ikisi için de aynı form kullanılarak nasıl doğru şekilde gerçekleştirilir? - -Çoğu durumda kayıt ekleme ve düzenleme formları birbirinin aynısıdır, belki yalnızca düğme metninde ayrılırlar. Formu önce kayıt eklemek, sonra düzenlemek için kullandığımız basit presenter örneklerini göstereceğiz ve en sonunda iki çözümü birleştireceğiz. - - -Kayıt Ekleme ------------- - -Kayıt eklemeye yarayan bir presenter örneği. Asıl veritabanı işini, kodu bu örnek için önemli olmayan bir `Facade` sınıfına bırakacağız. - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentRecordForm(): Form - { - $form = new Form; - - // ... form alanlarını ekle ... - - $form->onSuccess[] = $this->recordFormSucceeded(...); - return $form; - } - - private function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // kaydı veritabanına ekle - $this->flashMessage('Başarıyla eklendi'); - $this->redirect('...'); - } - - public function renderAdd(): void - { - // ... - } -} -``` - - -Kayıt Düzenleme ---------------- - -Şimdi kayıt düzenlemeye yarayan bir presenter'ın nasıl görüneceğine bakalım: - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - private $record; - - public function __construct( - private Facade $facade, - ) { - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // kaydın varlığını doğrula - || !$this->facade->isEditAllowed(/*...*/) // izinleri denetle - ) { - $this->error(); // 404 hatası - } - - $this->record = $record; - } - - protected function createComponentRecordForm(): Form - { - // eylemin 'edit' olduğunu doğrula - if ($this->getAction() !== 'edit') { - $this->error(); - } - - $form = new Form; - - // ... form alanlarını ekle ... - - $form->setDefaults($this->record); // varsayılan değerleri ayarla - $form->onSuccess[] = $this->recordFormSucceeded(...); - return $form; - } - - private function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->update($this->record->id, $data); // kaydı güncelle - $this->flashMessage('Başarıyla güncellendi'); - $this->redirect('...'); - } -} -``` - -[Presenter yaşam döngüsünün |application:presenters#Presenter'ın yaşam döngüsü] başında çağrılan *action* metodunda, kaydın varlığını ve kullanıcının onu düzenleme iznini doğrularız. - -Kaydı `$record` özelliğinde saklarız; böylece varsayılan değerleri ayarlamak için `createComponentRecordForm()` metodunda ve ID'ye erişmek için `recordFormSucceeded()` metodunda kullanılabilir olur. Alternatif bir çözüm, varsayılan değerleri doğrudan `actionEdit()` içinde ayarlamak ve ID değerini (URL'nin bir parçasıdır) `getParameter('id')` ile almaktır: - - -```php - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - // varlığı doğrula ve izinleri denetle - ) { - $this->error(); - } - - // formun varsayılan değerlerini ayarla - $this->getComponent('recordForm') - ->setDefaults($record); - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); - // ... - } -``` - -Ancak, ki bu **kodun tamamından çıkarılacak en önemli ders olmalı**, formu oluştururken eylemin gerçekten `edit` olduğundan emin olmalıyız. Aksi hâlde `actionEdit()` metodundaki doğrulama hiç yapılmazdı! - - -Ekleme ve Düzenleme İçin Aynı Form ----------------------------------- - -Şimdi iki presenter'ı tek bir presenter'da birleştirelim. Ya `createComponentRecordForm()` metodunda eylemi ayırt edip formu buna göre yapılandırabiliriz, ya da bunu doğrudan action metotlarına devredip koşullu denetimi ortadan kaldırabiliriz: - - -```php -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - public function actionAdd(): void - { - $form = $this->getComponent('recordForm'); - $form->onSuccess[] = $this->addingFormSucceeded(...); - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // kaydın varlığını doğrula - || !$this->facade->isEditAllowed(/*...*/) // izinleri denetle - ) { - $this->error(); // 404 hatası - } - - $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // varsayılan değerleri ayarla - $form->onSuccess[] = $this->editingFormSucceeded(...); - } - - protected function createComponentRecordForm(): Form - { - // eylemin 'add' ya da 'edit' olduğunu doğrula - if (!in_array($this->getAction(), ['add', 'edit'])) { - $this->error(); - } - - $form = new Form; - - // ... form alanlarını ekle ... - - return $form; - } - - private function addingFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // kaydı veritabanına ekle - $this->flashMessage('Başarıyla eklendi'); - $this->redirect('...'); - } - - private function editingFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // kaydı güncelle - $this->flashMessage('Başarıyla güncellendi'); - $this->redirect('...'); - } -} -``` - -{{priority: -1}} diff --git a/best-practices/tr/dynamic-snippets.texy b/best-practices/tr/dynamic-snippets.texy deleted file mode 100644 index 1cb0d6d957..0000000000 --- a/best-practices/tr/dynamic-snippets.texy +++ /dev/null @@ -1,176 +0,0 @@ -Dinamik Snippet'ler -******************* - -.[perex] -Latte'nin dinamik snippet'lerini kullanarak, bir sayfanın yalnızca gerçekten değişen bölümlerini (örneğin bir listedeki tek tek öğeleri) AJAX ile nasıl yenilersiniz. - -Uygulama geliştirirken, örneğin bir tablonun satırlarında ya da liste öğelerinde AJAX işlemleri yapma ihtiyacı sıkça doğar. Örnek olarak, oturum açmış kullanıcıların her makaleyi "beğen" ya da "beğenme" ile puanlayabildiği bir makale listesini düşünelim. AJAX'sız presenter kodu ve ilgili şablon aşağı yukarı şöyle görünürdü (yalnızca en can alıcı bölümler gösteriliyor; kod, puanlamayı yöneten ve makaleleri getiren bir servisin var olduğunu varsayar; somut gerçekleştirim bu kılavuz için önemli değil): - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - $this->redirect('this'); -} - -public function handleUnlike(int $articleId): void -{ - $this->ratingService->removeLike($articleId, $this->user->id); - $this->redirect('this'); -} -``` - -Şablon: - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {if !$article->liked} - <a n:href="like!, $article->id" class=ajax>Beğendim</a> - {else} - <a n:href="unlike!, $article->id" class=ajax>Artık beğenmiyorum</a> - {/if} -</article> -``` - - -AJAX'laştırma -============= - -Şimdi bu basit uygulamaya AJAX işlevi ekleyelim. Bir makalenin puanını değiştirmek, tüm sayfayı yeniden yüklemeyi gerektirecek kadar kritik değil; bu yüzden ideal olarak arka planda AJAX ile gerçekleşmeli. AJAX bağlantılarının `ajax` CSS sınıfına sahip olduğu yaygın uzlaşımıyla birlikte [eklentilerdeki işleyici betiğini |application:ajax#Naja] kullanacağız. - -Peki bunu tam olarak nasıl gerçekleştiririz? Nette iki yaklaşım sunar: dinamik snippet'ler ve bileşenler. Her ikisinin de artıları ve eksileri var, bu yüzden ikisini de göstereceğiz. - - -Dinamik Snippet Yolu -==================== - -Latte terminolojisinde dinamik snippet, `{snippet}` etiketinin snippet adında bir değişken kullanılan özel bir biçimidir. Böyle bir snippet şablonda herhangi bir yere konamaz; statik (sıradan) bir snippet'in içine sarılmalı ya da bir `{snippetArea}` içinde olmalıdır. Şablonumuzu şöyle değiştirebiliriz: - - -```latte -{snippet articlesContainer} - <article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {snippet article-{$article->id}} - {if !$article->liked} - <a n:href="like!, $article->id" class=ajax>Beğendim</a> - {else} - <a n:href="unlike!, $article->id" class=ajax>Artık beğenmiyorum</a> - {/if} - {/snippet} - </article> -{/snippet} -``` - -Artık her makale, adında makalenin ID'si geçen bir snippet tanımlıyor. Tüm bu dinamik snippet'ler de `articlesContainer` adlı statik bir snippet'in içine sarılıyor. Bu dış snippet'i atlasaydık Latte bir istisna fırlatırdı. - -Geriye yalnızca presenter'a yeniden çizme mantığını eklemek kalıyor: yalnızca statik sarmalayıcıyı yeniden çizin. - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - if ($this->isAjax()) { - $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- gerekmiyor - } else { - $this->redirect('this'); - } -} -``` - -İlgili `handleUnlike()` metodunu da benzer şekilde değiştirin; AJAX çalışıyor! - -Ancak bu çözümün bir sakıncası var. AJAX isteğini daha yakından incelersek, uygulama dışarıdan verimli görünse de (yalnızca ilgili makaleye ait tek bir snippet döndürse de), aslında sunucu tarafında *tüm* snippet'leri render ettiğini görürüz. Gereken snippet'i payload'a koyar, diğerlerini atar (yani onları da gereksiz yere getirip render etmiştir). - -Bunu iyileştirmek için `$articles` koleksiyonunun şablona aktarıldığı yere (diyelim ki `renderDefault()` metoduna) müdahale etmemiz gerekir. Sinyal işlemenin `render<Something>` metotlarından önce gerçekleştiğinden yararlanacağız: - -```php -public function handleLike(int $articleId): void -{ - // ... - if ($this->isAjax()) { - // ... - $this->template->articles = [ - $this->db->table('articles')->get($articleId), - ]; - } else { - // ... -} - -public function renderDefault(): void -{ - if (!isset($this->template->articles)) { - $this->template->articles = $this->db->table('articles'); - } -} -``` - -Artık sinyal işlenirken şablona makalelerin tümü yerine yalnızca ilgili tek makaleyi içeren bir dizi aktarılıyor; yani render edip payload'da tarayıcıya göndermeyi düşündüğümüz makale. Sonuç olarak `{foreach}` döngüsü yalnızca bir kez çalışıyor ve gereksiz snippet render edilmiyor. - - -Bileşen Yolu -============ - -Tümüyle farklı bir yaklaşım, dinamik snippet'lerden büsbütün kaçınır. Numara, tüm mantığı ayrı bir bileşenin içine almaktan ibarettir. Puanlamayı presenter yerine, bu iş için ayrılmış bir `LikeControl` yönetir. Sınıf şöyle görünür (ayrıca `render`, `handleUnlike` gibi metotları da içerir): - -```php -class LikeControl extends Nette\Application\UI\Control -{ - public function __construct( - private Article $article, - ) { - } - - public function handleLike(): void - { - $this->ratingService->saveLike($this->article->id, $this->presenter->user->id); - if ($this->presenter->isAjax()) { - $this->redrawControl(); - } else { - $this->presenter->redirect('this'); - } - } -} -``` - -Bileşenin şablonu: - -```latte -{snippet} - {if !$article->liked} - <a n:href="like!" class=ajax>Beğendim</a> - {else} - <a n:href="unlike!" class=ajax>Artık beğenmiyorum</a> - {/if} -{/snippet} -``` - -Doğal olarak view'ın şablonu değişecek ve presenter'a bir factory eklememiz gerekecek. Veritabanından gelen her makale için bu bileşenden bir örnek oluşturacağımızdan, oluşturulmalarını yönetmek için [Multiplier |application:Multiplier] sınıfını kullanacağız. - -```php -protected function createComponentLikeControl() -{ - $articles = $this->db->table('articles'); - return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { - return new LikeControl($articles[$articleId]); - }); -} -``` - -View'ın şablonu en aza iner (ve snippet'lerden tümüyle arınır!): - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {control "likeControl-$article->id"} -</article> -``` - -Neredeyse bitirdik: uygulama artık AJAX ile çalışacak. Burada da iyileştirme gerekiyor; çünkü Nette Database kullanımı nedeniyle sinyal işlenirken veritabanından yalnızca ilgili makale yerine tüm makaleler gereksiz yere yükleniyor. Buna karşılık üstünlük şu: yalnızca ilgili bileşen örneği render edildiğinden gereksiz render yapılmıyor. - -{{priority: -1}} diff --git a/best-practices/tr/form-reuse.texy b/best-practices/tr/form-reuse.texy deleted file mode 100644 index ff42e35b06..0000000000 --- a/best-practices/tr/form-reuse.texy +++ /dev/null @@ -1,348 +0,0 @@ -Formları Birden Çok Yerde Yeniden Kullanma -****************************************** - -.[perex] -Nette, aynı formu kod yinelemeden birden çok yerde kullanmanın çeşitli yollarını sunar. Bu yazı, kaçınmanız gerekenler de dahil olmak üzere çeşitli çözümleri ele alıyor. - - -Form Factory'si -=============== - -Bir bileşeni birden çok yerde yeniden kullanmanın temel yaklaşımı, bu bileşeni üreten bir metot ya da sınıf oluşturmaktır. Bu metot daha sonra uygulamanın çeşitli yerlerinden çağrılır. Böyle bir metoda ya da sınıfa *factory* denir. Lütfen bunu, factory kullanımının belirli bir biçimini anlatan ve bu konuyla doğrudan ilgisi olmayan *factory method* tasarım deseniyle karıştırmayın. - -Örnek olarak, bir düzenleme formu kuran bir factory oluşturalım: - -```php -use Nette\Application\UI\Form; - -class FormFactory -{ - public function createEditForm(): Form - { - $form = new Form; - $form->addText('title', 'Başlık:'); - // diğer form alanları buraya eklenir - $form->addSubmit('send', 'Kaydet'); - return $form; - } -} -``` - -Artık bu factory'yi uygulamanızın çeşitli yerlerinde, örneğin presenter'larda ya da bileşenlerde kullanabilirsiniz. Bunu, onu [bağımlılık olarak isteyerek |dependency-injection:passing-dependencies] yaparsınız. Önce sınıfı yapılandırma dosyasında kaydedin: - -```neon -services: - - FormFactory -``` - -Sonra onu bir presenter'da kullanın: - - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->createEditForm(); - $form->onSuccess[] = function () { - // gönderilen verilerin işlenmesi - }; - return $form; - } -} -``` - -Form factory'sini, uygulamanızın ihtiyacına göre başka form türleri oluşturan metotlarla genişletebilirsiniz. Doğal olarak, öğesiz temel bir form oluşturan ve diğer metotların yararlanabileceği bir metot da ekleyebiliriz: - -```php -class FormFactory -{ - public function createForm(): Form - { - $form = new Form; - return $form; - } - - public function createEditForm(): Form - { - $form = $this->createForm(); - $form->addText('title', 'Başlık:'); - // diğer form alanları buraya eklenir - $form->addSubmit('send', 'Kaydet'); - return $form; - } -} -``` - -`createForm()` metodu henüz pek yararlı bir şey yapmıyor, ama bu yakında değişecek. - - -Factory Bağımlılıkları -====================== - -Zamanla formların çok dilli olması gerekebilir. Bu da tüm formlar için bir [çevirmen |forms:rendering#Çeviri] ayarlamak demektir. Bunu sağlamak için `FormFactory` sınıfını, `Translator` nesnesini yapıcısında bağımlılık olarak alacak ve oluşturulan forma aktaracak şekilde değiştirin: - -```php -use Nette\Localization\Translator; - -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function createForm(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } - - // ... -} -``` - -`createForm()` metodu, belirli formları oluşturan diğer metotlar tarafından da çağrıldığından, çevirmeni burada ayarlamak yeterlidir. Ve iş bitti. Hiçbir presenter ya da bileşen kodunu değiştirmeye gerek yok; bu harika. - - -Daha Fazla Factory Sınıfı -========================= - -Alternatif olarak, uygulamanızda kullanmayı düşündüğünüz her form için ayrı factory sınıfları oluşturabilirsiniz. Bu yaklaşım kodun okunurluğunu artırabilir ve form yönetimini kolaylaştırabilir. Özgün `FormFactory` yalnızca temel yapılandırmayla (çeviri desteği gibi) basit bir form oluştursun; düzenleme formu için de `EditFormFactory` adlı yeni bir factory oluşturun. - -```php -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function create(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } -} - - -// ✅ kompozisyon kullanımı -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - // diğer form alanları buraya eklenir - $form->addSubmit('send', 'Kaydet'); - return $form; - } -} -``` - -Can alıcı nokta şu: `FormFactory` ile `EditFormFactory` sınıfları arasındaki ilişki [nesne kalıtımıyla |nette:introduction-to-object-oriented-programming#Kompozisyon] değil, [kompozisyonla |nette:introduction-to-object-oriented-programming#Kalıtım] kurulmalıdır: - -```php -// ⛔ HAYIR! KALITIMIN BURADA YERİ YOK -class EditFormFactory extends FormFactory -{ - public function create(): Form - { - $form = parent::create(); - $form->addText('title', 'Başlık:'); - // diğer form alanları buraya eklenir - $form->addSubmit('send', 'Kaydet'); - return $form; - } -} -``` - -Burada kalıtım kullanmak tümüyle ters teperdi. Çok kısa sürede sorunlarla karşılaşırdınız. Örneğin `create()` metoduna parametre eklemek isteseydiniz, imzası atanınkinden farklı olacağı için PHP hata verirdi. Ya da `EditFormFactory` sınıfına yapıcı üzerinden bağımlılık aktarırken. Bu da [constructor hell |dependency-injection:passing-dependencies#Constructor Hell] denen duruma yol açardı. - -Genel olarak [kompozisyonu kalıtıma yeğlemek |dependency-injection:faq#Kompozisyon neden kalıtıma yeğlenir?] daha iyidir. - - -Formun İşlenmesi -================ - -Başarılı gönderim üzerine çağrılan form işleyicisi de factory sınıfının bir parçası olabilir. Çalışma biçimi, gönderilen verileri işlenmek üzere model katmanına aktarmaktır. İşleme hataları [forma geri |forms:validation#İşleme Hataları] aktarılır. Aşağıdaki örnekte modeli `Facade` sınıfı temsil ediyor: - -```php -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - private Facade $facade, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - $form->addText('title', 'Başlık:'); - // diğer form alanları buraya eklenir - $form->addSubmit('send', 'Kaydet'); - $form->onSuccess[] = $this->processForm(...); - return $form; - } - - private function processForm(Form $form, array $data): void - { - try { - // gönderilen verilerin işlenmesi - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - } - } -} -``` - -Ancak yönlendirmeyi presenter'ın kendisi üstlensin. `onSuccess` olayına, yönlendirmeyi yapan başka bir işleyici ekler. Böylece form çeşitli presenter'larda kullanılabilir ve her biri başarı durumunda farklı bir yere yönlendirebilir. - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditFormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->create(); - $form->onSuccess[] = function () { - $this->flashMessage('Kayıt kaydedildi'); - $this->redirect('Homepage:'); - }; - return $form; - } -} -``` - -Bu çözüm, formların şu özelliğinden yararlanır: form üzerinde ya da öğelerinden birinde `addError()` çağrılırsa, sonraki `onSuccess` işleyicileri çağrılmaz. - - -Form Sınıfından Türetme -======================= - -Kurulmuş bir form, `Form` sınıfının torunu olmamalıdır. Başka bir deyişle, şu yaklaşımdan kaçının: - -```php -// ⛔ HAYIR! KALITIMIN BURADA YERİ YOK -class EditForm extends Form -{ - public function __construct(Translator $translator) - { - parent::__construct(); - $this->addText('title', 'Başlık:'); - // diğer form alanları buraya eklenir - $this->addSubmit('send', 'Kaydet'); - $this->setTranslator($translator); - } -} -``` - -Formu yapıcının içinde kurmak yerine bir factory kullanın. - -Şunu fark etmek önemlidir: `Form` sınıfı öncelikle form kurmaya yarayan bir araçtır, yani bir *form builder*. Kurulmuş form ise onun ürünü sayılabilir. Ancak ürün, builder'ın özel bir türü değildir; aralarında kalıtımın temeli olan bir *is a* ilişkisi yoktur. - - -Form Bileşeni -============= - -Tümüyle farklı bir yaklaşım, formu içine alan bir [bileşen |application:components] oluşturmaktır. Bu, yeni olanaklar açar; örneğin bileşenin kendi şablonu olduğundan formu belirli bir biçimde render edebilirsiniz. Ya da AJAX iletişimi ve forma dinamik olarak bilgi yüklemek için (örneğin öneriler vb.) sinyaller kullanılabilir. - - -```php -use Nette\Application\UI\Form; - -class EditControl extends Nette\Application\UI\Control -{ - public array $onSave = []; - - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentForm(): Form - { - $form = new Form; - $form->addText('title', 'Başlık:'); - // diğer form alanları buraya eklenir - $form->addSubmit('send', 'Kaydet'); - $form->onSuccess[] = $this->processForm(...); - - return $form; - } - - private function processForm(Form $form, array $data): void - { - try { - // gönderilen verilerin işlenmesi - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - return; - } - - // olayın çağrılması - $this->onSave($this, $data); - } -} -``` - -Sonra bu bileşeni üretecek bir factory oluşturalım. [Arayüzünü tanımlamak |application:components#Bağımlılıkları olan bileşenler] yeterlidir: - -```php -interface EditControlFactory -{ - function create(): EditControl; -} -``` - -Ve onu yapılandırma dosyasına ekleyin: - -```neon -services: - - EditControlFactory -``` - -Artık factory'yi isteyip presenter'da kullanabiliriz: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditControlFactory $controlFactory, - ) { - } - - protected function createComponentEditForm(): EditControl - { - $control = $this->controlFactory->create(); - - $control->onSave[] = function (EditControl $control, $data) { - $this->redirect('this'); - // ya da düzenleme sonucuna yönlendir, örneğin: - // $this->redirect('detail', ['id' => $data->id]); - }; - - return $control; - } -} -``` diff --git a/best-practices/tr/inject-method-attribute.texy b/best-practices/tr/inject-method-attribute.texy deleted file mode 100644 index 52689032c5..0000000000 --- a/best-practices/tr/inject-method-attribute.texy +++ /dev/null @@ -1,61 +0,0 @@ -Inject Metotları ve Attribute'ları -********************************** - -.[perex] -Bu yazı, Nette framework'ünde bağımlılıkları presenter'lara aktarmanın çeşitli yollarını ele alıyor. Tercih edilen yöntem olan constructor injection'ı, `inject` metotları ve attribute'ları gibi alternatiflerle karşılaştıracağız. - -Presenter'larda da, diğer sınıflarda olduğu gibi, bağımlılıkları [yapıcı |dependency-injection:passing-dependencies#Yapıcı Enjeksiyonu] üzerinden aktarmak tercih edilen yaklaşımdır. Ancak diğer presenter'ların türediği ortak bir ata sınıf (örneğin `BasePresenter`) oluşturursanız ve bu atanın da bağımlılıkları varsa, [constructor hell |dependency-injection:passing-dependencies#Constructor Hell] adıyla bilinen bir sorun ortaya çıkabilir. Bu, alternatif yöntemlerle, yani inject metotları ve attribute'larıyla (eskiden açıklamalarla) aşılabilir. - - -`inject*()` Metotları -===================== - -Bu, bağımlılıkları [setter'lar |dependency-injection:passing-dependencies#Setter Enjeksiyonu] üzerinden aktarmanın bir biçimidir. Bu setter'ların adları `inject` önekiyle başlamalıdır. Nette DI, bu şekilde adlandırılmış metotları presenter örneği oluşturulur oluşturulmaz otomatik olarak çağırır ve gereken tüm bağımlılıkları aktarır. Bu yüzden public olarak bildirilmeleri gerekir. - -`inject*()` metotları, yapıcının birden çok metoda bölünmüş uzantıları gibi görülebilir. Bu, `BasePresenter`'ın bağımlılıklarını ayrı bir metotla almasını sağlar ve yapıcıyı torunlarına bırakır: - -```php -abstract class BasePresenter extends Nette\Application\UI\Presenter -{ - private Foo $foo; - - public function injectBase(Foo $foo): void - { - $this->foo = $foo; - } -} - -class MyPresenter extends BasePresenter -{ - private Bar $bar; - - public function __construct(Bar $bar) - { - $this->bar = $bar; - } -} -``` - -Bir presenter istediği kadar `inject*()` metoduna sahip olabilir ve her biri istediği kadar parametre alabilir. Bu yaklaşım, presenter'ın [trait'lerden kurulduğu |presenter-traits] ve her trait'in kendi bağımlılıklarını gerektirdiği durumlara da çok uygundur. - - -`Inject` Attribute'ları -======================= - -Bu, [özelliklere enjekte etmenin |dependency-injection:passing-dependencies#Özellik Enjeksiyonu] bir biçimidir. Enjekte edilmesi gereken özellikleri işaretlemeniz yeterlidir; Nette DI, presenter örneği oluşturulur oluşturulmaz bağımlılıkları otomatik olarak aktarır. Enjeksiyonun mümkün olması için bu özelliklerin public olarak bildirilmesi gerekir. - -Özellikler bir attribute ile işaretlenir: (eskiden `/** @inject */` açıklaması kullanılıyordu) - -```php -use Nette\DI\Attributes\Inject; // bu satır önemli - -class MyPresenter extends Nette\Application\UI\Presenter -{ - #[Inject] - public Cache $cache; -} -``` - -Bu bağımlılık aktarma yönteminin üstünlüğü, çok derli toplu söz dizimiydi. Ancak [constructor property promotion |https://blog.nette.org/tr/php-8-0-complete-overview-of-news#toc-constructor-property-promotion] geldikten sonra yapıcıyı kullanmak çoğu zaman daha basit görünüyor. - -Buna karşılık bu yöntem, genel olarak property injection'ın taşıdığı sakıncaların aynısını taşır: değişkenin değişimi üzerinde denetimimiz olmaz ve değişken, sınıfın public arayüzünün bir parçası hâline gelir; bu da genellikle istenmeyen bir durumdur. diff --git a/best-practices/tr/lets-create-contact-form.texy b/best-practices/tr/lets-create-contact-form.texy deleted file mode 100644 index bf686c2288..0000000000 --- a/best-practices/tr/lets-create-contact-form.texy +++ /dev/null @@ -1,218 +0,0 @@ -Bir İletişim Formu Oluşturalım -****************************** - -.[perex] -Nette'de bir iletişim formunu, gönderilen verilerin e-postayla iletilmesi de dahil olmak üzere nasıl oluşturacağımıza bakalım. Başlayalım! - -Önce yeni bir proje oluşturmamız gerekiyor. [Başlangıç |nette:installation] sayfası bunun nasıl yapılacağını anlatıyor. Sonra formu oluşturmaya başlayabiliriz. - -En basit yaklaşım, [formu doğrudan presenter'ın içinde |forms:in-presenter] oluşturmaktır. Var olan `HomePresenter` sınıfını kullanabiliriz. Formumuzu temsil etmesi için `contactForm` adlı bir bileşen ekleyeceğiz. Bunu, presenter'ın koduna bileşeni oluşturacak `createComponentContactForm()` factory metodunu ekleyerek yaparız: - -```php -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - protected function createComponentContactForm(): Form - { - $form = new Form; - $form->addText('name', 'Ad:') - ->setRequired('Lütfen adınızı girin'); - $form->addEmail('email', 'E-posta:') - ->setRequired('Lütfen e-postanızı girin'); - $form->addTextArea('message', 'Mesaj:') - ->setRequired('Lütfen bir mesaj girin'); - $form->addSubmit('send', 'Gönder'); - $form->onSuccess[] = $this->contactFormSucceeded(...); - return $form; - } - - private function contactFormSucceeded(Form $form, $data): void - { - // e-posta gönderimi - } -} -``` - -Gördüğünüz gibi iki metot oluşturduk. İlki, `createComponentContactForm()`, yeni bir form örneği oluşturur. Sırasıyla `addText()`, `addEmail()` ve `addTextArea()` metotlarıyla eklenen ad, e-posta ve mesaj alanlarını içerir. Bir de gönder düğmesi ekledik. Peki kullanıcı bir alanı boş bırakırsa ne olur? O durumda ona alanın zorunlu olduğunu bildirmeliyiz. Bunu `setRequired()` metoduyla sağladık. Son olarak, form başarıyla gönderildiğinde tetiklenen `onSuccess` [olayına |nette:glossary#Olaylar] bir işleyici bağladık. Bizim durumumuzda bu, gönderilen verilerin işlenmesini üstlenecek `contactFormSucceeded` metodunu çağırır. Bu metodu birazdan yazacağız. - -`contactForm` bileşenini `Home/default.latte` şablonunda render edelim: - -```latte -{block content} -<h1>İletişim Formu</h1> -{control contactForm} -``` - -E-postanın kendisini göndermek için `ContactFacade` adında yeni bir sınıf oluşturup `app/Model/ContactFacade.php` dosyasına koyacağız: - -```php -namespace App\Model; - -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $mail = new Message; - $mail->addTo('admin@example.com') // sizin e-postanız - ->setFrom($email, $name) - ->setSubject('İletişim formundan mesaj') - ->setBody($message); - - $this->mailer->send($mail); - } -} -``` - -`sendMessage()` metodu e-postayı oluşturur ve gönderir. Bunun için, yapıcı üzerinden bağımlılık olarak aldığı bir mailer servisini kullanır. [E-posta gönderme |mail:] hakkında daha fazlasını okuyun. - -Şimdi presenter'a dönelim ve `contactFormSucceeded()` metodunu tamamlayalım. Bu metot, `ContactFacade` sınıfının `sendMessage()` metodunu çağıracak ve form üzerinden gönderilen verileri aktaracak. Peki `ContactFacade` nesnesini nasıl elde ederiz? Onu, bağımlılık enjeksiyonu kullanarak yapıcı üzerinden isteyeceğiz: - -```php -use App\Model\ContactFacade; -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - public function __construct( - private ContactFacade $facade, - ) { - } - - protected function createComponentContactForm(): Form - { - // ... - } - - public function contactFormSucceeded(stdClass $data): void - { - $this->facade->sendMessage($data->email, $data->name, $data->message); - $this->flashMessage('Mesaj gönderildi'); - $this->redirect('this'); - } -} -``` - -E-posta gönderildikten sonra kullanıcıya gönderimi doğrulayan bir [flash mesajı |application:components#Flash mesajları] gösteririz. Ardından, tarayıcı yenilenerek formun yeniden gönderilmesini önlemek için yönlendirme yaparız. - - -Yani her şey doğru kurulduysa, artık iletişim formunuzdan e-posta gönderebiliyor olmalısınız. Tebrikler! - - -HTML E-posta Şablonu --------------------- - -Şu anda yalnızca form üzerinden gönderilen mesajı içeren düz metin bir e-posta gönderiliyor. Ancak görünümünü daha çekici kılmak için e-postada HTML kullanabiliriz. Bunun için Latte'de bir şablon oluşturup `app/Model/contactEmail.latte` olarak kaydedeceğiz: - -```latte -<html> - <title>İletişim formundan mesaj - - -

    Ad: {$name}

    -

    E-posta: {$email}

    -

    Mesaj: {$message}

    - - -``` - -Geriye `ContactFacade` sınıfını bu şablonu kullanacak şekilde değiştirmek kalıyor. Yapıcıda, [Latte şablon render'ı |latte:develop#Bir şablon nasıl render edilir] olan `Latte\Engine` nesnesini oluşturabilen `LatteFactory` sınıfını isteyeceğiz. `renderToString()` metoduyla şablonu bir dizeye render ederiz. İlk parametre şablon dosyasının yolu, ikincisi ise ona aktarılacak değişkenlerden oluşan bir dizidir. - -```php -namespace App\Model; - -use Nette\Bridges\ApplicationLatte\LatteFactory; -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $latte = $this->latteFactory->create(); - $body = $latte->renderToString(__DIR__ . '/contactEmail.latte', [ - 'email' => $email, - 'name' => $name, - 'message' => $message, - ]); - - $mail = new Message; - $mail->addTo('admin@example.com') // sizin e-postanız - ->setFrom($email, $name) - ->setHtmlBody($body); - - $this->mailer->send($mail); - } -} -``` - -Üretilen HTML e-posta içeriğini, özgün `setBody()` yerine `setHtmlBody()` metoduna aktarırız. Ayrıca e-postanın konusunu `setSubject()` ile belirtmemize de gerek kalmaz; kütüphane onu şablondaki `` elemanından otomatik olarak çıkarır. - - -Yapılandırma ------------- - -`ContactFacade` sınıfının kodunda yönetici e-postamız `admin@example.com` hâlâ sabit kodlanmış durumda. Bunu yapılandırma dosyasına taşımak daha iyi olurdu. Bunu nasıl yaparız? - -Önce `ContactFacade` sınıfını değiştirin ve sabit kodlanmış e-posta dizesinin yerine yapıcı üzerinden aktarılan bir değişken koyun: - -```php -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - private string $adminEmail, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - // ... - $mail = new Message; - $mail->addTo($this->adminEmail) - ->setFrom($email, $name) - ->setHtmlBody($body); - // ... - } -} -``` - -İkinci adım, bu değişkenin değerini yapılandırmada vermektir. `app/config/services.neon` dosyasına şunu ekleyin: - -```neon -services: - - App\Model\ContactFacade(adminEmail: admin@example.com) -``` - -Ve hepsi bu. `services` bölümü çok sayıda girdi içeriyorsa ve e-posta adresinin aralarında kaybolduğunu düşünüyorsanız, onu bir parametreye dönüştürebiliriz. Girdiyi şöyle değiştirin: - -```neon -services: - - App\Model\ContactFacade(adminEmail: %adminEmail%) -``` - -Ve bu parametreyi `app/config/common.neon` dosyasında tanımlayın: - -```neon -parameters: - adminEmail: admin@example.com -``` - -Ve iş tamam! diff --git a/best-practices/tr/microsites.texy b/best-practices/tr/microsites.texy deleted file mode 100644 index e2bb5836fa..0000000000 --- a/best-practices/tr/microsites.texy +++ /dev/null @@ -1,63 +0,0 @@ -Mikro Siteler Nasıl Yazılır -*************************** - -Şirketinizin yaklaşan etkinliği için hızlıca küçük bir web sitesi oluşturmanız gerektiğini düşünün. Basit, hızlı ve gereksiz karmaşıklıklardan uzak olmalı. Böylesine küçük bir proje için güçlü bir framework'e gerek olmadığını düşünebilirsiniz. Peki ya Nette Framework kullanmak bu süreci gerçekten basitleştirip hızlandırıyorsa? - -Basit web siteleri oluştururken bile rahatlıktan ödün vermek istemezsiniz. Zaten çözülmüş olanı yeniden icat etmek istemezsiniz. Rahatça tembel olun ve kendinizi şımartın. Nette Framework, mikro framework olarak kullanmak için de harikadır. - -Böyle bir mikro site nasıl görünebilir? Örneğin sitenin tüm kodu, genel dizindeki tek bir `index.php` dosyasında bulunabilir: - -```php -<?php - -require __DIR__ . '/../vendor/autoload.php'; - -$configurator = new Nette\Bootstrap\Configurator; -$configurator->enableTracy(__DIR__ . '/../log'); -$configurator->setTempDirectory(__DIR__ . '/../temp'); - -// config.neon içindeki yapılandırmaya göre DI container oluştur -$configurator->addConfig(__DIR__ . '/../app/config.neon'); -$container = $configurator->createContainer(); - -// yönlendirmeyi kur -$router = new Nette\Application\Routers\RouteList; -$container->addService('router', $router); - -// https://example.com/ URL'si için rota -$router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { - // tarayıcı dilini sapta ve /en ya da /de gibi bir URL'ye yönlendir - $supportedLangs = ['en', 'de', 'cs']; - $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); - return $presenter->redirectUrl("/$lang"); -}); - -// https://example.com/cs ya da https://example.com/en URL'si için rota -$router->addRoute('<lang cs|en|de>', function ($presenter, string $lang) { - // uygun şablonu göster, örneğin ../templates/en.latte - $template = $presenter->createTemplate() - ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); - return $template; -}); - -// uygulamayı çalıştır! -$container->getByType(Nette\Application\Application::class)->run(); -``` - -Geri kalan her şey, üst `/templates` dizininde saklanan şablonlar olacak. - -`index.php` içindeki PHP kodu önce [ortamı kurar |bootstrap:], sonra [rotaları |application:routing#Callback'lerle dinamik yönlendirme] tanımlar ve en sonunda uygulamayı çalıştırır. Üstünlüğü, `addRoute()` fonksiyonunun ikinci parametresinin, ilgili sayfaya erişildiğinde çalıştırılan bir callable olabilmesidir. - - -Mikro Siteler İçin Neden Nette? -------------------------------- - -- [Tracy|tracy:] aracını denemiş programcılar, bugün onsuz kod yazmayı zor hayal eder. -- Her şeyden önce [Latte|latte:] şablon sisteminden yararlanırsınız; çünkü yalnızca iki sayfada bile [yerleşimi ve içeriği|latte:template-inheritance] ayırmak isteyeceksiniz. -- Ve XSS açıklarını önlemek için kesinlikle [otomatik kaçışlamaya |latte:safety-first] güvenmek isteyeceksiniz. -- Nette ayrıca, bir hata durumunda ham PHP hata mesajlarının asla gösterilmemesini, bunun yerine kullanıcı dostu bir sayfanın görünmesini sağlar. -- Belki bir iletişim formuyla kullanıcı geri bildirimi toplamak isterseniz, [form|forms:] ve [veritabanı|database:] desteğini kolayca ekleyebilirsiniz. -- Doldurulan formların [e-postayla gönderilmesini|mail:] de kolayca sağlayabilirsiniz. -- Bazen [önbellekleme|caching:] işe yarayabilir; örneğin akışları indirip gösterirken. - -Hızın ve verimliliğin can alıcı olduğu bugünün hızlı dünyasında, gereksiz gecikmeler olmadan sonuca ulaşmanızı sağlayan araçlara sahip olmak hayati önemdedir. Nette Framework tam da bunu sunar: hızlı geliştirme, güvenlik ve süreci akıcılaştıran Tracy ile Latte gibi geniş bir araç yelpazesi. Birkaç Nette paketini kurmanız yeter, böyle bir mikro site oluşturmak inanılmaz kolaylaşır. Üstelik gizli güvenlik açıkları olmadığından emin olabilirsiniz. diff --git a/best-practices/tr/pagination.texy b/best-practices/tr/pagination.texy deleted file mode 100644 index db396606d7..0000000000 --- a/best-practices/tr/pagination.texy +++ /dev/null @@ -1,273 +0,0 @@ -Veritabanı Sonuçlarını Sayfalama -******************************** - -.[perex] -Web uygulamaları geliştirirken, sayfa başına listelenen öğe sayısını sınırlama gereksinimiyle sık karşılaşırsınız; buna sayfalama denir. - -Tüm verileri sayfalamasız listelediğimiz bir durumdan başlayalım. Veritabanından veri seçmek için bir `ArticleRepository` sınıfımız var. Yapıcının yanı sıra, yayımlanmış tüm makaleleri yayın tarihine göre azalan sırada döndüren bir `findPublishedArticles` metodu içerir. - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC', - new \DateTime, - ); - } -} -``` - -Bu model sınıfını presenter'a enjekte ederiz. Render metodunda yayımlanmış makaleleri alır ve şablona aktarırız: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(): void - { - $this->template->articles = $this->articleRepository->findPublishedArticles(); - } -} -``` - -`default.latte` şablonu da makaleleri listelemeyi üstlenir: - -```latte -{block content} -<h1>Makaleler</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> -``` - - -Böylece tüm makaleleri listeleyebiliriz, ama makale sayısı arttıkça bu sorun olmaya başlar. İşte o noktada bir sayfalama düzeneği kurmak işe yarar. - -Bu düzenek tüm makaleleri birkaç sayfaya böler ve yalnızca o an seçili sayfaya ait makaleleri gösteririz. Toplam sayfa sayısını ve makalelerin bölünmesini, toplam makale sayısı ile sayfa başına istenen makale sayısına dayanarak [Paginator |utils:Paginator] aracı hesaplar. - -İlk adımda, repository sınıfındaki makale getirme metodunu, yalnızca tek bir sayfaya ait makaleleri döndürebilecek şekilde değiştireceğiz. Ayrıca, Paginator'ı yapılandırmak için gereken, veritabanındaki toplam makale sayısını veren bir metot ekleyeceğiz: - -```php -namespace App\Model; - -use Nette; - - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(int $limit, int $offset): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC - LIMIT ? - OFFSET ?', - new \DateTime, $limit, $offset, - ); - } - - /** - * Yayımlanmış makalelerin toplam sayısını döndürür - */ - public function getPublishedArticlesCount(): int - { - return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime); - } -} -``` - -Sonra presenter'ı değiştirelim. Geçerli sayfa numarasını `renderDefault` metoduna aktaracağız. Bu numara URL'nin bir parçası değilse varsayılan değer olarak 1 (ilk sayfa) alacağız. - -Ayrıca render metodunu, bir Paginator örneği oluşturup yapılandıracak ve şablonda gösterilecek uygun makaleleri seçecek şekilde genişleteceğiz. Değiştirilmiş `HomePresenter` şöyle görünür: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Yayımlanmış makalelerin toplam sayısını al - $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - - // Paginator örneğini oluştur ve yapılandır - $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // toplam öğe sayısı - $paginator->setItemsPerPage(10); // sayfa başına öğe - $paginator->setPage($page); // geçerli sayfa numarası - - // Paginator'ın hesabına göre veritabanından sınırlı bir makale kümesi getir - $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - - // onları şablona aktar - $this->template->articles = $articles; - // ve sayfalama denetimlerini göstermek için Paginator'ın kendisini de - $this->template->paginator = $paginator; - } -} -``` - -Şablon artık yalnızca geçerli sayfanın makaleleri üzerinde dönüyor. Yalnızca sayfalama bağlantılarını eklememiz gerekiyor: - -```latte -{block content} -<h1>Makaleler</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if !$paginator->isFirst()} - <a n:href="default, 1">İlk</a> -  |  - <a n:href="default, $paginator->getPage() - 1">Önceki</a> -  |  - {/if} - - Sayfa {$paginator->getPage()} / {$paginator->getPageCount()} - - {if !$paginator->isLast()} -  |  - <a n:href="default, $paginator->getPage() + 1">Sonraki</a> -  |  - <a n:href="default, $paginator->getPageCount()">Son</a> - {/if} -</div> -``` - - -Böylece Paginator kullanan sayfalama tamamlandı. Veritabanı katmanı olarak [Nette Database Explorer |database:explorer] kullanıyorsanız ([Nette Database Core |database:sql-way] yerine), sayfalamayı doğrudan Paginator aracını kullanmadan da gerçekleştirebilirsiniz. `Nette\Database\Table\Selection` sınıfı, sayfalama mantığını kendi içinde barındıran bir [page() |api:Nette\Database\Table\Selection::page()] metodu içerir. - -Bu yaklaşımda repository şöyle görünür: - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Explorer $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\Table\Selection - { - return $this->database->table('articles') - ->where('created_at < ', new \DateTime) - ->order('created_at DESC'); - } -} -``` - -Presenter'da bir Paginator örneği oluşturmamız gerekmez. Bunun yerine, repository'den dönen `Selection` nesnesinin sunduğu `page()` metodunu kullanırız: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Yayımlanmış makaleleri getir - $articles = $this->articleRepository->findPublishedArticles(); - - // ve şablona yalnızca page metodunun hesabıyla sınırlanan bölümünü aktar - $lastPage = 0; - $this->template->articles = $articles->page($page, 10, $lastPage); - - // ve sayfalama seçeneklerini göstermek için gereken verileri de - $this->template->page = $page; - $this->template->lastPage = $lastPage; - } -} -``` - -Şablona artık Paginator nesnesini aktarmadığımız için, sayfalama bağlantılarını gösteren bölümü uyarlamamız gerekiyor: - -```latte -{block content} -<h1>Makaleler</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if $page > 1} - <a n:href="default, 1">İlk</a> -  |  - <a n:href="default, $page - 1">Önceki</a> -  |  - {/if} - - Sayfa {$page} / {$lastPage} - - {if $page < $lastPage} -  |  - <a n:href="default, $page + 1">Sonraki</a> -  |  - <a n:href="default, $lastPage">Son</a> - {/if} -</div> -``` - -Böylece sayfalama düzeneğini, Paginator aracını açıkça kullanmadan gerçekleştirmiş olduk. - -{{priority: -1}} diff --git a/best-practices/tr/passing-settings-to-presenters.texy b/best-practices/tr/passing-settings-to-presenters.texy deleted file mode 100644 index bab7e1227c..0000000000 --- a/best-practices/tr/passing-settings-to-presenters.texy +++ /dev/null @@ -1,49 +0,0 @@ -Presenter'lara Ayar Aktarma -*************************** - -.[perex] -Presenter'lara, autowiring ile otomatik olarak aktarılamayan nesne olmayan argümanlar (hata ayıklama kipini gösteren bir bayrak, dizin yolları vb.) aktarmanız mı gerekiyor? Çözüm, bunları özel bir `Settings` nesnesinin içine almaktır. - -`Settings` servisi, çalışan uygulamayla ilgili bilgileri presenter'lara ulaştırmanın çok basit ama etkili bir yoludur. Somut yapısı tümüyle sizin ihtiyaçlarınıza bağlıdır. Örnek: - -```php -namespace App; - -class Settings -{ - public function __construct( - // PHP 8.1'den beri readonly kullanılabilir - public bool $debugMode, - public string $appDir, - // vb. - ) {} -} -``` - -Yapılandırmada kaydetme örneği: - -```neon -services: - - App\Settings( - %debugMode%, - %appDir%, - ) -``` - -Bir presenter bu servisin sunduğu bilgilere ihtiyaç duyduğunda, onu yalnızca yapıcısında ister: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private App\Settings $settings, - ) {} - - public function renderDefault() - { - if ($this->settings->debugMode) { - // ... - } - } -} -``` diff --git a/best-practices/tr/post-links.texy b/best-practices/tr/post-links.texy deleted file mode 100644 index 790f444fc0..0000000000 --- a/best-practices/tr/post-links.texy +++ /dev/null @@ -1,56 +0,0 @@ -POST Bağlantıları Doğru Şekilde Nasıl Kullanılır -************************************************ - -.[perex] -Web uygulamalarında, özellikle yönetim arayüzlerinde, sunucunun durumunu değiştiren eylemlerin HTTP GET metoduyla yapılmaması temel bir kural olmalıdır. Adından da anlaşılacağı gibi GET yalnızca veri almak için kullanılmalı, veriyi değiştirmek için değil. Kayıt silme gibi eylemlerde POST metodu daha uygundur. DELETE metodu ideal olurdu, ama JavaScript olmadan çağrılamaz; bu yüzden bu tür eylemlerde tarihsel olarak POST kullanılmıştır. - -Bu pratikte nasıl gerçekleştirilir? Şu basit numarayı kullanın. Yerleşim şablonunuzun başında `postForm` kimliğine sahip bir yardımcı form oluşturun. Ardından bu formu silme düğmeleri gibi eylemlerde kullanırsınız: - -```latte .{file:@layout.latte} -<form method="post" id="postForm"></form> -``` - -Bu form sayesinde, standart bir `<a>` bağlantısı yerine bir `<button>` kullanabilirsiniz. Bu düğme, sıradan bir bağlantı gibi görünecek şekilde biçimlendirilebilir. Örneğin Bootstrap CSS framework'ü `btn btn-link` sınıflarını sunar; bu da düğmeyi diğer bağlantılardan görsel olarak ayırt edilemez kılar. `form="postForm"` niteliğiyle düğmeyi hazırladığınız yardımcı forma bağlayın: - -```latte .{file:admin.latte} -<table> - <tr n:foreach="$posts as $post"> - <td>{$post->title}</td> - <td> - <button class="btn btn-link" form="postForm" formaction="{link delete, $post->id}">sil</button> - <!-- <a n:href="delete, $post->id">sil</a> yerine --> - </td> - </tr> -</table> -``` - -Bu düğmeye tıklamak artık `delete` eylemini çağırır. İsteklerin yalnızca POST metoduyla kabul edilmesini ve aynı alan adından gelmesini sağlamak için (CSRF saldırılarına karşı etkili bir savunma) `#[Requires]` attribute'unu kullanın: - -```php .{file:AdminPresenter.php} -use Nette\Application\Attributes\Requires; - -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST', sameOrigin: true)] - public function actionDelete(int $id): void - { - $this->facade->deletePost($id); // kaydı silen varsayımsal kod - $this->redirect('default'); - } -} -``` - -Bu attribute, Nette Application 3.2 sürümünden beri kullanılabilir. Yetenekleri hakkında daha fazlasını [#Requires attribute'u nasıl kullanılır |attribute-requires] sayfasında öğrenebilirsiniz. - -`actionDelete()` eylemi yerine `handleDelete()` sinyalini kullanıyorsanız, `sameOrigin: true` belirtmeye gerek yoktur; çünkü sinyallerde bu koruma varsayılan olarak açıktır: - -```php .{file:AdminPresenter.php} -#[Requires(methods: 'POST')] -public function handleDelete(int $id): void -{ - $this->facade->deletePost($id); - $this->redirect('this'); -} -``` - -Bu yaklaşım yalnızca uygulamanızın güvenliğini artırmakla kalmaz, doğru web standartlarına ve uygulamalarına bağlı kalmayı da teşvik eder. Durum değiştiren eylemlerde POST metodunu kullanmak, daha sağlam ve daha güvenli bir uygulama demektir. diff --git a/best-practices/tr/presenter-traits.texy b/best-practices/tr/presenter-traits.texy deleted file mode 100644 index abcbe39854..0000000000 --- a/best-practices/tr/presenter-traits.texy +++ /dev/null @@ -1,47 +0,0 @@ -Presenter'ları Trait'lerden Kurma -********************************* - -.[perex] -Aynı işlevi birden çok presenter'da gerçekleştirmeniz gerekiyorsa (örneğin kullanıcının oturum açtığını doğrulamak), kodu ortak bir ataya koymak yaygın bir yaklaşımdır. Bir başka seçenek de tek amaçlı [trait'ler |nette:introduction-to-object-oriented-programming#Trait'ler] oluşturmaktır. - -Trait kullanmanın üstünlüğü, her presenter'ın yalnızca gerçekten ihtiyaç duyduğu trait'leri almasıdır; hele ki PHP'de çoklu kalıtım desteklenmediği düşünülürse. - -Bu trait'ler, presenter örneği oluşturulurken tüm [inject metotlarının |inject-method-attribute#inject*() Metotları] sırayla çağrıldığı gerçeğinden yararlanabilir. Yalnızca her inject metodunun adının, kullanılan tüm trait'ler ve presenter'ın kendisi arasında benzersiz olmasını sağlamanız gerekir. - -Trait'ler, başlatma kodunu [onStartup ya da onRender |application:presenters#Olaylar] olaylarına bağlayabilir. - -Örnekler: - -```php -trait RequireLoggedUser -{ - public function injectRequireLoggedUser(): void - { - $this->onStartup[] = function () { - if (!$this->getUser()->isLoggedIn()) { - $this->redirect('Sign:in', $this->storeRequest()); - } - }; - } -} - -trait StandardTemplateFilters -{ - public function injectStandardTemplateFilters(TemplateBuilder $builder): void - { - $this->onRender[] = function () use ($builder) { - $builder->setupTemplate($this->template); - }; - } -} -``` - -Presenter ise yalnızca bu trait'leri kullanır: - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - use StandardTemplateFilters; - use RequireLoggedUser; -} -``` diff --git a/best-practices/tr/pretty-urls.texy b/best-practices/tr/pretty-urls.texy deleted file mode 100644 index 41fa193089..0000000000 --- a/best-practices/tr/pretty-urls.texy +++ /dev/null @@ -1,204 +0,0 @@ -Slug'larla Güzel URL'ler -************************ - -.[perex] -`/article/123-ekmek-nasil-pisirilir` gibi URL'ler `/article/123` adresinden daha iyi görünür ve hem kullanıcıların hem de arama motorlarının sayfada ne olduğunu anlamasına yardım eder. Bu kılavuz, bunları tümüyle router içinde, tek bir şablona bile dokunmadan nasıl üreteceğinizi ve her ziyaretçinin kurallı (canonical) URL'ye ulaşmasını nasıl sağlayacağınızı gösteriyor. - - -URL'lerde Slug Neden Kullanılır -=============================== - -Şu iki adresi karşılaştırın: - -``` -/article/123 -/article/123-ekmek-nasil-pisirilir -``` - -İkincisi, tıklamadan sonra kullanıcıyı (ve Google'ı) neyin beklediğini söyler. SEO için iyidir, bağlantıları sohbette ya da e-postada okunur kılar ve adres çubuğuna bir anlam katar. - -Yine de slug gerçek bir tanımlayıcı değildir. Sayfayı ID belirler. Slug ise uygulamanın başlıktan ürettiği bir süstür. Başlık değişirse slug da değişmelidir. Ve biri URL'yi elle düzenlerse ya da eski bir bağlantıyı izlerse, uygulama yine de doğru sayfayı bulmalıdır. - - -Hedef -===== - -Bunların hepsini karşılayan bir rota istiyoruz: - -``` -/article/123 → 123 numaralı makaleyi açar, kurallı URL'ye yönlendirir -/article/123-ekmek-nasil-pisirilir → 123 numaralı makaleyi doğrudan açar -/article/123-biri-ne-yazdiysa → 123 numaralı makaleyi açar, kurallı URL'ye yönlendirir -/article/ → 404 (ID yok) -``` - -Ve uygulamadaki her `n:href` ile `link()` çağrısının otomatik olarak `/article/123-ekmek-nasil-pisirilir` üretmesini istiyoruz; **tek bir şablonu bile yeniden yazmadan**. - - -Rota Maskesi -============ - -Numara, maskede slug'ı köşeli parantezlerle **isteğe bağlı** işaretlemektir: - -```php -$router->addRoute('article/<id [0-9]+>[-<slug>]', 'Article:detail'); -``` - -`[-<slug>]` maskesi şunu söyler: ID'den sonra bir tire ve bir slug gelebilir, ama zorunlu değildir. Rota hem `/article/123` hem de `/article/123-herhangibirsey` adreslerini kabul eder. - -`<slug>` parametresine dair bir not: varsayılan olarak **eğik çizgi dışındaki** her karakterle eşleşir; tam da istediğimiz şey. `<slug .+>` yazarsanız parametre eğik çizgilerle de eşleşir, yani `/article/123-birsey/baska` adresi içinde `/` geçen tek bir slug olarak ayrıştırılır. Gerçekten ihtiyacınız yoksa varsayılan `<slug>` ile kalın. - -Buraya kadar URL doğru ayrıştırılıyor, ama üretilen bağlantılar slug içermeyecek. Sıradaki adım, rotaya slug'ı nasıl dolduracağını öğretmek. - - -Şablonlara Dokunmadan Slug Üretme -================================= - -Asıl marifet bu. Var olan `n:href="Article:detail, $id"` çağrıları uygulamanın tamamında değişmeden çalışmayı sürdürür; başlığı router kendisi arar. - -Bunu, boş dize anahtarı altındaki bir **genel filtreyle** yaparız; bu filtre tüm parametreleri bir arada görür ve slug'ı ekleyebilir: - -```php -use Nette\Routing\Route; -use Nette\Utils\Strings; - -$router->addRoute('article/<id [0-9]+>[-<slug>]', [ - 'presenter' => 'Article', - 'action' => 'detail', - '' => [ - Route::FilterOut => function (array $params) use ($slugProvider): array { - if (isset($params['id']) && empty($params['slug'])) { - $params['slug'] = $slugProvider->getSlug((int) $params['id']); - } - return $params; - }, - ], -]); -``` - -`FilterOut`, router bir URL **ürettiği** her seferde çalışır. Slug verilmemişse filtre başlığı arar ve ekler. - -Slug'ları tüm uygulamaya tek bir değişiklikle, yalnızca bir rota tanımıyla yayabilirsiniz. Her şablondaki her bağlantı otomatik olarak `/article/123-ekmek-nasil-pisirilir` üretmeye başlar. Ne grep, ne şablon avı, ne de gözden kaçan bir köşe durumu. - - -Aramayı Önbelleğe Alın -====================== - -Bir bağlantı bir veritabanı sorgusu üretir, ama tipik bir sayfada bunlardan bolca vardır: listeler, gezinme yolu, "son görüntülenenler", ilgili makaleler. Aynı makale ID'si tek bir istek sırasında sık sık birkaç bağlantıda görünür ve her seferinde veritabanına gitmek istemezsiniz. - -Küçük bir istek başına önbellek bunu çözer. Veritabanı çağrısını küçük bir servisin içine alın: - -```php -final class SlugProvider -{ - /** @var array<int, string> */ - private array $cache = []; - - public function __construct( - private Nette\Database\Explorer $db, - ) { - } - - public function getSlug(int $id): string - { - return $this->cache[$id] ??= Strings::webalize(Strings::truncate( - (string) $this->db->fetchField('SELECT title FROM article WHERE id = ?', $id), - 100, '' - )); - } -} -``` - -Bu kadarı yeter: istek başına, benzersiz ID başına bir veritabanı sorgusu. - - -Başlığı Şablondan Aktarma (İsteğe Bağlı Hızlı Yol) -================================================== - -Başlık zaten şablonda elinizin altındaysa veritabanı aramasını tümüyle atlayabilirsiniz. Başlığı adlandırılmış parametre olarak aktarın: - -```latte -<a n:href="Article:detail, $article->id, slug => $article->title">{$article->title}</a> -``` - -…ve başlığı URL için güvenli bir dizeye çeviren, parametre başına bir `FilterOut` ekleyin: - -```php -$router->addRoute('article/<id [0-9]+>[-<slug>]', [ - 'presenter' => 'Article', - 'action' => 'detail', - 'slug' => [ - Route::FilterOut => fn($title) => Strings::webalize(Strings::truncate($title, 100, '')), - ], - '' => [/* yukarıdaki arama yedeği */], -]); -``` - -İki filtre birlikte çalışır. Önce genel filtre çalışır; slug'ın verilen başlıkla zaten dolu olduğunu görünce veritabanı aramasını atlar. Ardından parametre başına `FilterOut` o başlığı düzgün bir slug'a çevirir. Başlığı aktarmayan şablonlar da çalışmayı sürdürür; genel filtre slug'ı boş bulur ve arama yolundan gider. - -Bunu yalnızca gerçekten önemli olduğu yerlerde kullanın (istek başına yüzlerce kez render edilen büyük listeler). Uygulamanın çoğu için önbellekli arama yeterince hızlıdır. - - -Kurallılaştırma: Doğru URL'ye Yönlendirme -========================================= - -Artık `/article/123-ekmek-nasil-pisirilir` üretebiliyoruz, ama rota hâlâ `/article/123` ve `/article/123-biri-ne-yazdiysa` adreslerini kabul ediyor. Bu bilinçli bir tercih: kısa URL'ler istiyoruz (aşağıda ayrıntısı var) ve eski ya da elle yazılmış bağlantıların çalışmayı sürdürmesini istiyoruz. Ama arama motorlarının aynı makaleyi birden çok adres altında indekslemesini istemiyoruz. - -Çözüm [kurallılaştırmadır |application:presenters#Kanonikleştirme]: kullanıcı kurallı olmayan bir URL üzerinden geldiğinde uygulama onu 301 ile doğru adrese yönlendirir. Bunu `canonicalize()` metodu halleder: - -```php -public function actionDetail(int $id, ?string $slug = null): void -{ - $article = $this->facade->getArticle($id); - if (!$article) { - $this->error(); - } - - // aynı FilterOut üzerinden kurallı URL'yi üretir - // ve geçerli URL'den farklıysa HTTP 301 ile yönlendirir - $this->canonicalize('detail', ['id' => $id]); - - $this->template->article = $article; -} -``` - -`canonicalize()`, kurallı URL'yi `link()` ile aynı şekilde üretir (yani aynı `FilterOut` üzerinden geçer) ve onu geçerli URL ile karşılaştırır. Farklılarsa HTTP 301 ile yönlendirir. Ziyaretçiler doğru URL'ye ulaşır, arama motorları yalnızca tek bir kurallı sürüm görür. - - -Slug'ın Nasıl Görüneceğine Karar Veren Tek Bir Yer -================================================== - -`Strings::webalize(Strings::truncate(..., 100, ''))` çağrısının tek bir yerde, `SlugProvider` içinde (ya da parametre başına `FilterOut` içinde) durduğuna dikkat edin. Aynı mantık, şablondaki bağlantıyı, `redirect()` içindeki URL'yi ve `canonicalize()` içindeki kurallı biçimi üretir. - -Kuralları sonradan değiştirmek isterseniz (farklı uzunluk sınırı, farklı harf çevirisi, fazladan karakterlerin atılması), tek bir satırı değiştirirsiniz. Bu olmadan, `redirect()` metodunun `/article/123-ekmek-nasil-pisirilir` üretirken `canonicalize()` metodunun `/article/123-ekmek-nasil-pisiri` beklemesi (çünkü biri başka bir yerde farklı bir `truncate` uzunluğu kullanmıştır) riskini alır ve uygulama sonsuz döngüde yönlendirirdi. - - -Bonus: Kısa URL'ler de Çalışmayı Sürdürür -========================================= - -Slug isteğe bağlı olduğundan, onsuz adresler de çalışır: - -``` -/article/123 -``` - -Bu şunlar için yararlıdır: -- **QR kodları** - daha kısa URL, daha seyrek ve daha kolay taranan bir kod demektir -- **SMS ve sohbet** - bir tweet'e sığar, derli toplu görünür -- **Basılı malzemeler** - kısa URL daha hızlı yazılır - -Kullanıcı böyle bir URL'yi açtığında `canonicalize()` onu 301 ile slug'lı tam sürüme yönlendirir; böylece arama motorları yine yalnızca kurallı biçimi görür. Kısalık ve SEO'yu aynı anda elde edebilirsiniz. - - -Özet -==== - -- `<id>[-<slug>]` maskesi slug'ı isteğe bağlı yapar. Varsayılan `<slug>`, `/` ile eşleşmez; slug'ın içinde eğik çizgi gerçekten istiyorsanız `<slug .+>` kullanın. -- `''` anahtarı altındaki genel bir `FilterOut` başlığı ID'ye göre arar; **uygulamanın hiçbir yerinde şablon değişikliği gerekmez**. -- Aramayı küçük bir istek başına önbelleğin içine alın; benzersiz ID başına bir veritabanı sorgusu fazlasıyla yeter. -- İsteğe bağlı olarak, parametre başına bir `FilterOut` şablonların başlığı doğrudan aktarıp aramayı atlamasını sağlar. -- Eylemdeki `$this->canonicalize()` çağrısı, kurallı olmayan URL'leri HTTP 301 ile doğru adrese yönlendirir. -- Slug formülü (`webalize` + `truncate`) tek bir yerde durur; bir kez değiştirin, her yerde geçerli olsun. -- Yalnızca ID içeren kısa URL'ler çalışmayı sürdürür; bu da QR kodları ve SMS için kullanışlıdır. - -Filtreler ve kurallılaştırma hakkında daha fazlasını [yönlendirme |application:routing#Genel filtreler] ve [presenter'lar |application:presenters#Kanonikleştirme] belgelerinde bulabilirsiniz. diff --git a/best-practices/tr/restore-request.texy b/best-practices/tr/restore-request.texy deleted file mode 100644 index 384ddfc3f4..0000000000 --- a/best-practices/tr/restore-request.texy +++ /dev/null @@ -1,62 +0,0 @@ -Önceki Sayfaya Nasıl Dönülür? -***************************** - -.[perex] -Kullanıcı bir formu doldururken oturumunun süresi dolarsa ne olur? Veri kaybını önlemek için, giriş sayfasına yönlendirmeden önce geçerli isteği (form verileriyle birlikte) oturuma kaydedebiliriz. Nette'de bu şaşırtıcı derecede kolaydır. - -Geçerli istek, `storeRequest()` metoduyla oturuma saklanabilir. Bu metot, saklanan istek için benzersiz bir tanımlayıcı (kısa bir dize) döndürür. Metot, geçerli presenter'ın adını, view'ını ve parametrelerini kaydeder. İsteğin bir parçası olarak bir form gönderilmişse, alanlara girilen değerler (yüklenen dosyalar hariç) de kaydedilir. - -İstek, daha önce alınan tanımlayıcıyı verdiğiniz `restoreRequest($key)` metoduyla geri yüklenir. Bu metot, kullanıcıyı özgün presenter ve view'a geri yönlendirir. Ancak saklanan istek bir form gönderimi içeriyorsa, `restoreRequest()` yönlendirme yerine `forward()` metodunu kullanır. Daha önce doldurulmuş değerleri forma geri aktarır ve yeniden render edilmesini sağlar. Böylece kullanıcı, girdiği verileri kaybetmeden formu yeniden gönderebilir. - -Çok önemli bir nokta: `restoreRequest()`, yeni oturum açan kullanıcının formu özgün olarak gönderen kullanıcıyla aynı olduğunu doğrular. Kullanıcı farklıysa saklanan istek geri yüklenmez ve metot hiçbir şey yapmaz; bu da güvenliği artırır. - -Bunu bir örnekle gösterelim. Verilerin düzenlendiği bir `AdminPresenter` düşünün. `startup()` metodu kullanıcının oturum açıp açmadığını doğrular. Açmamışsa kullanıcı `SignPresenter`'a yönlendirilir. Aynı anda geçerli isteği `storeRequest()` ile saklarız ve anahtarını (`$backlink`) `SignPresenter`'a aktarırız. - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - protected function startup() - { - parent::startup(); - - if (!$this->user->isLoggedIn()) { - $this->redirect('Sign:in', ['backlink' => $this->storeRequest()]); - } - } -} -``` - -`SignPresenter`, giriş formunun yanı sıra anahtarın saklandığı kalıcı bir `$backlink` parametresi içerir. Parametre kalıcı olduğundan, değeri giriş formu gönderildikten sonra da korunur. - - -```php -use Nette\Application\Attributes\Persistent; - -class SignPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $backlink = ''; - - protected function createComponentSignInForm() - { - $form = new Nette\Application\UI\Form; - // ... form alanlarını ekle ... - $form->onSuccess[] = $this->signInFormSuceeded(...); - return $form; - } - - private function signInFormSuceeded($form) - { - // ... kullanıcının oturumunu burada aç ... - - $this->restoreRequest($this->backlink); - $this->redirect('Admin:'); - } -} -``` - -Saklanan isteğin anahtarını (`$this->backlink`) `restoreRequest()` metoduna veririz. Metot da kullanıcıyı özgün presenter ve view'a geri yönlendirir (ya da forward eder). - -Ancak anahtar geçersizse (örneğin oturumdan düşmüşse) metot hiçbir şey yapmaz. Bu yüzden ardından gelen `$this->redirect('Admin:')` çağrısı bir yedek görevi görür ve `AdminPresenter` gibi varsayılan bir sayfaya yönlendirir. - -{{priority: -1}} diff --git a/bootstrap/cs/@home.texy b/bootstrap/cs/@home.texy deleted file mode 100644 index 193044b514..0000000000 --- a/bootstrap/cs/@home.texy +++ /dev/null @@ -1,102 +0,0 @@ -Nette Bootstrap -*************** - -.[perex] -Jednotlivé součásti Nette nastavujeme pomocí konfiguračních souborů. Ukážeme si, jak tyto soubory načítat. - -.[tip] -Pokud používáte celý framework, není potřeba nic dalšího dělat. V projektu máte pro konfigurační soubory předpřipravený adresář `config/` a jejich načítání má na starosti [zavaděč aplikace |application:bootstrapping#Konfigurace DI kontejneru]. Tento článek je pro uživatele, kteří používají jen jednu knihovnu Nette a chtějí využít možnosti konfiguračních souborů. - -Konfigurační soubory se obvykle zapisují ve [formátu NEON|neon:format] a nejlépe se upravují v [editorech s jeho podporou |tools:ide]. Lze je chápat jako návody, jak **vytvářet a konfigurovat** objekty. Tedy výsledkem načtení konfigurace bude tzv. továrna, což je objekt, který nám na požádání vytvoří další objekty, které chceme používat. Například databázové spojení apod. - -Této továrně se také říká *dependency injection kontejner* (DI container) a pokud by vás zajímaly podrobnosti, přečtěte si kapitolu o [dependency injection |dependency-injection:]. - -Načtení konfigurace a vytvoření kontejneru obstará třída [api:Nette\Bootstrap\Configurator], takže si nejprve nainstalujeme její balíček `nette/bootstrap`: - -```shell -composer require nette/bootstrap -``` - -A vytvoříme instanci třídy `Configurator`. Protože vygenerovaný DI kontejner se bude kešovat na disk, je nutné nastavit cestu k adresáři, kam se bude ukládat: - -```php -$configurator = new Nette\Bootstrap\Configurator; -$configurator->setTempDirectory(__DIR__ . '/temp'); -``` - -Na Linuxu nebo macOS nastavte adresáři `temp/` [práva pro zápis |nette:troubleshooting#Nastavení práv adresářů]. - -A dostáváme se k samotným konfiguračním souborům. Ty načteme pomocí `addConfig()`: - -```php -$configurator->addConfig(__DIR__ . '/database.neon'); -``` - -Pokud chceme přidat více konfiguračních souborů, můžeme funkci `addConfig()` zavolat vícekrát. Pokud se v souborech objeví prvky se stejnými klíči, budou přepsány (nebo v případě polí [sloučeny |dependency-injection:configuration#Slučování]). Později vkládaný soubor má vyšší prioritu než předchozí. - -Posledním krokem je vytvoření DI kontejneru: - -```php -$container = $configurator->createContainer(); -``` - -A ten nám už vytvoří požadované objekty. Pokud například používáte konfiguraci pro [Nette Database|database:configuration], můžete jej požádat o vytvoření databázových spojení: - -```php -$db = $container->getByType(Nette\Database\Connection::class); -// nebo -$explorer = $container->getByType(Nette\Database\Explorer::class); -// nebo při vytváření více spojení -$db = $container->getByName('database.main.connection'); -``` - -A nyní už můžete s databází pracovat! - - -Vývojářský vs produkční režim ------------------------------ - -Ve vývojářském režimu se kontejner automaticky aktualizuje při každé změně konfiguračních souborů. V produkčním režimu se vygeneruje jen jednou a změny se nekontrolují. Vývojářský je tedy zaměřen na maximální pohodlí programátora, produkční na výkon a ostré nasazení. - -Volba režimu se provádí autodetekcí, takže obvykle není potřeba nic konfigurovat nebo ručně přepínat. Režim je vývojářský tehdy, pokud je aplikace spuštěna na localhostu (tj. IP adresa `127.0.0.1` nebo `::1`) a není přítomna proxy (tj. její HTTP hlavička). Jinak běží v produkčním režimu. - -Pokud chceme vývojářský režim povolit i v dalších případech, například programátorům přistupujícím z konkrétní IP adresy, použijeme `setDebugMode()`: - -```php -$configurator->setDebugMode('23.75.345.200'); -// lze zadat i pole IP adres -``` - -Rozhodně doporučujeme kombinovat IP adresu s cookie. Do cookie `nette-debug` uložíme tajný token, např. `secret1234`, a tímto způsobem aktivujeme vývojářský režim pro programátory přistupující z konkrétní IP adresy a zároveň mající v cookie zmíněný token: - -```php -$configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Vývojářský režim můžeme také vypnout úplně, i pro localhost: - -```php -$configurator->setDebugMode(false); -``` - - -Parametry ---------- - -V konfiguračních souborech můžete používat také parametry, které se definují [v sekci `parameters` |dependency-injection:configuration#Parametry]. - -Lze je také vkládat zvenčí pomocí metody `addDynamicParameters()`: - -```php -$configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Na parametr `remoteIp` se lze v konfiguraci odkázat zápisem `%remoteIp%`. - - -Pokud aktualizujete balíček na novější verzi, podívejte se na stránku [upgrade|upgrading]. - -{{maintitle: Nette Bootstrap: načtení konfigurace a sestavení kontejneru}} -{{description: Načtěte konfigurační soubory pomocí třídy Configurator a vytvořte z nich DI kontejner. Včetně vývojářského a produkčního režimu a parametrů.}} diff --git a/bootstrap/cs/@left-menu.texy b/bootstrap/cs/@left-menu.texy deleted file mode 100644 index 636e2ded56..0000000000 --- a/bootstrap/cs/@left-menu.texy +++ /dev/null @@ -1,13 +0,0 @@ -Nette Bootstrap -*************** -- [Úvod |@home] -- [Upgrade |upgrading] - - -Další četba -*********** -- [Dokumentace Nette |nette:] -- [Aplikace v Nette |application:how-it-works] -- [Utilities |utils:] -- [Návody a postupy |best-practices:] -- [Řešení problémů |nette:troubleshooting] diff --git a/bootstrap/cs/@meta.texy b/bootstrap/cs/@meta.texy deleted file mode 100644 index 462d9add80..0000000000 --- a/bootstrap/cs/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokumentace}} diff --git a/bootstrap/cs/upgrading.texy b/bootstrap/cs/upgrading.texy deleted file mode 100644 index 40c092fc58..0000000000 --- a/bootstrap/cs/upgrading.texy +++ /dev/null @@ -1,21 +0,0 @@ -Upgrade -******* - - -Upgrade na verzi 3.1 -==================== - -- třída `Nette\Configurator` byla přejmenována na `Nette\Bootstrap\Configurator` kvůli konzistenci se zbytkem frameworku -- přidána metoda `addStaticParameters()` jako alias pro `addParameters()` -- v parametrech předaných metodou `addStaticParameters()` nebo `addParameters()` se již neexpandují `%parametry%` - - -Upgrade na verzi 2.3 -==================== - -- odstraněny zastaralé konstanty `Configurator::DEVELOPMENT` a `PRODUCTION` -- `Configurator::setDebugMode()` přijímá pouze bool / string / array -- v konfiguračním souboru můžete všechny sekce umístěné pod `nette` posunout o úroveň výš; pokud posunete sekci `container`, `mailer` nebo `debugger`, přejmenujte ji na `di`, `mail` a `tracy` - -{{maintitle: Upgrade Nette Bootstrap: změny v jednotlivých verzích}} -{{description: Co se změnilo v Nette Bootstrap 3.1 a 2.3 a jak upravit kód: přejmenovaná třída Configurator, statické parametry a další.}} diff --git a/bootstrap/de/@home.texy b/bootstrap/de/@home.texy deleted file mode 100644 index 4ee3487d2a..0000000000 --- a/bootstrap/de/@home.texy +++ /dev/null @@ -1,99 +0,0 @@ -Nette Bootstrap -*************** - -.[perex] -Die einzelnen Komponenten von Nette werden über Konfigurationsdateien eingestellt. Wir zeigen Ihnen, wie sich diese Dateien laden lassen. - -.[tip] -Wenn Sie das gesamte Framework verwenden, brauchen Sie nichts weiter zu tun. Ihr Projekt hat ein vorbereitetes Verzeichnis `config/` für Konfigurationsdateien, und ihr Laden übernimmt der [Loader der Anwendung |application:bootstrapping#Konfiguration des DI-Containers]. Dieser Artikel richtet sich an Nutzer, die nur eine einzelne Nette-Bibliothek einsetzen und die Vorzüge der Konfigurationsdateien nutzen wollen. - -Konfigurationsdateien werden üblicherweise im [Format NEON|neon:format] geschrieben und lassen sich am besten in [Editoren bearbeiten, die es unterstützen |tools:ide]. Sie können als Anleitung verstanden werden, **wie Objekte erzeugt und eingestellt** werden. Das Ergebnis des Ladens einer Konfiguration ist deshalb eine sogenannte Factory, also ein Objekt, das bei Bedarf weitere Objekte erzeugt, die Sie verwenden wollen. Zum Beispiel eine Datenbankverbindung usw. - -Diese Factory wird auch *Dependency Injection Container* (DI-Container) genannt; wenn Sie sich für die Einzelheiten interessieren, lesen Sie das Kapitel über [Dependency Injection |dependency-injection:]. - -Das Laden der Konfiguration und das Erzeugen des Containers übernimmt die Klasse [api:Nette\Bootstrap\Configurator], deshalb installieren wir zuerst ihr Paket `nette/bootstrap`: - -```shell -composer require nette/bootstrap -``` - -Und erzeugen eine Instanz der Klasse `Configurator`. Da der erzeugte DI-Container auf der Festplatte zwischengespeichert wird, müssen Sie den Pfad zu dem Verzeichnis setzen, in dem er abgelegt wird: - -```php -$configurator = new Nette\Bootstrap\Configurator; -$configurator->setTempDirectory(__DIR__ . '/temp'); -``` - -Setzen Sie unter Linux oder macOS für das Verzeichnis `temp/` die [Schreibrechte |nette:troubleshooting#Einstellung der Verzeichnisberechtigungen]. - -Nun kommen wir zu den Konfigurationsdateien selbst. Wir laden sie mit `addConfig()`: - -```php -$configurator->addConfig(__DIR__ . '/database.neon'); -``` - -Wollen Sie mehrere Konfigurationsdateien hinzufügen, können Sie die Funktion `addConfig()` mehrfach aufrufen. Kommen in den Dateien Elemente mit denselben Schlüsseln vor, werden sie überschrieben (oder bei Arrays [zusammengeführt |dependency-injection:configuration#Zusammenführen]). Die später hinzugefügte Datei hat eine höhere Priorität als die vorherige. - -Der letzte Schritt ist das Erzeugen des DI-Containers: - -```php -$container = $configurator->createContainer(); -``` - -Und der erzeugt uns dann die gewünschten Objekte. Wenn Sie zum Beispiel die Konfiguration für [Nette Database|database:configuration] verwenden, können Sie ihn bitten, Datenbankverbindungen zu erzeugen: - -```php -$db = $container->getByType(Nette\Database\Connection::class); -// oder -$explorer = $container->getByType(Nette\Database\Explorer::class); -// oder beim Erzeugen mehrerer Verbindungen -$db = $container->getByName('database.main.connection'); -``` - -Und schon können Sie mit der Datenbank arbeiten! - - -Entwicklungs- vs. Produktionsmodus ----------------------------------- - -Im Entwicklungsmodus wird der Container automatisch aktualisiert, sobald sich die Konfigurationsdateien ändern. Im Produktionsmodus wird er nur einmal erzeugt, und Änderungen werden nicht geprüft. Der Entwicklungsmodus zielt also auf größtmöglichen Komfort für Programmierer, der Produktionsmodus auf Leistung und den Einsatz in der Produktion. - -Die Wahl des Modus erfolgt durch Autodetection, üblicherweise müssen Sie also nichts einstellen oder von Hand umschalten. Der Modus ist Entwicklung, wenn die Anwendung auf localhost läuft (also unter der IP-Adresse `127.0.0.1` oder `::1`) und kein Proxy vorhanden ist (also dessen HTTP-Header). Andernfalls läuft sie im Produktionsmodus. - -Wollen wir den Entwicklungsmodus auch in anderen Fällen einschalten, etwa für Programmierer, die von einer bestimmten IP-Adresse zugreifen, verwenden wir `setDebugMode()`: - -```php -$configurator->setDebugMode('23.75.345.200'); -// es lässt sich auch ein Array von IP-Adressen angeben -``` - -Wir empfehlen dringend, die IP-Adresse mit einem Cookie zu verbinden. Legen Sie im Cookie `nette-debug` ein geheimes Token ab, etwa `secret1234`. Auf diese Weise schalten Sie den Entwicklungsmodus für Programmierer ein, die von einer bestimmten IP-Adresse zugreifen und zugleich das genannte Token im Cookie haben: - -```php -$configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Wir können den Entwicklungsmodus auch vollständig abschalten, sogar für localhost: - -```php -$configurator->setDebugMode(false); -``` - - -Parameter ---------- - -In den Konfigurationsdateien können Sie auch Parameter verwenden, die [im Abschnitt `parameters` |dependency-injection:configuration#Parameter] definiert werden. - -Sie lassen sich auch von außen einfügen, und zwar mit der Methode `addDynamicParameters()`: - -```php -$configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Auf den Parameter `remoteIp` kann in der Konfiguration mit der Schreibweise `%remoteIp%` verwiesen werden. - - -Wenn Sie auf eine neuere Version aktualisieren, sehen Sie sich die Seite [Upgrade |upgrading] an. diff --git a/bootstrap/de/@left-menu.texy b/bootstrap/de/@left-menu.texy deleted file mode 100644 index 800f23ec5f..0000000000 --- a/bootstrap/de/@left-menu.texy +++ /dev/null @@ -1,13 +0,0 @@ -Nette Bootstrap -*************** -- [Übersicht |@home] -- [Upgrade|upgrading] - - -Weiterführende Lektüre -********************** -- [Nette Dokumentation |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Best Practices |best-practices:] -- [Fehlerbehebung |nette:troubleshooting] diff --git a/bootstrap/de/@meta.texy b/bootstrap/de/@meta.texy deleted file mode 100644 index b3b806b2ca..0000000000 --- a/bootstrap/de/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokumentation}} diff --git a/bootstrap/de/upgrading.texy b/bootstrap/de/upgrading.texy deleted file mode 100644 index 286f6b6e03..0000000000 --- a/bootstrap/de/upgrading.texy +++ /dev/null @@ -1,18 +0,0 @@ -Upgrade -******* - - -Upgrade auf Version 3.1 -======================= - -- die Klasse `Nette\Configurator` wurde aus Gründen der Einheitlichkeit mit dem übrigen Framework in `Nette\Bootstrap\Configurator` umbenannt -- die Methode `addStaticParameters()` wurde als Alias für `addParameters()` ergänzt -- über `addStaticParameters()` oder `addParameters()` übergebene Parameter expandieren `%parameters%` nicht mehr - - -Upgrade auf Version 2.3 -======================= - -- die veralteten Konstanten `Configurator::DEVELOPMENT` und `PRODUCTION` wurden entfernt -- `Configurator::setDebugMode()` akzeptiert nur bool / string / array -- in der Konfigurationsdatei können Sie alle unter `nette` platzierten Abschnitte eine Ebene höher verschieben; verschieben Sie den Abschnitt `container`, `mailer` oder `debugger`, benennen Sie ihn in `di`, `mail` bzw. `tracy` um diff --git a/bootstrap/en/@home.texy b/bootstrap/en/@home.texy deleted file mode 100644 index 7358e055f4..0000000000 --- a/bootstrap/en/@home.texy +++ /dev/null @@ -1,102 +0,0 @@ -Nette Bootstrap -*************** - -.[perex] -Individual Nette components are configured using configuration files. We will show how to load these files. - -.[tip] -If you are using the entire framework, there is no need to do anything else. Your project has a prepared `config/` directory for configuration files, and their loading is handled by the [application loader |application:bootstrapping#DI Container Configuration]. This article is for users who use only a single Nette library and want to take advantage of configuration files. - -Configuration files are usually written in [NEON format|neon:format] and are best edited in [editors that support it |tools:ide]. They can be thought of as instructions on how to **create and configure** objects. Thus, the result of loading a configuration will be a so-called factory, which is an object that creates on demand other objects you want to use. For example, a database connection, etc. - -This factory is also called a *dependency injection container* (DI container), and if you are interested in the details, read the chapter on [dependency injection |dependency-injection:]. - -Loading the configuration and creating the container is handled by the [api:Nette\Bootstrap\Configurator] class, so first, we install its `nette/bootstrap` package: - -```shell -composer require nette/bootstrap -``` - -And create an instance of the `Configurator` class. Since the generated DI container will be cached to disk, you need to set the path to the directory where it will be saved: - -```php -$configurator = new Nette\Bootstrap\Configurator; -$configurator->setTempDirectory(__DIR__ . '/temp'); -``` - -On Linux or macOS, set [write permissions |nette:troubleshooting#Setting Directory Permissions] for the `temp/` directory. - -Now we get to the configuration files themselves. We load them using `addConfig()`: - -```php -$configurator->addConfig(__DIR__ . '/database.neon'); -``` - -If you want to add more configuration files, you can call the `addConfig()` function multiple times. If elements with the same keys appear in the files, they will be overwritten (or [merged |dependency-injection:configuration#Merging] in the case of arrays). The file added later has a higher priority than the previous one. - -The final step is to create the DI container: - -```php -$container = $configurator->createContainer(); -``` - -And this will create the desired objects for us. For example, if you are using the configuration for [Nette Database|database:configuration], you can ask it to create database connections: - -```php -$db = $container->getByType(Nette\Database\Connection::class); -// or -$explorer = $container->getByType(Nette\Database\Explorer::class); -// or when creating multiple connections -$db = $container->getByName('database.main.connection'); -``` - -And now you can work with the database! - - -Development vs Production Mode ------------------------------- - -In development mode, the container is automatically updated whenever the configuration files are changed. In production mode, it is generated only once, and changes are not checked. Thus, development mode is aimed at maximum programmer convenience, while production mode focuses on performance and production deployment. - -Mode selection is done by autodetection, so there is usually no need to configure or manually switch anything. The mode is development if the application is running on localhost (i.e., IP address `127.0.0.1` or `::1`) and no proxy (i.e., its HTTP header) is present. Otherwise, it runs in production mode. - -If we want to enable development mode in other cases, for example, for programmers accessing from a specific IP address, use `setDebugMode()`: - -```php -$configurator->setDebugMode('23.75.345.200'); -// an array of IP addresses can also be specified -``` - -We strongly recommend combining the IP address with a cookie. Store a secret token, e.g., `secret1234`, in the `nette-debug` cookie. This way, you enable development mode for programmers accessing from a specific IP address who also have the mentioned token in their cookie: - -```php -$configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -We can also disable development mode completely, even for localhost: - -```php -$configurator->setDebugMode(false); -``` - - -Parameters ----------- - -You can also use parameters in configuration files, which are defined [in the `parameters` section |dependency-injection:configuration#Parameters]. - -They can also be inserted from the outside using the `addDynamicParameters()` method: - -```php -$configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -The `remoteIp` parameter can be referenced in the configuration using the `%remoteIp%` notation. - - -If you are upgrading to a newer version, see the [upgrading] page. - -{{maintitle: Nette Bootstrap: Load NEON Config and Build DI Container}} -{{description: Load configuration files with the Configurator class and create a DI container from them. Also covers development and production mode and parameters.}} diff --git a/bootstrap/en/@left-menu.texy b/bootstrap/en/@left-menu.texy deleted file mode 100644 index fa06cbfede..0000000000 --- a/bootstrap/en/@left-menu.texy +++ /dev/null @@ -1,13 +0,0 @@ -Nette Bootstrap -*************** -- [Overview |@home] -- [Upgrading] - - -Further Reading -*************** -- [Nette Documentation |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Best practices |best-practices:] -- [Troubleshooting |nette:troubleshooting] diff --git a/bootstrap/en/@meta.texy b/bootstrap/en/@meta.texy deleted file mode 100644 index 42471908b0..0000000000 --- a/bootstrap/en/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Documentation}} diff --git a/bootstrap/en/upgrading.texy b/bootstrap/en/upgrading.texy deleted file mode 100644 index 9afc283762..0000000000 --- a/bootstrap/en/upgrading.texy +++ /dev/null @@ -1,21 +0,0 @@ -Upgrading -********* - - -Upgrading to Version 3.1 -======================== - -- the class `Nette\Configurator` was renamed to `Nette\Bootstrap\Configurator` for consistency with the rest of the framework -- the method `addStaticParameters()` was added as an alias for `addParameters()` -- parameters passed via `addStaticParameters()` or `addParameters()` no longer expand `%parameters%` - - -Upgrading to Version 2.3 -======================== - -- the deprecated constants `Configurator::DEVELOPMENT` and `PRODUCTION` were removed -- `Configurator::setDebugMode()` accepts only bool / string / array -- in the config file you can move all sections placed under `nette` one level up; if you move up the `container`, `mailer` or `debugger` section, rename it to `di`, `mail` and `tracy` - -{{maintitle: Upgrading Nette Bootstrap: What Changed in Each Version}} -{{description: What changed in Nette Bootstrap 3.1 and 2.3 and how to update your code: the renamed Configurator class, static parameters and more.}} diff --git a/bootstrap/es/@home.texy b/bootstrap/es/@home.texy deleted file mode 100644 index bd94f66c99..0000000000 --- a/bootstrap/es/@home.texy +++ /dev/null @@ -1,99 +0,0 @@ -Nette Bootstrap -*************** - -.[perex] -Los distintos componentes de Nette se configuran con archivos de configuración. Le mostraremos cómo cargar esos archivos. - -.[tip] -Si usa todo el framework, no hace falta hacer nada más. Su proyecto tiene un directorio `config/` preparado para los archivos de configuración, y de cargarlos se encarga el [cargador de la aplicación |application:bootstrapping#Configuración del contenedor DI]. Este artículo es para los usuarios que usan solo una biblioteca de Nette y quieren aprovechar los archivos de configuración. - -Los archivos de configuración se escriben normalmente en [formato NEON|neon:format] y se editan mejor en [editores que lo soportan |tools:ide]. Se pueden entender como instrucciones sobre cómo **crear y configurar** objetos. Así, el resultado de cargar una configuración será una llamada factory, que es un objeto que crea bajo demanda otros objetos que usted quiere usar. Por ejemplo, una conexión a la base de datos, etc. - -Esa factory se llama también *dependency injection container* (contenedor DI) y, si le interesan los detalles, lea el capítulo sobre [dependency injection |dependency-injection:]. - -De cargar la configuración y crear el contenedor se encarga la clase [api:Nette\Bootstrap\Configurator], así que primero instalamos su paquete `nette/bootstrap`: - -```shell -composer require nette/bootstrap -``` - -Y creamos una instancia de la clase `Configurator`. Como el contenedor DI generado se cacheará en disco, hay que establecer la ruta al directorio donde se guardará: - -```php -$configurator = new Nette\Bootstrap\Configurator; -$configurator->setTempDirectory(__DIR__ . '/temp'); -``` - -En Linux o macOS, establezca los [permisos de escritura |nette:troubleshooting#Establecer los permisos de los directorios] del directorio `temp/`. - -Ahora llegamos a los archivos de configuración en sí. Los cargamos con `addConfig()`: - -```php -$configurator->addConfig(__DIR__ . '/database.neon'); -``` - -Si quiere añadir más archivos de configuración, puede llamar a la función `addConfig()` varias veces. Si en los archivos aparecen elementos con las mismas claves, se sobrescribirán (o se [fusionarán |dependency-injection:configuration#Fusión] en el caso de los arrays). El archivo añadido después tiene mayor prioridad que el anterior. - -El último paso es crear el contenedor DI: - -```php -$container = $configurator->createContainer(); -``` - -Y eso nos creará los objetos deseados. Por ejemplo, si usa la configuración de [Nette Database|database:configuration], puede pedirle que cree las conexiones a la base de datos: - -```php -$db = $container->getByType(Nette\Database\Connection::class); -// o -$explorer = $container->getByType(Nette\Database\Explorer::class); -// o, al crear varias conexiones -$db = $container->getByName('database.main.connection'); -``` - -¡Y ya puede trabajar con la base de datos! - - -Modo de desarrollo frente a modo de producción ----------------------------------------------- - -En modo de desarrollo, el contenedor se actualiza automáticamente siempre que cambian los archivos de configuración. En modo de producción se genera una sola vez y no se comprueban los cambios. Así, el modo de desarrollo busca la máxima comodidad del programador, mientras que el modo de producción se centra en el rendimiento y en el despliegue en producción. - -El modo se elige por autodetección, así que normalmente no hace falta configurar ni cambiar nada a mano. El modo es de desarrollo si la aplicación corre en localhost (es decir, la dirección IP `127.0.0.1` o `::1`) y no hay ningún proxy (es decir, su cabecera HTTP). En los demás casos corre en modo de producción. - -Si queremos activar el modo de desarrollo en otros casos, por ejemplo para los programadores que acceden desde una dirección IP concreta, use `setDebugMode()`: - -```php -$configurator->setDebugMode('23.75.345.200'); -// también se puede indicar un array de direcciones IP -``` - -Recomendamos encarecidamente combinar la dirección IP con una cookie. Guarde un token secreto, p. ej. `secret1234`, en la cookie `nette-debug`. Así activa el modo de desarrollo para los programadores que acceden desde una dirección IP concreta y que además tienen ese token en la cookie: - -```php -$configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -También podemos desactivar por completo el modo de desarrollo, incluso para localhost: - -```php -$configurator->setDebugMode(false); -``` - - -Parámetros ----------- - -En los archivos de configuración también puede usar parámetros, que se definen [en la sección `parameters` |dependency-injection:configuration#Parámetros]. - -También se pueden insertar desde fuera con el método `addDynamicParameters()`: - -```php -$configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Al parámetro `remoteIp` se puede hacer referencia en la configuración con la notación `%remoteIp%`. - - -Si está actualizando a una versión más reciente, vea la página de [actualización |upgrading]. diff --git a/bootstrap/es/@left-menu.texy b/bootstrap/es/@left-menu.texy deleted file mode 100644 index cbd167179a..0000000000 --- a/bootstrap/es/@left-menu.texy +++ /dev/null @@ -1,13 +0,0 @@ -Nette Bootstrap -*************** -- [Introducción |@home] -- [Actualización|upgrading] - - -Lecturas adicionales -******************** -- [Documentación de Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Buenas prácticas |best-practices:] -- [Solución de problemas |nette:troubleshooting] diff --git a/bootstrap/es/@meta.texy b/bootstrap/es/@meta.texy deleted file mode 100644 index 3798d9cda4..0000000000 --- a/bootstrap/es/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentación de Nette}} diff --git a/bootstrap/es/upgrading.texy b/bootstrap/es/upgrading.texy deleted file mode 100644 index ea7ece7df8..0000000000 --- a/bootstrap/es/upgrading.texy +++ /dev/null @@ -1,18 +0,0 @@ -Actualización -************* - - -Actualización a la versión 3.1 -============================== - -- la clase `Nette\Configurator` se renombró a `Nette\Bootstrap\Configurator` por coherencia con el resto del framework -- se añadió el método `addStaticParameters()` como alias de `addParameters()` -- los parámetros pasados con `addStaticParameters()` o `addParameters()` ya no expanden `%parameters%` - - -Actualización a la versión 2.3 -============================== - -- se eliminaron las constantes obsoletas `Configurator::DEVELOPMENT` y `PRODUCTION` -- `Configurator::setDebugMode()` acepta solo bool / string / array -- en el archivo de configuración puede subir un nivel todas las secciones colocadas bajo `nette`; si sube la sección `container`, `mailer` o `debugger`, renómbrela a `di`, `mail` y `tracy` diff --git a/bootstrap/fr/@home.texy b/bootstrap/fr/@home.texy deleted file mode 100644 index b83da75f53..0000000000 --- a/bootstrap/fr/@home.texy +++ /dev/null @@ -1,99 +0,0 @@ -Nette Bootstrap -*************** - -.[perex] -Les différents composants de Nette se règlent à l'aide de fichiers de configuration. Nous allons montrer comment charger ces fichiers. - -.[tip] -Si vous utilisez tout le framework, vous n'avez rien d'autre à faire. Votre projet dispose d'un répertoire `config/` prêt pour les fichiers de configuration, et leur chargement est assuré par le [chargeur de l'application |application:bootstrapping#Configuration du Conteneur DI]. Cet article s'adresse à ceux qui n'utilisent qu'une seule bibliothèque de Nette et veulent profiter des fichiers de configuration. - -Les fichiers de configuration s'écrivent d'ordinaire au [format NEON|neon:format] et s'éditent le mieux dans des [éditeurs qui le prennent en charge |tools:ide]. On peut les voir comme un mode d'emploi indiquant comment **créer et configurer** des objets. Le résultat du chargement d'une configuration est donc ce qu'on appelle une fabrique, un objet qui crée à la demande les autres objets que vous voulez utiliser. Une connexion à la base de données, par exemple. - -Cette fabrique est aussi appelée *conteneur d'injection de dépendances* (conteneur DI) et, si les détails vous intéressent, lisez le chapitre sur l'[injection de dépendances |dependency-injection:]. - -Le chargement de la configuration et la création du conteneur sont assurés par la classe [api:Nette\Bootstrap\Configurator] ; nous installons donc d'abord son paquet `nette/bootstrap` : - -```shell -composer require nette/bootstrap -``` - -Puis nous créons une instance de la classe `Configurator`. Comme le conteneur DI généré sera mis en cache sur le disque, il faut indiquer le chemin du répertoire où l'enregistrer : - -```php -$configurator = new Nette\Bootstrap\Configurator; -$configurator->setTempDirectory(__DIR__ . '/temp'); -``` - -Sous Linux ou macOS, donnez les [droits d'écriture |nette:troubleshooting#Régler les permissions des répertoires] au répertoire `temp/`. - -Venons-en aux fichiers de configuration eux-mêmes. Nous les chargeons avec `addConfig()` : - -```php -$configurator->addConfig(__DIR__ . '/database.neon'); -``` - -Si vous voulez ajouter plusieurs fichiers de configuration, vous pouvez appeler la fonction `addConfig()` plusieurs fois. Si des éléments de même clé apparaissent dans plusieurs fichiers, ils sont écrasés (ou [fusionnés |dependency-injection:configuration#Fusion] dans le cas des tableaux). Le fichier ajouté en dernier a la priorité sur le précédent. - -La dernière étape est la création du conteneur DI : - -```php -$container = $configurator->createContainer(); -``` - -Et celui-ci créera pour nous les objets voulus. Si vous utilisez par exemple la configuration de [Nette Database|database:configuration], vous pouvez lui demander de créer les connexions à la base : - -```php -$db = $container->getByType(Nette\Database\Connection::class); -// or -$explorer = $container->getByType(Nette\Database\Explorer::class); -// or when creating multiple connections -$db = $container->getByName('database.main.connection'); -``` - -Et vous voilà prêt à travailler avec la base de données ! - - -Mode développement et mode production -------------------------------------- - -En mode développement, le conteneur est automatiquement mis à jour dès que les fichiers de configuration changent. En mode production, il n'est généré qu'une fois et les changements ne sont pas vérifiés. Le mode développement vise donc le confort maximal du programmeur, tandis que le mode production vise la performance et le déploiement. - -Le choix du mode se fait par détection automatique : il n'y a en général rien à configurer ni à basculer à la main. Le mode est celui du développement si l'application tourne sur localhost (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 tourne en mode production. - -Si nous voulons activer le mode développement dans d'autres cas, par exemple pour les programmeurs qui se connectent depuis une adresse IP donnée, utilisez `setDebugMode()` : - -```php -$configurator->setDebugMode('23.75.345.200'); -// an array of IP addresses can also be specified -``` - -Nous recommandons vivement de combiner l'adresse IP avec un cookie. Stockez un jeton secret, par ex. `secret1234`, dans le cookie `nette-debug`. Vous activez ainsi le mode développement pour les programmeurs qui se connectent depuis une adresse IP donnée et qui ont en plus ce jeton dans leur cookie : - -```php -$configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Nous pouvons aussi désactiver complètement le mode développement, y compris sur localhost : - -```php -$configurator->setDebugMode(false); -``` - - -Paramètres ----------- - -Vous pouvez aussi utiliser dans les fichiers de configuration des paramètres, définis [dans la section `parameters` |dependency-injection:configuration#Paramètres]. - -Ils peuvent également être injectés de l'extérieur par la méthode `addDynamicParameters()` : - -```php -$configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Le paramètre `remoteIp` se référence dans la configuration par l'écriture `%remoteIp%`. - - -Si vous passez à une version plus récente, consultez la page [mise à niveau |upgrading]. diff --git a/bootstrap/fr/@left-menu.texy b/bootstrap/fr/@left-menu.texy deleted file mode 100644 index 610ccd39e0..0000000000 --- a/bootstrap/fr/@left-menu.texy +++ /dev/null @@ -1,13 +0,0 @@ -Nette Bootstrap -*************** -- [Introduction |@home] -- [Mise à niveau|upgrading] - - -Pour aller plus loin -******************** -- [Documentation Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Bonnes pratiques |best-practices:] -- [Résolution de problèmes |nette:troubleshooting] diff --git a/bootstrap/fr/@meta.texy b/bootstrap/fr/@meta.texy deleted file mode 100644 index 72ae4b8db8..0000000000 --- a/bootstrap/fr/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentation Nette}} diff --git a/bootstrap/fr/upgrading.texy b/bootstrap/fr/upgrading.texy deleted file mode 100644 index 281e99fa41..0000000000 --- a/bootstrap/fr/upgrading.texy +++ /dev/null @@ -1,18 +0,0 @@ -Mise à niveau -************* - - -Passage à la version 3.1 -======================== - -- la classe `Nette\Configurator` a été renommée en `Nette\Bootstrap\Configurator`, par cohérence avec le reste du framework -- la méthode `addStaticParameters()` a été ajoutée comme alias d'`addParameters()` -- les paramètres passés par `addStaticParameters()` ou `addParameters()` ne développent plus les `%parameters%` - - -Passage à la version 2.3 -======================== - -- les constantes dépréciées `Configurator::DEVELOPMENT` et `PRODUCTION` ont été supprimées -- `Configurator::setDebugMode()` n'accepte que bool / string / array -- dans le fichier de configuration, vous pouvez remonter d'un niveau toutes les sections placées sous `nette` ; si vous remontez les sections `container`, `mailer` ou `debugger`, renommez-les en `di`, `mail` et `tracy` diff --git a/bootstrap/it/@home.texy b/bootstrap/it/@home.texy deleted file mode 100644 index 15ff03669c..0000000000 --- a/bootstrap/it/@home.texy +++ /dev/null @@ -1,99 +0,0 @@ -Nette Bootstrap -*************** - -.[perex] -I singoli componenti di Nette si configurano con i file di configurazione. Mostreremo come caricare questi file. - -.[tip] -Se usate tutto il framework, non serve fare altro. Il vostro progetto ha già pronta la directory `config/` per i file di configurazione e del loro caricamento si occupa il [loader dell'applicazione |application:bootstrapping#Configurazione del container DI]. Questo articolo è per chi usa una sola libreria di Nette e vuole sfruttare i file di configurazione. - -I file di configurazione si scrivono di solito nel [formato NEON|neon:format] e si modificano al meglio negli [editor che lo supportano |tools:ide]. Si possono considerare istruzioni su come **creare e configurare** gli oggetti. Il risultato del caricamento di una configurazione sarà quindi una cosiddetta factory, cioè un oggetto che crea su richiesta gli altri oggetti che volete usare. Per esempio una connessione al database ecc. - -Questa factory si chiama anche *dependency injection container* (container DI) e, se vi interessano i dettagli, leggete il capitolo sulla [dependency injection |dependency-injection:]. - -Del caricamento della configurazione e della creazione del container si occupa la classe [api:Nette\Bootstrap\Configurator], quindi per prima cosa installiamo il suo pacchetto `nette/bootstrap`: - -```shell -composer require nette/bootstrap -``` - -E creiamo un'istanza della classe `Configurator`. Poiché il container DI generato verrà messo in cache su disco, dovete impostare il percorso della directory in cui verrà salvato: - -```php -$configurator = new Nette\Bootstrap\Configurator; -$configurator->setTempDirectory(__DIR__ . '/temp'); -``` - -Su Linux o macOS impostate i [permessi di scrittura |nette:troubleshooting#Impostazione dei permessi delle directory] per la directory `temp/`. - -Ora arriviamo ai file di configurazione veri e propri. Li carichiamo con `addConfig()`: - -```php -$configurator->addConfig(__DIR__ . '/database.neon'); -``` - -Se volete aggiungere altri file di configurazione, potete chiamare la funzione `addConfig()` più volte. Se nei file compaiono elementi con le stesse chiavi, verranno sovrascritti (oppure [uniti |dependency-injection:configuration#Unione] nel caso degli array). Il file aggiunto dopo ha la priorità su quello precedente. - -L'ultimo passo è creare il container DI: - -```php -$container = $configurator->createContainer(); -``` - -E questo creerà per noi gli oggetti desiderati. Se per esempio usate la configurazione di [Nette Database|database:configuration], potete chiedergli di creare le connessioni al database: - -```php -$db = $container->getByType(Nette\Database\Connection::class); -// oppure -$explorer = $container->getByType(Nette\Database\Explorer::class); -// oppure quando si creano più connessioni -$db = $container->getByName('database.main.connection'); -``` - -E ora potete lavorare con il database! - - -Modalità di sviluppo e modalità produzione ------------------------------------------- - -In modalità di sviluppo il container viene aggiornato automaticamente a ogni modifica dei file di configurazione. In modalità produzione viene generato una sola volta e le modifiche non vengono controllate. La modalità di sviluppo punta quindi alla massima comodità del programmatore, mentre quella di produzione si concentra sulle prestazioni e sul deploy in produzione. - -La scelta della modalità avviene per rilevamento automatico, quindi di solito non serve configurare o cambiare nulla a mano. La modalità è di sviluppo se l'applicazione gira su localhost (cioè con indirizzo IP `127.0.0.1` oppure `::1`) e non è presente un proxy (cioè il suo header HTTP). Altrimenti gira in modalità produzione. - -Se vogliamo attivare la modalità di sviluppo anche in altri casi, per esempio per i programmatori che accedono da un indirizzo IP determinato, usate `setDebugMode()`: - -```php -$configurator->setDebugMode('23.75.345.200'); -// si può indicare anche un array di indirizzi IP -``` - -Consigliamo vivamente di combinare l'indirizzo IP con un cookie. Salvate nel cookie `nette-debug` un token segreto, per esempio `secret1234`. In questo modo attivate la modalità di sviluppo per i programmatori che accedono da un determinato indirizzo IP e hanno nel cookie il token indicato: - -```php -$configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Possiamo anche disattivare del tutto la modalità di sviluppo, perfino per localhost: - -```php -$configurator->setDebugMode(false); -``` - - -Parametri ---------- - -Nei file di configurazione potete usare anche i parametri, che si definiscono [nella sezione `parameters` |dependency-injection:configuration#Parametri]. - -Si possono anche inserire dall'esterno con il metodo `addDynamicParameters()`: - -```php -$configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Al parametro `remoteIp` si può fare riferimento nella configurazione con la notazione `%remoteIp%`. - - -Se state aggiornando a una versione più recente, guardate la pagina [aggiornamento |upgrading]. diff --git a/bootstrap/it/@left-menu.texy b/bootstrap/it/@left-menu.texy deleted file mode 100644 index 24db87db11..0000000000 --- a/bootstrap/it/@left-menu.texy +++ /dev/null @@ -1,13 +0,0 @@ -Nette Bootstrap -*************** -- [Panoramica |@home] -- [Aggiornamento|upgrading] - - -Letture consigliate -******************* -- [Documentazione di Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Best practice |best-practices:] -- [Risoluzione dei problemi |nette:troubleshooting] diff --git a/bootstrap/it/@meta.texy b/bootstrap/it/@meta.texy deleted file mode 100644 index 4647d0c8a2..0000000000 --- a/bootstrap/it/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentazione Nette}} diff --git a/bootstrap/it/upgrading.texy b/bootstrap/it/upgrading.texy deleted file mode 100644 index fb280b0119..0000000000 --- a/bootstrap/it/upgrading.texy +++ /dev/null @@ -1,18 +0,0 @@ -Aggiornamento -************* - - -Aggiornamento alla versione 3.1 -=============================== - -- la classe `Nette\Configurator` è stata rinominata in `Nette\Bootstrap\Configurator` per coerenza con il resto del framework -- è stato aggiunto il metodo `addStaticParameters()` come alias di `addParameters()` -- i parametri passati con `addStaticParameters()` oppure `addParameters()` non espandono più `%parameters%` - - -Aggiornamento alla versione 2.3 -=============================== - -- le costanti deprecate `Configurator::DEVELOPMENT` e `PRODUCTION` sono state rimosse -- `Configurator::setDebugMode()` accetta solo bool / string / array -- nel file di configurazione potete spostare di un livello in su tutte le sezioni collocate sotto `nette`; se spostate in su la sezione `container`, `mailer` oppure `debugger`, rinominatela rispettivamente in `di`, `mail` e `tracy` diff --git a/bootstrap/ja/@home.texy b/bootstrap/ja/@home.texy deleted file mode 100644 index c7cea8d524..0000000000 --- a/bootstrap/ja/@home.texy +++ /dev/null @@ -1,99 +0,0 @@ -Nette Bootstrap -*************** - -.[perex] -Nette の個々のコンポーネントは設定ファイルで設定します。ここではそのファイルの読み込み方をお見せします。 - -.[tip] -フレームワーク全体を使っているなら、ほかに何もする必要はありません。あなたのプロジェクトには設定ファイル用の `config/` のディレクトリが用意されていて、その読み込みは[アプリケーションの立ち上げ役 |application:bootstrapping#DI コンテナの設定]が受け持ちます。この記事は、Nette のライブラリをひとつだけ使っていて、設定ファイルを活かしたい人のためのものです。 - -設定ファイルはふつう [NEON 形式|neon:format]で書かれ、[それに対応したエディタ |tools:ide]で編集するのがいちばんです。設定ファイルは、オブジェクトを**どう作ってどう整えるか**の指示だと考えられます。ですから設定を読み込んだ結果はいわゆるファクトリになります。これは、あなたが使いたいほかのオブジェクトを求めに応じて作るオブジェクトです。たとえばデータベース接続などです。 - -このファクトリは *dependency injection コンテナ*(DI コンテナ)とも呼ばれます。詳しいことに関心があるなら、[dependency injection |dependency-injection:]の章をご覧ください。 - -設定の読み込みとコンテナの生成は [api:Nette\Bootstrap\Configurator]クラスが受け持つので、まずその `nette/bootstrap` のパッケージを入れます。 - -```shell -composer require nette/bootstrap -``` - -そして `Configurator` クラスのインスタンスを作ります。作られる DI コンテナはディスクに蓄えられるので、それを保存するディレクトリへのパスを設定する必要があります。 - -```php -$configurator = new Nette\Bootstrap\Configurator; -$configurator->setTempDirectory(__DIR__ . '/temp'); -``` - -Linux や macOS では、`temp/` のディレクトリに[書き込みの権限 |nette:troubleshooting#ディレクトリの権限の設定]を与えてください。 - -ではいよいよ設定ファイルそのものです。`addConfig()` で読み込みます。 - -```php -$configurator->addConfig(__DIR__ . '/database.neon'); -``` - -設定ファイルをもっと足したいなら、`addConfig()` の関数を何度でも呼べます。ファイルに同じキーの要素が現れると、それは上書きされます(配列の場合は[併合されます |dependency-injection:configuration#統合])。あとから足したファイルのほうが、前のものより優先されます。 - -最後の一歩は DI コンテナを作ることです。 - -```php -$container = $configurator->createContainer(); -``` - -これで望むオブジェクトが作られます。たとえば [Nette Database|database:configuration]の設定を使っているなら、データベース接続を作ってもらえます。 - -```php -$db = $container->getByType(Nette\Database\Connection::class); -// または -$explorer = $container->getByType(Nette\Database\Explorer::class); -// あるいは複数の接続を作る場合 -$db = $container->getByName('database.main.connection'); -``` - -これでデータベースを扱えます。 - - -開発モードと本番モード ------------ - -開発モードでは、設定ファイルが変わるたびにコンテナが自動的に更新されます。本番モードでは一度だけ作られ、変更は確かめられません。ですから開発モードはプログラマーの心地よさを最大にすることを、本番モードは性能と本番への配置を目指しています。 - -モードの選択は自動の判別で行われるので、ふつうは何かを設定したり手で切り替えたりする必要はありません。アプリケーションが localhost(つまり IP アドレス `127.0.0.1` または `::1`)で動いていて、プロキシ(つまりその HTTP ヘッダー)がないときが開発モードです。そうでなければ本番モードで動きます。 - -たとえば特定の IP アドレスからアクセスするプログラマーのように、ほかの場合にも開発モードを有効にしたいなら、`setDebugMode()` を使います。 - -```php -$configurator->setDebugMode('23.75.345.200'); -// IP アドレスの配列も指定できます -``` - -IP アドレスとクッキーを組み合わせることを強くおすすめします。`nette-debug` のクッキーに秘密のトークン、たとえば `secret1234` を入れておきます。こうすれば、そのトークンをクッキーに持つ、特定の IP アドレスからアクセスするプログラマーにだけ開発モードを有効にできます。 - -```php -$configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -localhost も含めて、開発モードを完全に切ることもできます。 - -```php -$configurator->setDebugMode(false); -``` - - -パラメータ ------ - -設定ファイルではパラメータも使えます。それは [`parameters` の区画 |dependency-injection:configuration#パラメータ]で定めます。 - -`addDynamicParameters()` メソッドで外から入れることもできます。 - -```php -$configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -`remoteIp` のパラメータは、設定の中で `%remoteIp%` の書き方で参照できます。 - - -新しい版へ上げるなら、[アップグレード |upgrading]のページをご覧ください。 diff --git a/bootstrap/ja/@left-menu.texy b/bootstrap/ja/@left-menu.texy deleted file mode 100644 index a14c57e9d4..0000000000 --- a/bootstrap/ja/@left-menu.texy +++ /dev/null @@ -1,13 +0,0 @@ -Nette Bootstrap -*************** -- [概要 |@home] -- [アップグレード|upgrading] - - -関連情報 -**** -- [Nette ドキュメント |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [ベストプラクティス |best-practices:] -- [トラブルシューティング |nette:troubleshooting] diff --git a/bootstrap/ja/@meta.texy b/bootstrap/ja/@meta.texy deleted file mode 100644 index 43b85f3cac..0000000000 --- a/bootstrap/ja/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette ドキュメント}} diff --git a/bootstrap/ja/upgrading.texy b/bootstrap/ja/upgrading.texy deleted file mode 100644 index 7d422f5d3a..0000000000 --- a/bootstrap/ja/upgrading.texy +++ /dev/null @@ -1,18 +0,0 @@ -アップグレード -******* - - -バージョン 3.1 へのアップグレード -=================== - -- クラス `Nette\Configurator` は、フレームワークのほかの部分と揃えるために `Nette\Bootstrap\Configurator` に改名されました -- `addStaticParameters()` メソッドが `addParameters()` の別名として足されました -- `addStaticParameters()` や `addParameters()` で渡されたパラメータは、もう `%parameters%` を展開しません - - -バージョン 2.3 へのアップグレード -=================== - -- 非推奨だった定数 `Configurator::DEVELOPMENT` と `PRODUCTION` は取り除かれました -- `Configurator::setDebugMode()` は bool / string / array だけを受け取ります -- 設定ファイルでは、`nette` の下に置かれたすべての区画をひとつ上の階層へ移せます。`container`、`mailer`、`debugger` の区画を上へ移すなら、それぞれ `di`、`mail`、`tracy` に名前を変えてください diff --git a/bootstrap/meta.json b/bootstrap/meta.json deleted file mode 100644 index 2ccc9134fc..0000000000 --- a/bootstrap/meta.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": "3.x", - "repo": "nette/bootstrap", - "composer": "nette/bootstrap", - "api": "https://api.nette.org/bootstrap/" -} diff --git a/bootstrap/pl/@home.texy b/bootstrap/pl/@home.texy deleted file mode 100644 index 4f20b60939..0000000000 --- a/bootstrap/pl/@home.texy +++ /dev/null @@ -1,99 +0,0 @@ -Nette Bootstrap -*************** - -.[perex] -Poszczególne komponenty Nette konfiguruje się za pomocą plików konfiguracyjnych. Pokażemy, jak te pliki wczytać. - -.[tip] -Jeśli używasz całego frameworku, nie musisz robić nic więcej. Twój projekt ma przygotowany katalog `config/` na pliki konfiguracyjne, a ich wczytywaniem zajmuje się [loader aplikacji |application:bootstrapping#Konfiguracja kontenera DI]. Ten artykuł jest dla użytkowników używających tylko jednej biblioteki Nette, którzy chcą skorzystać z plików konfiguracyjnych. - -Pliki konfiguracyjne pisze się zwykle w [formacie NEON|neon:format], a najlepiej edytuje w [edytorach, które go wspierają |tools:ide]. Można myśleć o nich jak o instrukcjach, jak **tworzyć i konfigurować** obiekty. Wynikiem wczytania konfiguracji będzie więc tak zwana fabryka, czyli obiekt tworzący na żądanie inne obiekty, których chcesz używać. Na przykład połączenie z bazą danych itd. - -Fabrykę tę nazywa się też *kontenerem dependency injection* (kontenerem DI), a jeśli interesują Cię szczegóły, przeczytaj rozdział o [dependency injection |dependency-injection:]. - -Wczytaniem konfiguracji i utworzeniem kontenera zajmuje się klasa [api:Nette\Bootstrap\Configurator], więc najpierw zainstalujemy jej pakiet `nette/bootstrap`: - -```shell -composer require nette/bootstrap -``` - -I utworzymy instancję klasy `Configurator`. Ponieważ wygenerowany kontener DI będzie buforowany na dysku, musisz ustawić ścieżkę do katalogu, w którym zostanie zapisany: - -```php -$configurator = new Nette\Bootstrap\Configurator; -$configurator->setTempDirectory(__DIR__ . '/temp'); -``` - -W Linuksie albo macOS ustaw katalogowi `temp/` [uprawnienia do zapisu |nette:troubleshooting#Ustawienie uprawnień do katalogów]. - -Teraz przechodzimy do samych plików konfiguracyjnych. Wczytujemy je za pomocą `addConfig()`: - -```php -$configurator->addConfig(__DIR__ . '/database.neon'); -``` - -Jeśli chcesz dodać więcej plików konfiguracyjnych, możesz wywołać funkcję `addConfig()` wielokrotnie. Jeśli w plikach pojawią się elementy o tych samych kluczach, zostaną nadpisane (albo [scalone |dependency-injection:configuration#Scalanie] w przypadku tablic). Plik dodany później ma wyższy priorytet niż poprzedni. - -Ostatnim krokiem jest utworzenie kontenera DI: - -```php -$container = $configurator->createContainer(); -``` - -I to utworzy nam pożądane obiekty. Jeśli na przykład używasz konfiguracji dla [Nette Database|database:configuration], możesz poprosić go o utworzenie połączeń z bazą danych: - -```php -$db = $container->getByType(Nette\Database\Connection::class); -// albo -$explorer = $container->getByType(Nette\Database\Explorer::class); -// albo przy tworzeniu wielu połączeń -$db = $container->getByName('database.main.connection'); -``` - -I teraz możesz pracować z bazą danych! - - -Tryb deweloperski a produkcyjny -------------------------------- - -W trybie deweloperskim kontener aktualizowany jest automatycznie zawsze, gdy zmienią się pliki konfiguracyjne. W trybie produkcyjnym generowany jest tylko raz, a zmiany nie są sprawdzane. Tryb deweloperski nastawiony jest więc na maksymalną wygodę programisty, a tryb produkcyjny skupia się na wydajności i wdrożeniu produkcyjnym. - -Wybór trybu odbywa się przez autodetekcję, więc zwykle nie trzeba niczego konfigurować ani ręcznie przełączać. Tryb jest deweloperski, jeśli aplikacja działa na localhoście (czyli pod adresem IP `127.0.0.1` albo `::1`) i nie ma proxy (czyli jego nagłówka HTTP). W przeciwnym razie działa w trybie produkcyjnym. - -Jeśli chcemy włączyć tryb deweloperski także w innych przypadkach, na przykład dla programistów łączących się z konkretnego adresu IP, użyj `setDebugMode()`: - -```php -$configurator->setDebugMode('23.75.345.200'); -// można podać też tablicę adresów IP -``` - -Zdecydowanie zalecamy łączenie adresu IP z cookie. Zapisz do cookie `nette-debug` tajny token, np. `secret1234`. W ten sposób włączysz tryb deweloperski dla programistów łączących się z konkretnego adresu IP, którzy mają w cookie wspomniany token: - -```php -$configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Tryb deweloperski możemy też całkowicie wyłączyć, nawet dla localhosta: - -```php -$configurator->setDebugMode(false); -``` - - -Parametry ---------- - -W plikach konfiguracyjnych możesz też używać parametrów definiowanych [w sekcji `parameters` |dependency-injection:configuration#Parametry]. - -Można je też wstawić z zewnątrz metodą `addDynamicParameters()`: - -```php -$configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Do parametru `remoteIp` można odwołać się w konfiguracji zapisem `%remoteIp%`. - - -Jeśli aktualizujesz do nowszej wersji, zajrzyj na stronę [aktualizacji |upgrading]. diff --git a/bootstrap/pl/@left-menu.texy b/bootstrap/pl/@left-menu.texy deleted file mode 100644 index e5a94dddb2..0000000000 --- a/bootstrap/pl/@left-menu.texy +++ /dev/null @@ -1,13 +0,0 @@ -Nette Bootstrap -*************** -- [Przegląd |@home] -- [Aktualizacja|upgrading] - - -Dalsza lektura -************** -- [Dokumentacja Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Dobre praktyki |best-practices:] -- [Rozwiązywanie problemów |nette:troubleshooting] diff --git a/bootstrap/pl/@meta.texy b/bootstrap/pl/@meta.texy deleted file mode 100644 index 61ac92d1af..0000000000 --- a/bootstrap/pl/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Dokumentacja Nette}} diff --git a/bootstrap/pl/upgrading.texy b/bootstrap/pl/upgrading.texy deleted file mode 100644 index d71d8cc454..0000000000 --- a/bootstrap/pl/upgrading.texy +++ /dev/null @@ -1,18 +0,0 @@ -Aktualizacja -************ - - -Aktualizacja do wersji 3.1 -========================== - -- klasa `Nette\Configurator` została przemianowana na `Nette\Bootstrap\Configurator` dla spójności z resztą frameworku -- dodano metodę `addStaticParameters()` jako alias dla `addParameters()` -- parametry przekazywane przez `addStaticParameters()` albo `addParameters()` nie rozwijają już `%parameters%` - - -Aktualizacja do wersji 2.3 -========================== - -- usunięto przestarzałe stałe `Configurator::DEVELOPMENT` i `PRODUCTION` -- `Configurator::setDebugMode()` przyjmuje tylko bool / string / array -- w pliku konfiguracyjnym możesz przenieść wszystkie sekcje umieszczone pod `nette` o poziom wyżej; jeśli przeniesiesz wyżej sekcję `container`, `mailer` albo `debugger`, przemianuj ją na `di`, `mail` i `tracy` diff --git a/bootstrap/ru/@home.texy b/bootstrap/ru/@home.texy deleted file mode 100644 index e7c34492a1..0000000000 --- a/bootstrap/ru/@home.texy +++ /dev/null @@ -1,99 +0,0 @@ -Nette Bootstrap -*************** - -.[perex] -Отдельные компоненты Nette настраиваются через конфигурационные файлы. Мы покажем, как эти файлы загрузить. - -.[tip] -Если вы используете весь фреймворк, ничего больше делать не нужно. У вашего проекта есть подготовленный каталог `config/` для конфигурационных файлов, а их загрузкой занимается [загрузчик приложения |application:bootstrapping#Конфигурация DI-контейнера]. Эта статья для тех, кто использует только одну библиотеку Nette и хочет воспользоваться конфигурационными файлами. - -Конфигурационные файлы обычно записываются в [формате NEON|neon:format], и лучше всего править их в [редакторах, которые его поддерживают |tools:ide]. Их можно понимать как указания, как **создавать и настраивать** объекты. Так что результатом загрузки конфигурации будет так называемая фабрика - объект, который по требованию создаёт другие объекты, которые вы хотите использовать. Например, соединение с базой данных и т. п. - -Эту фабрику называют ещё *контейнером внедрения зависимостей* (DI-контейнером), и если вас интересуют подробности, прочитайте главу о [внедрении зависимостей |dependency-injection:]. - -Загрузкой конфигурации и созданием контейнера занимается класс [api:Nette\Bootstrap\Configurator], поэтому сначала мы установим его пакет `nette/bootstrap`: - -```shell -composer require nette/bootstrap -``` - -И создадим экземпляр класса `Configurator`. Поскольку порождённый DI-контейнер будет кешироваться на диск, нужно задать путь к каталогу, куда он будет сохраняться: - -```php -$configurator = new Nette\Bootstrap\Configurator; -$configurator->setTempDirectory(__DIR__ . '/temp'); -``` - -В Linux или macOS задайте каталогу `temp/` [права на запись |nette:troubleshooting#Задание прав на каталоги]. - -Теперь перейдём к самим конфигурационным файлам. Мы загружаем их через `addConfig()`: - -```php -$configurator->addConfig(__DIR__ . '/database.neon'); -``` - -Если вы хотите добавить больше конфигурационных файлов, функцию `addConfig()` можно вызвать несколько раз. Если в файлах окажутся элементы с одинаковыми ключами, они будут перезаписаны (или [объединены |dependency-injection:configuration#Слияние] в случае массивов). Файл, добавленный позже, имеет более высокий приоритет, чем предыдущий. - -Последний шаг - создание DI-контейнера: - -```php -$container = $configurator->createContainer(); -``` - -И он создаст за нас нужные объекты. Например, если вы используете конфигурацию для [Nette Database|database:configuration], вы можете попросить его создать соединения с базой данных: - -```php -$db = $container->getByType(Nette\Database\Connection::class); -// либо -$explorer = $container->getByType(Nette\Database\Explorer::class); -// либо при создании нескольких соединений -$db = $container->getByName('database.main.connection'); -``` - -И теперь вы можете работать с базой данных! - - -Режим разработки и продакшн-режим ---------------------------------- - -В режиме разработки контейнер автоматически обновляется всякий раз, когда меняются конфигурационные файлы. В продакшн-режиме он порождается только один раз, и изменения не проверяются. Так что режим разработки нацелен на максимальное удобство программиста, а продакшн-режим - на производительность и боевое развёртывание. - -Выбор режима выполняется автоопределением, так что обычно ничего настраивать и переключать вручную не нужно. Режим считается разработческим, если приложение запущено на localhost (то есть IP-адрес `127.0.0.1` или `::1`) и при этом нет прокси (то есть его HTTP-заголовка). Иначе работает продакшн-режим. - -Если мы хотим включить режим разработки и в других случаях, например для программистов, заходящих с определённого IP-адреса, используйте `setDebugMode()`: - -```php -$configurator->setDebugMode('23.75.345.200'); -// можно указать и массив IP-адресов -``` - -Мы настоятельно рекомендуем сочетать IP-адрес с cookie. В cookie `nette-debug` сохраните секретный токен, например `secret1234`. Так вы включите режим разработки для программистов, заходящих с определённого IP-адреса и имеющих в cookie этот токен: - -```php -$configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Режим разработки можно и полностью отключить, даже для localhost: - -```php -$configurator->setDebugMode(false); -``` - - -Параметры ---------- - -В конфигурационных файлах можно использовать и параметры, которые определяются [в секции `parameters` |dependency-injection:configuration#Параметры]. - -Их можно вставить и снаружи методом `addDynamicParameters()`: - -```php -$configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -На параметр `remoteIp` можно сослаться в конфигурации записью `%remoteIp%`. - - -Если вы переходите на более новую версию, посмотрите страницу [обновления |upgrading]. diff --git a/bootstrap/ru/@left-menu.texy b/bootstrap/ru/@left-menu.texy deleted file mode 100644 index 8b44f1f22d..0000000000 --- a/bootstrap/ru/@left-menu.texy +++ /dev/null @@ -1,13 +0,0 @@ -Nette Bootstrap -*************** -- [Обзор |@home] -- [Обновление|upgrading] - - -Дополнительные материалы -************************ -- [Документация Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Лучшие практики |best-practices:] -- [Устранение неполадок |nette:troubleshooting] diff --git a/bootstrap/ru/@meta.texy b/bootstrap/ru/@meta.texy deleted file mode 100644 index 7f329adfce..0000000000 --- a/bootstrap/ru/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документация Nette}} diff --git a/bootstrap/ru/upgrading.texy b/bootstrap/ru/upgrading.texy deleted file mode 100644 index 7972806304..0000000000 --- a/bootstrap/ru/upgrading.texy +++ /dev/null @@ -1,18 +0,0 @@ -Обновление -********** - - -Обновление до версии 3.1 -======================== - -- класс `Nette\Configurator` переименован в `Nette\Bootstrap\Configurator` ради согласованности с остальным фреймворком -- добавлен метод `addStaticParameters()` как синоним `addParameters()` -- параметры, переданные через `addStaticParameters()` или `addParameters()`, больше не разворачивают `%parameters%` - - -Обновление до версии 2.3 -======================== - -- удалены устаревшие константы `Configurator::DEVELOPMENT` и `PRODUCTION` -- `Configurator::setDebugMode()` принимает только bool, string или array -- в конфигурационном файле все секции, помещённые под `nette`, можно поднять на уровень выше; если вы поднимаете секцию `container`, `mailer` или `debugger`, переименуйте её соответственно в `di`, `mail` и `tracy` diff --git a/bootstrap/tr/@home.texy b/bootstrap/tr/@home.texy deleted file mode 100644 index 0f97f854c5..0000000000 --- a/bootstrap/tr/@home.texy +++ /dev/null @@ -1,99 +0,0 @@ -Nette Bootstrap -*************** - -.[perex] -Tek tek Nette bileşenleri yapılandırma dosyalarıyla ayarlanır. Bu dosyaların nasıl yükleneceğini göstereceğiz. - -.[tip] -Framework'ün tamamını kullanıyorsanız başka bir şey yapmanıza gerek yok. Projenizde yapılandırma dosyaları için hazır bir `config/` dizini vardır ve onların yüklenmesini [uygulama yükleyicisi |application:bootstrapping#DI konteynerinin yapılandırması] üstlenir. Bu yazı, yalnızca tek bir Nette kütüphanesini kullanan ve yapılandırma dosyalarından yararlanmak isteyen kullanıcılar içindir. - -Yapılandırma dosyaları genellikle [NEON biçiminde|neon:format] yazılır ve onları [biçimi destekleyen düzenleyicilerde |tools:ide] düzenlemek en iyisidir. Onları, nesnelerin nasıl **oluşturulacağı ve yapılandırılacağı** yönergeleri olarak düşünebilirsiniz. Böylece bir yapılandırmayı yüklemenin sonucu, kullanmak istediğiniz diğer nesneleri istendiğinde oluşturan bir nesne olan factory olacaktır. Örneğin bir veritabanı bağlantısı vb. - -Bu factory'ye *dependency injection container* (DI container) da denir ve ayrıntılarla ilgileniyorsanız [dependency injection |dependency-injection:] bölümünü okuyun. - -Yapılandırmanın yüklenmesini ve container'ın oluşturulmasını [api:Nette\Bootstrap\Configurator] sınıfı üstlenir, dolayısıyla önce onun `nette/bootstrap` paketini kuralım: - -```shell -composer require nette/bootstrap -``` - -Ve `Configurator` sınıfının bir örneğini oluşturalım. Üretilen DI container diske önbelleklendiğinden, onun kaydedileceği dizinin yolunu ayarlamanız gerekir: - -```php -$configurator = new Nette\Bootstrap\Configurator; -$configurator->setTempDirectory(__DIR__ . '/temp'); -``` - -Linux ya da macOS'ta `temp/` dizini için [yazma izinlerini |nette:troubleshooting#Dizin İzinlerini Ayarlama] ayarlayın. - -Şimdi yapılandırma dosyalarının kendisine geliyoruz. Onları `addConfig()` ile yükleriz: - -```php -$configurator->addConfig(__DIR__ . '/database.neon'); -``` - -Daha fazla yapılandırma dosyası eklemek isterseniz, `addConfig()` fonksiyonunu birden çok kez çağırabilirsiniz. Dosyalarda aynı anahtarlara sahip öğeler bulunursa, onların üzerine yazılır (diziler söz konusuysa [birleştirilir |dependency-injection:configuration#Birleştirme]). Sonradan eklenen dosyanın önceliği öncekinden yüksektir. - -Son adım DI container'ı oluşturmaktır: - -```php -$container = $configurator->createContainer(); -``` - -Ve bu bizim için istenen nesneleri oluşturacak. Örneğin [Nette Database|database:configuration] için yapılandırmayı kullanıyorsanız, ondan veritabanı bağlantıları oluşturmasını isteyebilirsiniz: - -```php -$db = $container->getByType(Nette\Database\Connection::class); -// ya da -$explorer = $container->getByType(Nette\Database\Explorer::class); -// ya da birden çok bağlantı oluşturulurken -$db = $container->getByName('database.main.connection'); -``` - -Ve artık veritabanıyla çalışabilirsiniz! - - -Geliştirme Kipi ile Üretim Kipi -------------------------------- - -Geliştirme kipinde, yapılandırma dosyaları her değiştiğinde container otomatik güncellenir. Üretim kipinde yalnızca bir kez üretilir ve değişiklikler denetlenmez. Böylece geliştirme kipi programcının en üst düzey rahatlığını hedeflerken, üretim kipi başarıma ve üretime dağıtıma odaklanır. - -Kip seçimi otomatik algılamayla yapılır, dolayısıyla genellikle hiçbir şeyi yapılandırmaya ya da elle değiştirmeye gerek yoktur. Uygulama localhost üzerinde çalışıyorsa (yani IP adresi `127.0.0.1` ya da `::1` ise) ve hiçbir vekil sunucu (yani onun HTTP başlığı) yoksa kip geliştirmedir. Aksi hâlde üretim kipinde çalışır. - -Geliştirme kipini başka durumlarda da, örneğin belirli bir IP adresinden erişen programcılar için etkinleştirmek istersek `setDebugMode()` kullanırız: - -```php -$configurator->setDebugMode('23.75.345.200'); -// bir IP adresleri dizisi de belirtilebilir -``` - -IP adresini bir çerezle birleştirmenizi güçlü biçimde öneririz. `nette-debug` çerezinde gizli bir token saklayın, örneğin `secret1234`. Böylece geliştirme kipini, belirli bir IP adresinden erişen ve çerezinde anılan token da bulunan programcılar için etkinleştirirsiniz: - -```php -$configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Geliştirme kipini, localhost için bile tümüyle kapatabiliriz: - -```php -$configurator->setDebugMode(false); -``` - - -Parametreler ------------- - -Yapılandırma dosyalarında, [`parameters` bölümünde |dependency-injection:configuration#Parametreler] tanımlanan parametreleri de kullanabilirsiniz. - -Onlar `addDynamicParameters()` metoduyla dışarıdan da eklenebilir: - -```php -$configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -`remoteIp` parametresine yapılandırmada `%remoteIp%` yazımıyla başvurulabilir. - - -Daha yeni bir sürüme yükseltiyorsanız [yükseltme |upgrading] sayfasına bakın. diff --git a/bootstrap/tr/@left-menu.texy b/bootstrap/tr/@left-menu.texy deleted file mode 100644 index 858f95fcdf..0000000000 --- a/bootstrap/tr/@left-menu.texy +++ /dev/null @@ -1,13 +0,0 @@ -Nette Bootstrap -*************** -- [Genel bakış |@home] -- [Yükseltme|upgrading] - - -Devamını okuyun -*************** -- [Nette dokümantasyonu |nette:] -- [Nette Application |application:how-it-works] -- [Yardımcı araçlar |utils:] -- [En iyi uygulamalar |best-practices:] -- [Sorun giderme |nette:troubleshooting] diff --git a/bootstrap/tr/@meta.texy b/bootstrap/tr/@meta.texy deleted file mode 100644 index 8dfe82f311..0000000000 --- a/bootstrap/tr/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokümantasyonu}} diff --git a/bootstrap/tr/upgrading.texy b/bootstrap/tr/upgrading.texy deleted file mode 100644 index 1af0aaba79..0000000000 --- a/bootstrap/tr/upgrading.texy +++ /dev/null @@ -1,18 +0,0 @@ -Yükseltme -********* - - -Sürüm 3.1'e Yükseltme -===================== - -- `Nette\Configurator` sınıfı, framework'ün geri kalanıyla tutarlılık için `Nette\Bootstrap\Configurator` olarak yeniden adlandırıldı -- `addStaticParameters()` metodu, `addParameters()` metodunun alias'ı olarak eklendi -- `addStaticParameters()` ya da `addParameters()` ile aktarılan parametreler artık `%parameters%` ifadelerini genişletmiyor - - -Sürüm 2.3'e Yükseltme -===================== - -- deprecated `Configurator::DEVELOPMENT` ve `PRODUCTION` sabitleri kaldırıldı -- `Configurator::setDebugMode()` yalnızca bool / string / array kabul ediyor -- yapılandırma dosyasında `nette` altına yerleştirilmiş tüm bölümleri bir düzey yukarı taşıyabilirsiniz; `container`, `mailer` ya da `debugger` bölümünü yukarı taşırsanız, onları `di`, `mail` ve `tracy` olarak yeniden adlandırın diff --git a/caching/cs/@home.texy b/caching/cs/@home.texy deleted file mode 100644 index 78f886e24d..0000000000 --- a/caching/cs/@home.texy +++ /dev/null @@ -1,258 +0,0 @@ -Nette Caching -************* - -<div class=perex> - -Počítá vaše aplikace pořád dokola totéž, třeba převádí článek do HTML, spouští pomalý databázový dotaz nebo volá vzdálené API? Nette Caching si výsledek zapamatuje a příště ho vrátí ve zlomku času: - -- Jedna metoda hodnotu přečte z cache, a když tam chybí, vypočítá a uloží ji -- Data vyexpirují po čase, nebo když se změní soubor, jiná položka či vaše vlastní podmínka -- Tagy smažou přesně ty položky, které souvisejí s právě upraveným článkem -- Nával požadavků na chybějící položku server nepřetíží -- Ukládá do souborů, do Redisu, Memcached, SQLite nebo libovolné PSR-16 cache -- Cachuje části šablon Latte - -</div> - - -Instalace -========= - -Knihovnu stáhnete a nainstalujete pomocí nástroje [Composer|best-practices:composer]: - -```shell -composer require nette/caching -``` - -Vyžaduje PHP verze 8.4 a podporuje PHP až do verze 8.6. - - -První cache -=========== - -Nejlíp se to ukáže na příkladu. Dejme tomu, že provozujete blog. Pokaždé, když někdo otevře článek, načte ho váš kód z databáze a převede jeho text do HTML. Chvíli to trvá a výsledek je pro všechny návštěvníky stejný, dokud článek neupravíte. Ideální kandidát pro cache. - -Napíšeme si to jako krátký skript. Nejprve vytvoříme dva objekty: - -```php -use Nette\Caching\Cache; -use Nette\Caching\Storages\FileStorage; - -require __DIR__ . '/vendor/autoload.php'; - -$storage = new FileStorage(__DIR__ . '/temp'); -$cache = new Cache($storage, 'articles'); -``` - -**Úložiště** určuje, kde data fyzicky leží. `FileStorage` je ukládá do souborů v adresáři `temp`, který musí existovat. **Cache** je objekt, se kterým budete pracovat. Jeho druhý argument, `'articles'`, je jmenný prostor: odděluje články od všeho ostatního, co aplikace cachuje, takže dvě části programu mohou použít stejný klíč, aniž by si překážely. - -Teď to podstatné. Cache funguje jako asociativní pole: hodnoty čtete a zapisujete pod klíči. Metoda `load()` dělá obojí najednou: - -```php -$html = $cache->load("article/$id", function () use ($id) { - return renderArticle($id); // načte článek z databáze a převede ho do HTML -}); -``` - -Proměnná `$id` je ID zobrazovaného článku. Prvním argumentem `load()` je klíč, druhým funkce, které se říká **generátor**. Když klíč v cache ještě není, `load()` generátor zavolá, uloží, co vrátí, a vrátí to vám. Příště už hodnotu v cache najde a vrátí ji rovnou, generátor vůbec nevolá. - -Všimněte si, že nikde nezjišťujete, jestli už hodnota v cache je. Jen řeknete, jak ji spočítat. - -A co když stejný článek otevře sto návštěvníků zrovna ve chvíli, kdy v cache ještě není, třeba hned po nasazení? Bez ochrany by ho všech sto požadavků renderovalo naráz a mohlo by server přetížit. Takové situaci se říká [cache stampede |https://en.wikipedia.org/wiki/Cache_stampede] a `FileStorage` jí brání: generátor spustí jen první požadavek, ostatní počkají na jeho výsledek. - -.[tip] -Do cache můžete ukládat jakoukoliv hodnotu, kterou PHP umí serializovat, nejen řetězce: pole, objekty, čísla. Totéž platí i pro klíče. - -Pokud chcete hodnotu jen přečíst a negenerovat, zavolejte `load()` jen s klíčem; když položka v cache není, vrátí `null`. Metoda `remove()` položku smaže: - -```php -$cache->remove("article/$id"); -``` - - -Jak dlouho data platí? -====================== - -Náš článek v cache se teď nikdy nezmění. Jenže co když v něm opravíte překlep? Návštěvníci by dál viděli starou verzi. Musíte cache říct, kdy položka přestává platit. Nejjednodušší je časový limit: - -```php -use Nette\Caching\Dependencies; - -$html = $cache->load("article/$id", function (Dependencies $dp) use ($id) { - $dp->expire = '1 hour'; - return renderArticle($id); -}); -``` - -Generátor teď přijímá parametr typu `Dependencies` a `load()` mu předá objekt, ve kterém generátor popíše, kdy položka vyexpiruje. Říká se mu závislosti, protože na nich závisí platnost položky. Tady je to jedna hodina. - -Čas je jen jedna z možností. Položka může vyexpirovat i ve chvíli, kdy se změní soubor nebo jiná položka v cache, případně když to řekne vaše vlastní podmínka. To všechno popisuje stránka [Expirace a invalidace |expiration]. - - -Smazat přesně to, co se změnilo -=============================== - -Časový limit je kompromis: nastavíte ho krátký a cache moc nepomůže, nastavíte dlouhý a návštěvníci vidí stará data. Lepší je smazat článek z cache přesně ve chvíli, kdy ho upravíte. K tomu slouží **tagy**. Položce je přiřadíte jako libovolné řetězce: - -```php -$html = $cache->load("article/$id", function (Dependencies $dp) use ($id) { - $dp->tags = ["article/$id", "comments/$id"]; - return renderArticle($id); -}); -``` - -Stránka ukazuje článek i s komentáři, proto jsme jí dali dva tagy. V administraci pak hned po uložení upraveného článku do databáze smažete všechno, co nese jeho tag: - -```php -$cache->clean(tags: ["article/$id"]); -``` - -A totéž, když někdo přidá komentář, tentokrát s tagem `comments/$id`. Od teď stránka z cache zmizí přesně ve chvíli, kdy se změní její článek nebo komentáře, a další návštěvník dostane čerstvě vyrenderovanou. - -`FileStorage` si pamatuje, která položka nese který tag, v tzv. journalu, malé SQLite databázi. V aplikaci postavené na Nette je nastavený za vás; v našem skriptu ho vytvoříme a předáme úložišti: - -```php -use Nette\Caching\Storages\SQLiteJournal; - -$journal = new SQLiteJournal(__DIR__ . '/temp/journal.s3db'); -$storage = new FileStorage(__DIR__ . '/temp', $journal); -``` - -Kromě tagů můžete mazat položky podle priority nebo celý jmenný prostor najednou. Viz [Expirace a invalidace |expiration#Mazání položek]. - - -Jmenné prostory -=============== - -Jmenný prostor `'articles'` jsme už potkali v konstruktoru cache. Každá část aplikace by měla mít svůj, aby se její klíče nikdy nepotkaly s cizími. Komentáře by mohly mít třeba vlastní cache: - -```php -$commentCache = new Cache($storage, 'comments'); -``` - -Metoda `derive()` vytvoří cache ve vnořeném jmenném prostoru: `$cache->derive('previews')` dá prostor `previews` uvnitř `articles`. A když chcete zahodit všechno ve jmenném prostoru včetně vnořených, zavolejte: - -```php -$cache->clean(all: true); -``` - -Ostatní jmenné prostory zůstanou nedotčené. Jen cache vytvořená bez jmenného prostoru, `new Cache($storage)`, takhle smaže celé úložiště. - - -Memoizace -========= - -Někdy chcete cachovat výsledek volání funkce. V moderaci komentářů třeba ukazujete u každého autora název počítače podle jeho IP adresy. Zjistit ho funkcí `gethostbyaddr()` znamená zeptat se DNS serveru a to může trvat i sekundy. Metoda `call()` zavolá funkci pro každý argument jen jednou a pak vrací výsledek z cache: - -```php -$host = $cache->call('gethostbyaddr', $ip); -``` - -Proměnná `$ip` je IP adresa autora komentáře. Metoda `wrap()` dělá totéž, ale místo aby funkci hned zavolala, vrátí novou funkci, kterou můžete volat později a kolikrát chcete: - -```php -$getHost = $cache->wrap('gethostbyaddr'); - -$host = $getHost($ip); // poprvé se zeptá DNS serveru -$host = $getHost($ip); // podruhé vrátí výsledek z cache -``` - - -Cachování části stránky -======================= - -Ne všechno, co chcete cachovat, je návratová hodnota. Někdy je to kus HTML, který nějaký kód vypisuje. Ten zachytí metoda `capture()`: - -```php -if ($capture = $cache->capture('sidebar')) { - // echo ... vypíše postranní panel s nejnovějšími články - $capture->end(); // uloží vypsaný výstup do cache -} -``` - -Když už je výstup v cache, `capture()` ho vypíše a vrátí `null`, takže se blok uvnitř `if` vůbec nevykoná. Jinak začne výstup zachytávat a vrátí objekt, jehož metoda `end()` uloží všechno, co se mezitím vypsalo. Můžete jí předat i závislosti, třeba `$capture->end(new Dependencies(tags: ['articles']))`. - - -Cachování v Latte -================= - -V šablonách [Latte|latte:] je to ještě jednodušší. Část, kterou chcete cachovat, obalíte značkami `{cache}...{/cache}`: - -```latte -{cache expire: '20 minutes', tags: [articles]} - <h2>Nejnovější články</h2> - ... -{/cache} -``` - -Blok se automaticky zneplatní, kdykoliv se změní šablona, včetně šablon vložených uvnitř bloku. Bloky lze vnořovat; když se zneplatní vnořený blok, zneplatní se i ten nadřazený. - -Všechny parametry jsou volitelné. Bez `expire` blok vyexpiruje po 7 dnech. Pokud obsah závisí na proměnné, uveďte ji na začátku a pro každou hodnotu vznikne vlastní kopie v cache: `{cache $article->id}`. A parametr `if` rozhodne, jestli vůbec cachovat: - -```latte -{cache $id, if: !$form->isSubmitted()} - {$form} -{/cache} -``` - - -Kam se data ukládají -==================== - -Zatím jsme používali `FileStorage`, kterému stačí adresář. Díky atomickým operacím se soubory je bezpečné, i když najednou čte a zapisuje mnoho požadavků. Je to ale jen jedno z několika úložišť: - -|---------------------- -| Úložiště | Kde data leží -|---------------------- -| `FileStorage` | v souborech na disku; výchozí úložiště -| `RedisStorage` | na serveru Redis -| `MemcachedStorage` | na serveru Memcached -| `SQLiteStorage` | v souboru databáze SQLite -| `MemoryStorage` | v paměti, jen do konce požadavku -| `Psr16Storage` | v libovolné PSR-16 cache -| `DevNullStorage` | nikde; pro testy a pro vypnutí cache - -Úložiště můžete vyměnit, aniž byste sáhli na kód, který cache používá. Které si vybrat, co které umí a jak ho nastavit, včetně vypnutí cache, popisuje stránka [Úložiště |storages]. - - -Cache v aplikaci Nette -====================== - -V aplikaci postavené na Nette si úložiště sami nevytváříte. Nette poskytuje `FileStorage`, které ukládá data do podadresáře `cache` v adresáři pro [dočasné soubory |application:bootstrapping#Dočasné soubory], a k němu i journal. Dostanete ho pomocí [dependency injection |dependency-injection:passing-dependencies], když si řeknete o typ `Nette\Caching\Storage`, a cache si vytvoříte v konstruktoru: - -```php -use Nette\Caching\Cache; -use Nette\Caching\Storage; - -class ArticleFacade -{ - private Cache $cache; - - - public function __construct(Storage $storage) - { - $this->cache = new Cache($storage, 'articles'); - } -} -``` - -Zbytek je stejný jako v našem skriptu. Případně si můžete nechat do konstruktoru předat rovnou objekt `Cache` a nadefinovat ho v konfiguraci: - -```neon -services: - - ArticleFacade( Nette\Caching\Cache(namespace: 'articles') ) -``` - -.[note] -Cache šablon Latte a DI kontejneru je samostatná a toto úložiště nepoužívá. Během vývoje ji [není potřeba vypínat |nette:troubleshooting#Jak vypnout cache během vývoje]. - - -Co dál -====== - -- [Expirace a invalidace |expiration] - všechny způsoby, jak cache říct, kdy data vyexpirují, a mazání podle tagů, priority a jmenného prostoru -- [Úložiště |storages] - výběr a nastavení úložiště, PSR-16 oběma směry, vlastní úložiště -- [Upgrade |upgrading] - co se změnilo ve verzi 4.0 - -{{maintitle: Cache v PHP s Nette: úložiště, expirace a invalidace}} -{{description: Zrychlete aplikaci tím, že náročně získaná data uložíte pro příští použití. Cache z Nette v PHP i Latte, změna úložiště a správná invalidace.}} diff --git a/caching/cs/@left-menu.texy b/caching/cs/@left-menu.texy deleted file mode 100644 index a9d7c652aa..0000000000 --- a/caching/cs/@left-menu.texy +++ /dev/null @@ -1,15 +0,0 @@ -Nette Caching -************* -- [Úvod |@home] -- [Expirace a invalidace |expiration] -- [Úložiště |storages] -- [Upgrade |upgrading] - - -Další četba -*********** -- [Dokumentace Nette |nette:] -- [Aplikace v Nette |application:how-it-works] -- [Utilities |utils:] -- [Návody a postupy |best-practices:] -- [Řešení problémů |nette:troubleshooting] diff --git a/caching/cs/@meta.texy b/caching/cs/@meta.texy deleted file mode 100644 index 462d9add80..0000000000 --- a/caching/cs/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokumentace}} diff --git a/caching/cs/expiration.texy b/caching/cs/expiration.texy deleted file mode 100644 index 14940c2b89..0000000000 --- a/caching/cs/expiration.texy +++ /dev/null @@ -1,241 +0,0 @@ -Expirace a invalidace -********************* - -<div class=perex> - -Cache je užitečná jen tak dlouho, dokud nevrací zastaralá data. Tato stránka ukazuje všechny způsoby, jak Nette Caching říct, kdy položka přestává platit: - -- po uplynutí doby, nebo když ji delší dobu nikdo nečetl -- když se změní soubor, PHP konstanta nebo jiná položka v cache -- když to řekne vaše vlastní funkce -- nebo hned, když ji smažete podle tagu, priority či jmenného prostoru - -</div> - -Ukázky navazují na blog z [úvodu |@home]: `$cache` je cache ve jmenném prostoru `articles`, `$id` je ID článku a `renderArticle()` zastupuje náročnou práci, kdy se článek načte a převede do HTML. - - -Nastavení závislostí -==================== - -Kdy položka vyexpiruje, popisují **závislosti**, tedy objekt `Nette\Caching\Dependencies`. Nejpohodlnější je nechat je vyplnit generátor. Stačí deklarovat jeho parametr s typem `Dependencies` a `load()` mu předá prázdný objekt: - -```php -use Nette\Caching\Dependencies; - -$html = $cache->load("article/$id", function (Dependencies $dp) use ($id) { - $dp->expire = '1 hour'; - return renderArticle($id); -}); -``` - -Výhoda je, že se generátor může rozhodnout podle toho, co spočítal. Rozepsaný článek může třeba vyexpirovat dřív než publikovaný. - -Když závislosti znáte předem, můžete je místo toho předat jako třetí argument metodě `load()`, případně metodě `save()`, která hodnotu uloží rovnou, bez generátoru. Konstruktor přijímá všechny vlastnosti jako pojmenované argumenty: - -```php -$html = $cache->load("article/$id", fn() => renderArticle($id), new Dependencies(expire: '1 hour')); - -$cache->save("article/$id", $html, new Dependencies(expire: '1 hour', tags: ["article/$id"])); -``` - -Dávejte přednost `load()` s generátorem před `save()`. Když o chybějící položku požádá víc požadavků najednou, `FileStorage` a `RedisStorage` spustí generátor jen u jednoho z nich; se `save()` by hodnotu počítaly všechny. - -Závislosti lze zapsat i jako pole s klíči `Cache::*`, třeba `[Cache::Expire => '1 hour', Cache::Tags => ["article/$id"]]`. Generátor, jehož parametr nemá typ `Dependencies`, dostane toto pole referencí a přidává do něj: `function (&$dependencies)`. Neznámý klíč nebo hodnota špatného typu v poli vyhodí výjimku, takže překlep jako `'expir'` nenechá položku potichu bez expirace. - - -Expirace podle času -=================== - -Vlastnost `expire` určuje, kdy položka vyexpiruje. Přijímá relativní čas jako řetězec, počet sekund, UNIX timestamp nebo objekt `DateTimeInterface`: - -```php -$dp->expire = '20 minutes'; -$dp->expire = 3600; // jedna hodina -$dp->expire = new DateTimeImmutable('tomorrow'); -``` - -U **klouzavé expirace** každé čtení posune expiraci dál. Článek, který návštěvníci pořád čtou, pak zůstává v cache, zatímco ten, který 20 minut nikdo neotevře, z ní vypadne: - -```php -$dp->expire = '20 minutes'; -$dp->sliding = true; -``` - -Počítejte s tím, že každé čtení pak položku v úložišti zároveň aktualizuje, což přidává režii. - - -Když se změní soubor -==================== - -Data spočítaná ze souboru by měla vyexpirovat, když se soubor změní. Dejme tomu, že blog čte seznam kategorií ze souboru `categories.neon`. Seznam v cache na tom souboru tedy závisí: - -```php -$categories = $cache->load('categories', function (Dependencies $dp) { - $dp->files[] = __DIR__ . '/categories.neon'; - return loadCategories(); // přečte a zpracuje categories.neon -}); -``` - -Jakmile `categories.neon` upravíte, cache si toho při dalším čtení všimne a zavolá generátor znovu. Používejte absolutní cesty. Položka může záviset i na více souborech: `$dp->files = [$file1, $file2]`. - - -Když se změní konstanta -======================= - -Podobně může položka vyexpirovat, když se změní hodnota PHP konstanty. Hodí se to třeba pro číslo verze, které nastavujete při nasazení, aby každá nová verze začínala s čerstvou cache: - -```php -$dp->constants[] = 'APP_VERSION'; -``` - - -Když se změní jiná položka -========================== - -Položka může záviset na jiných položkách v cache. Dejme tomu, že článek a jeho komentáře cachujete zvlášť, pod klíči `article/$id` a `comments/$id`, a cachujete i celou stránku z nich složenou. Stránka musí vyexpirovat, kdykoliv vyexpiruje některá z jejích částí: - -```php -$page = $cache->load("page/$id", function (Dependencies $dp) use ($id) { - $dp->items = ["article/$id", "comments/$id"]; - return renderPage($id); // poskládá článek a jeho komentáře -}); -``` - -Když se komentáře znovu uloží nebo smažou, vyexpiruje i stránka. - - -Vlastní podmínka -================ - -Když nic z uvedeného nesedí, napište si vlastní funkci. Volá se při každém čtení s argumenty, které jste uložili, a když vrátí `false`, položka vyexpiruje. Dejme tomu, že vyrenderované HTML nesmí přežít upgrade PHP: - -```php -function checkPhpVersion(int $version): bool -{ - return $version === PHP_VERSION_ID; -} - -$dp->callbacks[] = ['checkPhpVersion', PHP_VERSION_ID]; -``` - -Každá položka je pole: funkce, případně statická metoda zapsaná jako `[ClassName::class, 'method']`, a za ní její argumenty. Funkce se volá při každém čtení, takže by měla být rychlá. - - -Kombinace podmínek -================== - -Všechny tyto podmínky lze kombinovat. Položka vyexpiruje, jakmile přestane platit alespoň jedna z nich: - -```php -$dp->expire = '1 day'; -$dp->files[] = __DIR__ . '/categories.neon'; -$dp->constants[] = 'APP_VERSION'; -``` - - -Tagy -==== - -Čekat na expiraci je kompromis. Lepší je položku smazat přesně ve chvíli, kdy se změní data, ze kterých vznikla. To umožňují **tagy**. Jsou to libovolné řetězce, které položce přiřadíte: - -```php -$html = $cache->load("article/$id", function (Dependencies $dp) use ($id) { - $dp->tags = ["article/$id", "comments/$id"]; - return renderArticle($id); -}); -``` - -V administraci pak hned po uložení upraveného článku do databáze smažete všechno, co nese jeho tag: - -```php -$cache->clean(tags: ["article/$id"]); -``` - -Totéž platí pro přidání komentáře, tentokrát s tagem `comments/$id`. Všimněte si, že kód, který položky maže, nemusí znát jejich klíče, ani kolik jich je. Článek může v cache ležet jako celá stránka, jako náhled na úvodní stránce i v RSS kanálu; jedno volání je odstraní všechny. - -Tagy fungují napříč jmennými prostory: `clean(tags: [...])` smaže odpovídající položky ve všech jmenných prostorech úložiště, nejen v prostoru `$cache`. - -.[note] -`FileStorage`, `MemcachedStorage` a `Psr16Storage` potřebují pro tagy [journal |storages#Journal]; ostatní úložiště si je drží sama. - - -Priorita -======== - -Každé položce můžete dát prioritu, tedy číslo, které říká, jak je důležitá. Když cache příliš naroste, můžete pak smazat nejdřív ty méně důležité. Dejme tomu, že náhledy starých článků mají menší cenu než úvodní stránka: - -```php -$dp->priority = 10; -``` - -Takto smažete všechny položky s prioritou 50 nebo menší: - -```php -$cache->clean(priority: 50); -``` - -Položky bez priority se tímto způsobem nikdy nesmažou. - -.[note] -`FileStorage`, `MemcachedStorage` a `Psr16Storage` potřebují pro priority [journal |storages#Journal], `RedisStorage` a `MemoryStorage` si je drží samy a `SQLiteStorage` je nepodporuje. - - -Mazání položek -============== - -Metoda `clean()` maže položky podle podmínek, které předáte jako pojmenované argumenty: - -| Argument | Smaže -|---------------------- -| `tags: [...]` | položky s kterýmkoliv z tagů -| `priority: 50` | položky s prioritou 50 nebo menší -| `namespaces: [...]` | všechny položky v uvedených jmenných prostorech, včetně prostorů z nich odvozených -| `all: true` | vše ve jmenném prostoru této cache, včetně prostorů z něj odvozených - -Podmínky lze kombinovat a smaže se položka, která splňuje kteroukoliv z nich: - -```php -$cache->clean(tags: ["article/$id"], priority: 50); -``` - -Podívejme se blíž na `all`. Smaže celý jmenný prostor cache spolu s prostory, které z něj vznikly pomocí `derive()`, a ostatních jmenných prostorů se nedotkne. Jen cache vytvořená bez jmenného prostoru, `new Cache($storage)`, takhle smaže celé úložiště. - -Podmínky lze zapsat i jako pole s klíči `Cache::*`, třeba `$cache->clean([Cache::Tags => ["article/$id"]])`. Stejně jako u závislostí vyhodí neznámý klíč nebo hodnota špatného typu výjimku. - -Volání `clean()` bez jakékoliv podmínky vyhodí výjimku také, protože není jasné, co by mělo smazat. Odstranit položky, které už vyexpirovaly, je věc úložiště: zavolejte `$storage->clean([])`. Většina úložišť to ostatně dělá sama, viz [Úložiště |storages]. - -Jednu položku smažete pomocí `$cache->remove("article/$id")`. - - -Co které úložiště umí -===================== - -Ne každé úložiště umí každý druh závislosti. Úložiště, které některou neumí, vyhodí `Nette\NotSupportedException`, místo aby ji potichu ignorovalo, takže na to přijdete hned, a ne až podle zastaralých dat. `SQLiteStorage` třeba nepodporuje priority ani závislosti na souborech. Jedinou výjimkou je `MemoryStorage`, které ignoruje, co neumí. Úplný přehled najdete v tabulce na stránce [Úložiště |storages#Výběr úložiště]. - - -Hromadné čtení a zápis -====================== - -Úvodní stránka blogu ukazuje náhledy deseti článků. Číst je z cache jeden po druhém znamená desetkrát se obrátit na úložiště. Metoda `bulkLoad()` je přečte všechny najednou: předáte jí pole `$ids` s jejich ID jako klíči a vrátí pole hodnot indexované těmito klíči: - -```php -$previews = $cache->bulkLoad($ids, function ($id, Dependencies $dp) { - $dp->tags = ["article/$id"]; - return renderPreview($id); // vyrenderuje krátký náhled článku -}); -``` - -Generátor se zavolá jen pro náhledy, které chybí, a dostane klíč a za ním závislosti. Bez generátoru vrátí `bulkLoad($ids)` pro chybějící položky `null`. - -Metoda `bulkSave()` uloží několik položek najednou, všechny se stejnými závislostmi: - -```php -$cache->bulkSave([ - "preview/$id1" => $preview1, - "preview/$id2" => $preview2, -], new Dependencies(expire: '1 hour')); -``` - -{{maintitle: Nette Caching: expirace, tagy a invalidace cache}} -{{description: Zajistěte, aby cache nikdy nevracela zastaralá data. Expirace podle času, změny souboru či konstanty a mazání podle tagů, priority a jmenného prostoru.}} diff --git a/caching/cs/storages.texy b/caching/cs/storages.texy deleted file mode 100644 index 312141b9a1..0000000000 --- a/caching/cs/storages.texy +++ /dev/null @@ -1,236 +0,0 @@ -Úložiště -******** - -<div class=perex> - -Úložiště určuje, kde data z cache fyzicky leží: v souborech na disku, na serveru Redis nebo Memcached, v databázi SQLite nebo v PSR-16 cache jiného frameworku. Vyměnit ho můžete, aniž byste sáhli na kód, který cache používá. Tato stránka vám pomůže: - -- vybrat správné úložiště a zjistit, co které umí -- nastavit ho v aplikaci Nette, případně cache vypnout -- použít úložiště z Nette jako PSR-16 cache, a obráceně -- napsat si vlastní úložiště - -</div> - - -Výběr úložiště -============== - -Pokud si nejste jistí, nechte výchozí `FileStorage`. Stačí mu adresář, je bezpečné, i když najednou čte a zapisuje mnoho požadavků, a podporuje všechny druhy závislostí. - -Po `RedisStorage` sáhněte, když aplikace běží na více serverech. Se soubory by měl každý server vlastní cache, a když byste smazali článek podle tagu na jednom serveru, na ostatních by zůstaly jeho zastaralé kopie. Server Redis sdílejí všechny. - -Úložiště se liší tím, které [závislosti |expiration] umí respektovat: - -|------------------------------------------------------------------------------------------------- -| Úložiště | Tagy, priority | Soubory, konstanty, callbacky | Jiné položky | Klouzavá expirace | Mazání jmenných prostorů | Ochrana proti stampede -|------------------------------------------------------------------------------------------------- -| [#FileStorage] | s [journalem |#journal] | ano | ano | ano | ano | ano -| [#RedisStorage] | ano | ano | ne | ano | ano | ano -| [#MemcachedStorage] | s [journalem |#journal] | ano | ne | ano | ne | ne -| [#SQLiteStorage] | jen tagy | ne | ne | ano | ano | ne -| [#MemoryStorage] | ano | ignoruje | ignoruje | ano | ano | ne -| [#Psr16Storage] | s [journalem |#journal] | ano | ne | ne | ne | ne -| [#DevNullStorage] | nic neukládá | | | | | - -Úložiště, které některou závislost neumí, vyhodí `Nette\NotSupportedException`, místo aby ji potichu ignorovalo, takže na to přijdete hned, a ne až podle zastaralých dat. Jedinou výjimkou je `MemoryStorage`, které ignoruje, co neumí. - -Ochrana proti stampede znamená, že když o chybějící položku požádá najednou mnoho požadavků, generátor z `load()` spustí jen první z nich a ostatní počkají na jeho výsledek. Bez ní hodnotu počítají všechny zároveň. - - -Konfigurace -=========== - -V aplikaci postavené na Nette poskytuje DI kontejner `FileStorage`, které ukládá data do podadresáře `cache` v adresáři pro [dočasné soubory |application:bootstrapping#Dočasné soubory], a k němu i journal. Dostanete ho pomocí [dependency injection |dependency-injection:passing-dependencies], když si řeknete o typ `Nette\Caching\Storage`. - -Pokud chcete jiné úložiště, nahraďte v konfiguraci službu `cache.storage`: - -```neon -services: - cache.storage: Nette\Caching\Storages\RedisStorage('10.0.0.5') -``` - -Kontejner obsahuje tyto dvě služby: - -| Název | Typ | Popis -|---------------------------------------------------------- -| `cache.storage` | [api:Nette\Caching\Storage] | úložiště -| `cache.journal` | [api:Nette\Caching\Storages\Journal] | [journal |#journal] pro tagy a priority - - -Vypnutí cache -------------- - -Pokud chcete cache vypnout, třeba v testech, použijte [#DevNullStorage], které nic neukládá: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - -Na cache šablon Latte a DI kontejneru to nemá vliv, ty jsou samostatné a toto úložiště nepoužívají. Během vývoje je ostatně [není potřeba vypínat |nette:troubleshooting#Jak vypnout cache během vývoje]. - - -FileStorage -=========== - -`Nette\Caching\Storages\FileStorage` zapisuje každou položku do souboru v zadaném adresáři, který musí existovat. Tagy a priority potřebují [journal |#journal]: - -```php -$journal = new Nette\Caching\Storages\SQLiteJournal('/path/to/temp/journal.s3db'); -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp', $journal); -``` - -Všechny operace jsou atomické. Nikdy nepřečtete soubor, který jiný požadavek ještě nedopsal, a nikdo vám ho nemůže smazat pod rukama, zatímco ho čtete. Úložiště je proto bezpečné i pod velkou zátěží. - -Navíc chrání před [cache stampede |https://en.wikipedia.org/wiki/Cache_stampede]. Když o chybějící položku požádá najednou mnoho požadavků, třeba hned po nasazení, první z nich soubor zamkne a spustí generátor. Ostatní počkají na zámek a pak přečtou čerstvou hodnotu, místo aby ji počítaly znovu. - -Vyexpirované soubory se mažou postupně. Čas od času jim konstruktor věnuje nejvýš 20 ms, takže ani velká cache požadavek znatelně nezdrží. Všechny vyexpirované soubory najednou smažete voláním `$storage->clean([])`. - -Data se ukládají pomocí PHP funkce `serialize()`. Pokud chcete jiný formát, vytvořte potomka a přepište metody `serializeData()` a `unserializeData()`. - - -RedisStorage -============ - -`Nette\Caching\Storages\RedisStorage` ukládá data na server [Redis |https://redis.io]. Tagy a priority si drží sám, takže nepotřebuje journal, a chrání před cache stampede stejně jako `FileStorage`. - -.[caution] -Vyžaduje PHP rozšíření `redis`. - -```neon -services: - cache.storage: Nette\Caching\Storages\RedisStorage('10.0.0.5') -``` - -Konstruktor přijímá adresu serveru a port, výchozí je 6379. Pokud už máte připojený objekt `Redis`, předejte ho místo adresy. - -Všechny klíče dostanou prefix `nette.cache:`, takže databázi můžete sdílet s jinými daty; změníte ho parametrem `prefix`. Prázdný prefix znamená, že celá databáze patří cache, a smazání cache pak databázi vyprázdní. - -Zatímco jeden proces chybějící položku generuje, ostatní na ni čekají, standardně nejvýš 30 sekund (parametr `lockTimeout`). Generátor, který trvá déle, znamená jen to, že stejnou hodnotu spočítá i další proces, nikdy to, že se uloží špatná hodnota. - -Úložiště nepodporuje závislost na jiných položkách. - - -MemcachedStorage -================ - -`Nette\Caching\Storages\MemcachedStorage` ukládá data na server [Memcached |https://memcached.org]. Konstruktor přijímá adresu serveru a port, výchozí je 11211. Tagy a priority potřebují [journal |#journal]. - -.[caution] -Vyžaduje PHP rozšíření `memcached`. - -```neon -services: - cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5') -``` - -Memcached neumí vypsat své klíče, takže úložiště neumí mazat jmenné prostory. Nepodporuje ani závislost na jiných položkách. Smazání celé cache vyprázdní celý server Memcached včetně dat jiných aplikací. - -Když Memcached položku odmítne uložit, typicky takovou, která přesahuje jeho limit velikosti, úložiště vyvolá varování. - - -SQLiteStorage -============= - -`Nette\Caching\Storages\SQLiteStorage` drží celou cache v jediném souboru databáze SQLite. Tagy si ukládá do vlastní tabulky, takže nepotřebuje journal. - -.[caution] -Vyžaduje PHP rozšíření `pdo` a `pdo_sqlite`. - -```neon -services: - cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db') -``` - -Databáze běží v režimu WAL, ve kterém čtení nečeká na zápis. SQLite si pak vedle databáze zakládá další dva soubory, takže PHP musí mít právo zapisovat do jejího adresáře. - -Úložiště podporuje expiraci, klouzavou expiraci, tagy a mazání jmenných prostorů. Nepodporuje priority ani závislosti na souborech, konstantách, callbackech a jiných položkách. - - -MemoryStorage -============= - -`Nette\Caching\Storages\MemoryStorage` drží data v PHP poli, takže se s koncem požadavku ztratí. Hodí se pro testy a pro cachování v rámci jednoho požadavku. - -Respektuje expiraci, klouzavou expiraci, tagy, priority a mazání jmenných prostorů. Závislosti na souborech, konstantách, callbackech a jiných položkách potichu ignoruje. Myslete na to, když jím v testech nahrazujete skutečné úložiště: položka, která by tam vyexpirovala, tady zůstane. - - -Psr16Storage -============ - -`Nette\Bridges\Psr\Psr16Storage` vám umožní použít jako úložiště pro Nette libovolnou [PSR-16 |https://www.php-fig.org/psr/psr-16/] cache, třeba cache jiného frameworku, který už vaše aplikace používá. Tagy a priority potřebují [journal |#journal]: - -```php -// $simpleCache je libovolná Psr\SimpleCache\CacheInterface -$storage = new Nette\Bridges\Psr\Psr16Storage($simpleCache, journal: $journal); -``` - -PSR-16 povoluje v klíčích jen některé znaky, proto úložiště každý klíč převede na hash. Před každý z nich přidá parametr `prefix`, nejvýš 32 písmen, číslic, podtržítek a teček. - -PSR-16 neumí prodloužit platnost položky ani vypsat klíče, takže úložiště nepodporuje klouzavou expiraci ani mazání jmenných prostorů. Nepodporuje ani závislost na jiných položkách. Smazání celé cache zavolá `clear()` na PSR-16 cache, takže smaže i data všech, kdo ji sdílejí, bez ohledu na prefix. - - -DevNullStorage -============== - -`Nette\Caching\Storages\DevNullStorage` nic neukládá, takže každé `load()` zavolá generátor. Použijte ho pro [vypnutí cache |#Vypnutí cache]. - - -Journal -======= - -`FileStorage`, `MemcachedStorage` a `Psr16Storage` neumí své položky vyhledávat podle tagu nebo priority, proto si vedou samostatný index, tzv. journal. Vestavěný `Nette\Caching\Storages\SQLiteJournal` ho ukládá do databáze SQLite: - -```php -$journal = new Nette\Caching\Storages\SQLiteJournal('/path/to/temp/journal.s3db'); -``` - -.[caution] -Vyžaduje PHP rozšíření `pdo` a `pdo_sqlite`. - -V aplikaci postavené na Nette se journal vytvoří za vás jako služba `cache.journal` v souboru `journal.s3db` a úložišti se předá automaticky. Nahradit ho může vaše vlastní implementace rozhraní `Nette\Caching\Storages\Journal`: - -```neon -services: - cache.journal: MyJournal -``` - -Bez journalu tato úložiště fungují také, jen uložení položky s tagy nebo prioritou vyhodí výjimku. - - -Úložiště jako PSR-16 cache -========================== - -Některé knihovny očekávají [PSR-16 |https://www.php-fig.org/psr/psr-16/] cache. Adaptér `Nette\Bridges\Psr\PsrCacheAdapter` jim ji poskytne nad libovolným úložištěm z Nette: - -```php -$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage); - -$psrCache->set('key', 'value', 3600); // uloží hodnotu na 1 hodinu -$value = $psrCache->get('key', 'default'); -``` - -Adaptér podporuje všechny metody PSR-16 včetně `getMultiple()`, `setMultiple()` a `deleteMultiple()`. Jak PSR-16 vyžaduje, odmítne prázdný klíč i klíč obsahující některý ze znaků `{}()/\@:`, třeba `user:1`. - -Opačný směr, tedy PSR-16 cache použitou jako úložiště pro Nette, pokrývá [#Psr16Storage]. - - -Vlastní úložiště -================ - -Úložiště je třída, která implementuje rozhraní `Nette\Caching\Storage` se čtyřmi metodami: - -- `read(string $key): mixed` vrátí uloženou hodnotu, nebo `null`, když položka neexistuje nebo vyexpirovala -- `write(string $key, mixed $data, array $dependencies): void` hodnotu uloží -- `remove(string $key): void` položku smaže -- `clean(array $conditions): void` smaže položky podle podmínek metody [clean() |expiration#Mazání položek]; prázdné pole znamená odstranit vyexpirované položky - -Závislosti přicházejí jako pole s klíči `Cache::*`, které už `Cache` zjednodušila: expirace je počet sekund od teď, ze souborů a konstant se staly callbacky a jiné položky jsou dané svými klíči v úložišti. Při čtení položky ověřte její callbacky pomocí `Nette\Caching\Cache::checkCallbacks()`. Pro závislost nebo podmínku, kterou vaše úložiště neumí, vyhoďte `Nette\NotSupportedException`. - -Další schopnosti přidávají volitelná rozhraní. `Nette\Caching\BulkReader` a `Nette\Caching\BulkWriter` umožní úložišti číst a zapisovat více položek najednou. `Nette\Caching\LockingReader` přidává metodu `readOrGenerate()`, která chrání před cache stampede: dostane generátor z `load()` jako funkci a musí ji spustit pod zámkem. Funkce si výsledek uloží sama, pomocí `write()` nebo `remove()` téhož úložiště, takže tyto metody musí fungovat, i když je zámek držený. - -Krátkým úplným příkladem je zdrojový kód [SQLiteStorage |https://github.com/nette/caching/blob/master/src/Caching/Storages/SQLiteStorage.php]. - -{{maintitle: Úložiště Nette Caching: soubory, Redis, Memcached, SQLite, PSR-16}} -{{description: Vyberte, kam cache z Nette ukládá data: soubory, Redis, Memcached, SQLite nebo libovolná PSR-16 cache. Porovnejte, co které umí, a nastavte ho.}} diff --git a/caching/cs/upgrading.texy b/caching/cs/upgrading.texy deleted file mode 100644 index 34325e258c..0000000000 --- a/caching/cs/upgrading.texy +++ /dev/null @@ -1,50 +0,0 @@ -Upgrade -******* - - -Upgrade na verzi 4.0 -==================== - -Minimální požadovaná verze PHP je 8.4. Značka `{cache}` vyžaduje Latte 3.1. - -.[tip] -Část práce za vás udělá [DressCode |tools:coding-standard#Novější PHP a Nette] s balíčkem `dresscode/upgrade-nette`: přepíše přejmenované třídy a konstanty a ohlásí, co přepsat nedokáže. Viz [Upgrade Nette |https://dresscode.run/cs/upgrading-nette]. - -Rozhraní `Nette\Caching\IStorage`, `Nette\Caching\IBulkReader` a `Nette\Caching\Storages\IJournal` byla odstraněna; použijte `Nette\Caching\Storage`, `Nette\Caching\BulkReader` a `Nette\Caching\Storages\Journal`. Odstraněny byly i aliasy služeb `cacheStorage` a `nette.cacheJournal`; použijte služby `cache.storage` a `cache.journal`, případně autowiring. - -Konstanty psané velkými písmeny, třeba `Cache::EXPIRE` nebo `Cache::TAGS`, a metoda `Cache::start()` nyní vyvolávají `E_USER_DEPRECATED`. Použijte `Cache::Expire`, `Cache::Tags` a `capture()`. Předávat closure jako data do `Cache::save()` je zastaralé, předejte ji jako generátor do `load()`. - -Závislosti lze zapsat novým objektem `Nette\Caching\Dependencies`, viz [Expirace a invalidace |expiration]. Pole s klíči `Cache::*` dál funguje, ale neznámý klíč nebo hodnota špatného typu nyní vyhodí výjimku. Typicky jde o prioritu zapsanou jako řetězec, třeba `'50'`, která musí být celé číslo. - -Metoda `Cache::clean()` přijímá podmínky jako pojmenované argumenty, třeba `$cache->clean(tags: ['article/10'])`; pole se kontroluje stejně jako závislosti. Několik věcí se chová jinak: - -- `clean()` bez jakékoliv podmínky vyhodí výjimku. Vyexpirované položky smažete voláním `clean([])` na úložišti. -- `all` u cache s jmenným prostorem smaže jen tento prostor a prostory z něj odvozené; dříve smazal celé úložiště. Pokud chcete smazat vše, zavolejte ho na cache bez jmenného prostoru, nebo zavolejte `$storage->clean([Cache::All => true])`. -- Mazání podle `Cache::Namespaces` smaže i prostory odvozené od uvedených pomocí `derive()`. - -Úložiště vyhodí `Nette\NotSupportedException` pro závislost nebo podmínku, kterou neumí, místo aby ji potichu ignorovalo. `SQLiteStorage` nepodporuje priority ani závislosti na souborech, konstantách, callbackech a jiných položkách; `MemcachedStorage` nepodporuje mazání podle jmenných prostorů. Viz přehled [úložišť |storages#Výběr úložiště]. - -`PsrCacheAdapter` nyní kontroluje klíče tak, jak vyžaduje PSR-16, takže odmítne klíče obsahující některý ze znaků `{}()/\@:`, třeba `user:1`. - -Pokud jste napsali vlastní úložiště, odstraňte metodu `lock()`, která už neexistuje, a deklarujte návratový typ `mixed` metody `read()`. Úložiště, které má chybějící položku nechat vygenerovat jen jednomu procesu, implementuje rozhraní `Nette\Caching\LockingReader`. Potomek `Cache`, který přepisuje `clean()`, musí deklarovat její nové parametry. - - -Upgrade na verzi 3.1 -==================== - -- metoda `Nette\Caching\Cache::start()` byla přejmenována na `capture()` - - -Upgrade na verzi 2.4 -==================== - -- třída `Nette\Caching\Storages\FileJournal` již není k dispozici - - -Upgrade na verzi 2.3 -==================== - -- prastará a zastaralá ArrayAccess syntaxe `$val = $cache[$key]` nebo `$cache[$key] = $val` vyvolá `E_USER_DEPRECATED`; použijte místo ní `$cache->load($key)` a `$cache->save($key, $val)` - -{{maintitle: Upgrade Nette Caching: změny v jednotlivých verzích}} -{{description: Co se změnilo v Nette Caching 4.0, 3.1 a starších verzích a jak upravit kód při přechodu na novější verzi.}} diff --git a/caching/de/@home.texy b/caching/de/@home.texy deleted file mode 100644 index 518f599a11..0000000000 --- a/caching/de/@home.texy +++ /dev/null @@ -1,502 +0,0 @@ -Nette Caching -************* - -<div class=perex> - -Der Cache beschleunigt Ihre Anwendung, indem er einmal aufwendig beschaffte Daten speichert und sie beim nächsten Mal schneller zur Verfügung stellt. Wir zeigen Ihnen: - -- wie Sie den Cache verwenden -- wie Sie das Storage wechseln -- wie Sie den Cache richtig invalidieren - -</div> - -Die Verwendung des Caches ist in Nette sehr einfach und deckt zugleich anspruchsvolle Anforderungen ab. Er ist auf Leistung und 100%ige Zuverlässigkeit ausgelegt. Für die gängigsten Storage-Backends sind bereits Adapter enthalten. Er unterstützt Invalidierung anhand von Tags, zeitlichen Ablauf, Schutz vor Cache Stampede und mehr. - - -Installation -============ - -Laden Sie das Paket mit [Composer|best-practices:composer] herunter und installieren Sie es: - -```shell -composer require nette/caching -``` - - -Grundlegende Verwendung -======================= - -Der Mittelpunkt der Arbeit mit dem Cache ist das Objekt [api:Nette\Caching\Cache]. Wir erzeugen eine Instanz davon und übergeben dem Konstruktor ein sogenanntes Storage. Das ist ein Objekt, das den Ort repräsentiert, an dem die Daten physisch abgelegt werden (Datenbank, Memcached, Dateien auf der Festplatte, ...). Das Storage lassen Sie sich üblicherweise per [Dependency Injection |dependency-injection:passing-dependencies] mit dem Typ `Nette\Caching\Storage` übergeben. Alles Wesentliche erfahren Sie im [Abschnitt Storages |#Storages]. - -.[warning] -In Version 3.0 hatte das Interface noch das Präfix `I`, der Name lautete also `Nette\Caching\IStorage`. Außerdem wurden die Konstanten der Klasse `Cache` in Großbuchstaben geschrieben, also etwa `Cache::EXPIRE` statt `Cache::Expire`. - -Für die folgenden Beispiele nehmen wir an, dass wir einen Alias `Cache` und in der Variable `$storage` ein Storage haben. - -```php -use Nette\Caching\Cache; - -$storage = /* ... */; // Instanz von Nette\Caching\Storage -``` - -Der Cache ist im Grunde ein *Key-Value-Store*, wir lesen und schreiben die Daten also unter Schlüsseln, ganz ähnlich wie bei assoziativen Arrays. Anwendungen bestehen aus einer Reihe unabhängiger Teile, und würden alle ein einziges Storage nutzen (stellen Sie sich ein einziges Verzeichnis auf der Festplatte vor), käme es früher oder später zu Kollisionen der Schlüssel. Das Nette Framework löst das Problem, indem es den gesamten Raum in Namensräume (also gedanklich Unterverzeichnisse) aufteilt. Jeder Teil des Programms arbeitet dann in seinem eigenen Raum mit einem eindeutigen Namen, und zu einer Kollision kann es nicht mehr kommen. - -Den Namen des Raums geben wir als zweiten Parameter des Konstruktors der Klasse `Cache` an: - -```php -$cache = new Cache($storage, 'Full Html Pages'); -``` - -Bei Bedarf lässt sich aus einer bestehenden Instanz mit der Methode `derive()` ein neuer Cache in einem verschachtelten Unterraum ableiten: - -```php -$subCache = $cache->derive('Images'); -``` - -Jetzt können wir mit dem Objekt `$cache` aus dem Cache lesen und in ihn schreiben. Für beides dient die Methode `load()`. Das erste Argument ist der Schlüssel, das zweite ein PHP-Callback, das aufgerufen wird, wenn der Schlüssel im Cache nicht gefunden wird. Das Callback erzeugt den Wert, gibt ihn zurück, und die Methode `load()` legt ihn im Cache ab: - -```php -$value = $cache->load($key, function () use ($key) { - $computedValue = /* ... */; // aufwendige Berechnung - return $computedValue; -}); -``` - -Wenn wir den zweiten Parameter weglassen (`$value = $cache->load($key)`), gibt `load()` `null` zurück, falls der Eintrag nicht im Cache liegt. - -.[tip] -Praktisch ist, dass sich beliebige serialisierbare Strukturen im Cache ablegen lassen, nicht nur Strings. Und dasselbe gilt sogar für die Schlüssel. - -Einen Eintrag löschen wir mit der Methode `remove()` aus dem Cache: - -```php -$cache->remove($key); -``` - -Einen Eintrag können Sie auch mit der Methode `$cache->save($key, $data, ?array $dependencies = null)` im Cache speichern. Bevorzugt wird jedoch der oben gezeigte Weg über `load()`. - - -Memoization -=========== - -Memoization bedeutet, das Ergebnis eines Funktions- oder Methodenaufrufs zu cachen, damit Sie es beim nächsten Mal verwenden können, ohne dasselbe immer wieder neu zu berechnen. - -Methoden und Funktionen lassen sich mit `call(callable $callback, ...$args)` memoisiert aufrufen: - -```php -$result = $cache->call('gethostbyaddr', $ip); -``` - -Die Funktion `gethostbyaddr()` wird so für jeden Parameter `$ip` nur einmal aufgerufen, beim nächsten Mal mit demselben `$ip` kommt der Wert bereits aus dem Cache. - -Es ist auch möglich, einen memoisierten Wrapper um eine Methode oder Funktion zu erzeugen, der sich erst später aufrufen lässt: - -```php -function factorial($num) -{ - return /* ... */; -} - -$memoizedFactorial = $cache->wrap('factorial'); - -$result = $memoizedFactorial(5); // berechnet es beim ersten Mal -$result = $memoizedFactorial(5); // beim zweiten Mal aus dem Cache -``` - - -Ablauf & Invalidierung -====================== - -Beim Ablegen im Cache muss die Frage geklärt werden, wann zuvor gespeicherte Daten ungültig werden. Das Nette Framework bietet einen Mechanismus, mit dem sich die Gültigkeit der Daten begrenzen oder die Daten gezielt löschen lassen (in der Terminologie des Frameworks "invalidieren"). - -Die Gültigkeit der Daten wird im Moment des Speicherns festgelegt, und zwar über den dritten Parameter der Methode `save()`, z. B.: - -```php -$cache->save($key, $value, [ - $cache::Expire => '20 minutes', -]); -``` - -Oder über den Parameter `$dependencies`, der dem Callback der Methode `load()` per Referenz übergeben wird, z. B.: - -```php -$value = $cache->load($key, function (&$dependencies) { - $dependencies[Cache::Expire] = '20 minutes'; - return /* ... */; -}); -``` - -Oder über den 3. Parameter der Methode `load()`, z. B.: - -```php -$value = $cache->load($key, function () { - return /* ... */; -}, [Cache::Expire => '20 minutes']); -``` - -In den folgenden Beispielen gehen wir von der zweiten Variante aus, also von der Existenz der Variable `$dependencies`. - - -Ablauf ------- - -Die einfachste Form des Ablaufs ist ein Zeitlimit. So legen wir Daten mit einer Gültigkeit von 20 Minuten im Cache ab: - -```php -// akzeptiert auch eine Anzahl von Sekunden oder einen UNIX-Timestamp -$dependencies[Cache::Expire] = '20 minutes'; -``` - -Wenn sich die Gültigkeitsdauer mit jedem Lesen verlängern soll, erreichen Sie das folgendermaßen; beachten Sie aber, dass der Overhead des Caches dadurch steigt: - -```php -$dependencies[Cache::Sliding] = true; -``` - -Praktisch ist die Möglichkeit, Daten in dem Moment ablaufen zu lassen, in dem sich eine Datei oder eine von mehreren Dateien ändert. Das lässt sich etwa nutzen, wenn Sie Daten im Cache ablegen, die aus der Verarbeitung dieser Dateien entstanden sind. Verwenden Sie absolute Pfade. - -```php -$dependencies[Cache::Files] = '/path/to/data.yaml'; -// oder -$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; -``` - -Wir können einen Eintrag im Cache in dem Moment ablaufen lassen, in dem ein anderer Eintrag (oder einer von mehreren anderen) abläuft. Das lässt sich nutzen, wenn wir etwa eine ganze HTML-Seite und unter anderen Schlüsseln ihre Fragmente im Cache ablegen. Sobald sich ein Fragment ändert, wird die ganze Seite invalidiert. Haben wir die Fragmente unter den Schlüsseln `frag1` und `frag2` gespeichert, verwenden wir: - -```php -$dependencies[Cache::Items] = ['frag1', 'frag2']; -``` - -Der Ablauf lässt sich auch über eigene Funktionen oder statische Methoden steuern, die bei jedem Lesen entscheiden, ob der Eintrag noch gültig ist. So können wir einen Eintrag etwa immer dann ablaufen lassen, wenn sich die PHP-Version ändert. Wir erstellen eine Funktion, die die aktuelle Version mit einem Parameter vergleicht, und fügen beim Speichern den Abhängigkeiten ein Array in der Form `[Funktionsname, ...Argumente]` hinzu: - -```php -function checkPhpVersion($ver): bool -{ - return $ver === PHP_VERSION_ID; -} - -$dependencies[Cache::Callbacks] = [ - ['checkPhpVersion', PHP_VERSION_ID] // ablaufen lassen, wenn checkPhpVersion(...) === false -]; -``` - -Alle diese Kriterien lassen sich selbstverständlich kombinieren. Der Cache läuft dann ab, wenn mindestens ein Kriterium nicht mehr erfüllt ist. - -```php -$dependencies[Cache::Expire] = '20 minutes'; -$dependencies[Cache::Files] = '/path/to/data.yaml'; -``` - - -Invalidierung mittels Tags --------------------------- - -Ein sehr nützliches Werkzeug zur Invalidierung sind sogenannte Tags. Jedem Eintrag im Cache können wir eine Liste von Tags zuweisen, also beliebige Strings. Nehmen wir etwa eine HTML-Seite mit einem Artikel und Kommentaren, die wir cachen wollen. Beim Speichern geben wir die Tags an: - -```php -$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"]; -``` - -Wechseln wir nun in die Administration. Dort finden wir ein Formular zum Bearbeiten des Artikels. Zusammen mit dem Speichern des Artikels in der Datenbank rufen wir die Methode `clean()` auf, die Einträge anhand des Tags aus dem Cache löscht: - -```php -$cache->clean([ - $cache::Tags => ["article/$articleId"], -]); -``` - -Ebenso vergessen wir dort, wo ein neuer Kommentar hinzugefügt (oder ein Kommentar bearbeitet) wird, nicht, den entsprechenden Tag zu invalidieren: - -```php -$cache->clean([ - $cache::Tags => ["comments/$articleId"], -]); -``` - -Was haben wir damit erreicht? Dass unser HTML-Cache immer dann invalidiert (gelöscht) wird, wenn sich der Artikel oder die Kommentare ändern. Wird der Artikel mit der ID = 10 bearbeitet, wird der Tag `article/10` zwangsweise invalidiert und die HTML-Seite, die diesen Tag trägt, aus dem Cache gelöscht. Dasselbe passiert beim Einfügen eines neuen Kommentars unter dem betreffenden Artikel. - -.[note] -Tags erfordern ein sogenanntes [#Journal]. - - -Invalidierung mittels Priorität -------------------------------- - -Einzelnen Einträgen im Cache können wir eine Priorität zuweisen, mit deren Hilfe sie sich löschen lassen, wenn der Cache etwa eine bestimmte Größe überschreitet: - -```php -$dependencies[Cache::Priority] = 50; -``` - -So löschen wir alle Einträge mit einer Priorität gleich oder kleiner als 100: - -```php -$cache->clean([ - $cache::Priority => 100, -]); -``` - -.[note] -Prioritäten erfordern ein sogenanntes [#Journal]. - - -Löschen des Caches ------------------- - -Der Parameter `Cache::All` löscht alles: - -```php -$cache->clean([ - $cache::All => true, -]); -``` - - -Massenlesen -=========== - -Für das massenhafte Lesen und Schreiben in den Cache dient die Methode `bulkLoad()`, der wir ein Array von Schlüsseln übergeben und ein Array von Werten erhalten: - -```php -$values = $cache->bulkLoad($keys); -``` - -Die Methode `bulkLoad()` funktioniert ähnlich wie `load()`, auch mit einem Callback als zweitem Parameter, dem der Schlüssel des erzeugten Eintrags übergeben wird: - -```php -$values = $cache->bulkLoad($keys, function ($key, &$dependencies) { - $computedValue = /* ... */; // aufwendige Berechnung - return $computedValue; -}); -``` - -Umgekehrt dient für das gleichzeitige Schreiben mehrerer Einträge die Methode `bulkSave()`, der wir ein Array von Paaren `Schlüssel => Wert` und optional Abhängigkeiten übergeben: - -```php -$cache->bulkSave([ - $key1 => $value1, - $key2 => $value2, -], [Cache::Expire => '20 minutes']); -``` - - -Verwendung mit PSR-16 .{data-version:3.3.1} -=========================================== - -Um Nette Cache mit dem Interface PSR-16 zu verwenden, können Sie den Adapter `PsrCacheAdapter` nutzen. Er ermöglicht eine reibungslose Integration zwischen Nette Cache und beliebigem Code oder beliebigen Bibliotheken, die eine PSR-16-kompatible Cache-Implementierung erwarten. - -```php -$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage); -``` - -Jetzt können Sie `$psrCache` als PSR-16-Cache verwenden: - -```php -$psrCache->set('key', 'value', 3600); // speichert den Wert für 1 Stunde -$value = $psrCache->get('key', 'default'); -``` - -Der Adapter unterstützt alle in PSR-16 definierten Methoden, einschließlich `getMultiple()`, `setMultiple()` und `deleteMultiple()`. - - -Caching der Ausgabe -=================== - -Sehr elegant lässt sich die Ausgabe abfangen und cachen: - -```php -if ($capture = $cache->capture($key)) { - - // echo ... wir geben Daten aus - - $capture->end(); // die Ausgabe im Cache speichern -} -``` - -Ist die Ausgabe bereits im Cache gespeichert, gibt die Methode `capture()` sie aus und liefert `null` zurück, der Block der `if`-Bedingung wird also übersprungen. Andernfalls beginnt sie, die Ausgabe abzufangen, und gibt ein Objekt `$capture` zurück, mit dem wir die ausgegebenen Daten am Ende über dessen Methode `end()` im Cache speichern. - -.[note] -In Version 3.0 hieß die Methode `$cache->start()`. - - -Caching in Latte -================ - -Caching in [Latte|latte:]-Templates ist sehr einfach, es genügt, den zu cachenden Teil des Templates mit den Tags `{cache}...{/cache}` zu umschließen. Der Cache wird automatisch in dem Moment invalidiert, in dem sich das Quell-Template ändert (einschließlich eventuell eingebundener Templates innerhalb des Cache-Blocks). Die Tags `{cache}` lassen sich ineinander verschachteln, und wird ein verschachtelter Block ungültig (etwa über einen Tag), wird auch der übergeordnete Block ungültig. - -Im Tag lassen sich die Schlüssel angeben, an die der Cache gebunden wird (hier die Variable `$id`), eine Ablaufzeit setzen und [Tags für die Invalidierung |#Invalidierung mittels Tags] festlegen. - -```latte -{cache $id, expire: '20 minutes', tags: [tag1, tag2]} - ... -{/cache} -``` - -Alle Angaben sind optional, wir müssen also weder die Ablaufzeit noch die Tags noch überhaupt die Schlüssel angeben. - -Die Verwendung des Caches lässt sich auch mit `if` an eine Bedingung knüpfen - der Inhalt wird dann nur gecacht, wenn die Bedingung erfüllt ist: - -```latte -{cache $id, if: !$form->isSubmitted()} - {$form} -{/cache} -``` - - -Storages -======== - -Ein Storage ist ein Objekt, das den Ort repräsentiert, an dem die Daten physisch abgelegt werden. Wir können eine Datenbank, einen Memcached-Server oder das am leichtesten verfügbare Storage nutzen: Dateien auf der Festplatte. - -|---------------------- -| Storage | Beschreibung -|---------------------- -| [#FileStorage] | Standard-Storage, speichert den Cache in Dateien auf der Festplatte. -| [#MemcachedStorage] | Nutzt einen `Memcached`-Server als Ablageort. -| [#MemoryStorage] | Daten liegen vorübergehend im Arbeitsspeicher (gehen am Ende des Requests verloren). -| [#SQLiteStorage] | Daten werden in einer SQLite-Datenbankdatei abgelegt. -| [#DevNullStorage] | Daten werden gar nicht gespeichert, nützlich zum Testen. - -Das Storage lassen Sie sich per [Dependency Injection |dependency-injection:passing-dependencies] mit dem Typ `Nette\Caching\Storage` übergeben. Als Standard-Storage stellt Nette ein `FileStorage`-Objekt bereit, das die Daten im Unterordner `cache` innerhalb des Verzeichnisses für [temporäre Dateien |application:bootstrapping#Temporäre Dateien] ablegt. - -Das Storage können Sie in der Konfiguration ändern: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - - -FileStorage ------------ - -Schreibt den Cache in Dateien auf der Festplatte. Das Storage `Nette\Caching\Storages\FileStorage` ist sehr gut auf Leistung optimiert und stellt vor allem die volle Atomizität der Operationen sicher. Was bedeutet das? Dass bei der Verwendung des Caches nicht passieren kann, dass wir eine Datei lesen, die ein anderer Thread noch nicht vollständig geschrieben hat, oder dass sie jemand "unter unseren Händen" löscht. Die Verwendung des Caches ist also völlig sicher. - -Dieses Storage hat außerdem eine wichtige eingebaute Funktion, die einen extremen Anstieg der CPU-Auslastung in dem Moment verhindert, in dem der Cache gelöscht wird oder noch nicht aufgewärmt (also noch nicht erzeugt) ist. Es handelt sich um die Vorbeugung gegen "Cache Stampede":https://en.wikipedia.org/wiki/Cache_stampede. Es kommt vor, dass zu einem Zeitpunkt eine größere Zahl gleichzeitiger Requests zusammentrifft, die dieselbe Sache aus dem Cache haben wollen (etwa das Ergebnis einer teuren SQL-Query), und weil sie nicht im Cache liegt, beginnen alle Prozesse dieselbe SQL-Query auszuführen. Die Auslastung vervielfacht sich dadurch, und es kann sogar passieren, dass kein Thread es schafft, im Zeitlimit zu antworten, der Cache nicht entsteht und die Anwendung zusammenbricht. Zum Glück funktioniert der Cache in Nette so, dass bei mehreren gleichzeitigen Requests auf einen Eintrag nur der erste Thread ihn erzeugt, die übrigen warten und nutzen anschließend das erzeugte Ergebnis. - -Beispiel für das Erzeugen eines `FileStorage`: - -```php -// das Storage ist das Verzeichnis '/path/to/temp' auf der Festplatte -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); -``` - - -MemcachedStorage ----------------- - -Der Server [Memcached |https://memcached.org] ist ein hochperformantes System zum Ablegen von Objekten im verteilten Arbeitsspeicher, dessen Adapter `Nette\Caching\Storages\MemcachedStorage` ist. In der Konfiguration geben wir die IP-Adresse und den Port an, falls er vom Standard 11211 abweicht. - -.[caution] -Erfordert die PHP-Erweiterung `memcached`. - -```neon -services: - cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5') -``` - - -MemoryStorage -------------- - -`Nette\Caching\Storages\MemoryStorage` ist ein Storage, das die Daten in einem PHP-Array ablegt und sie also mit dem Ende des Requests verliert. - - -SQLiteStorage -------------- - -Die Datenbank SQLite und der Adapter `Nette\Caching\Storages\SQLiteStorage` bieten eine Möglichkeit, den Cache in einer einzigen Datei auf der Festplatte abzulegen. In der Konfiguration geben wir den Pfad zu dieser Datei an. - -.[caution] -Erfordert die PHP-Erweiterungen `pdo` und `pdo_sqlite`. - -```neon -services: - cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db') -``` - - -DevNullStorage --------------- - -Eine besondere Implementierung eines Storage ist `Nette\Caching\Storages\DevNullStorage`, das in Wirklichkeit überhaupt keine Daten speichert. Es eignet sich damit zum Testen, wenn wir den Einfluss des Caches ausschalten wollen. - - -Verwendung des Caches im Code -============================= - -Bei der Verwendung des Caches im Code haben wir zwei Möglichkeiten. Die erste besteht darin, uns per [Dependency Injection |dependency-injection:passing-dependencies] das Storage übergeben zu lassen und das Objekt `Cache` zu erzeugen: - -```php -use Nette; - -class ClassOne -{ - private Nette\Caching\Cache $cache; - - public function __construct(Nette\Caching\Storage $storage) - { - $this->cache = new Nette\Caching\Cache($storage, 'my-namespace'); - } -} -``` - -Die zweite Möglichkeit ist, uns gleich das Objekt `Cache` übergeben zu lassen: - -```php -class ClassTwo -{ - public function __construct( - private Nette\Caching\Cache $cache, - ) { - } -} -``` - -Das Objekt `Cache` wird dann direkt in der Konfiguration auf diese Weise erzeugt: - -```neon -services: - - ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') ) -``` - - -Journal -======= - -Nette legt Tags und Prioritäten in einem sogenannten Journal ab. Standardmäßig wird dafür SQLite und die Datei `journal.s3db` verwendet, und **die PHP-Erweiterungen `pdo` und `pdo_sqlite` sind erforderlich.** - -Das Journal können Sie in der Konfiguration ändern: - -```neon -services: - cache.journal: MyJournal -``` - - -DI-Services -=========== - -Diese Services werden dem DI-Container hinzugefügt: - -| Name | Typ | Beschreibung -|---------------------------------------------------------- -| `cache.journal` | [api:Nette\Caching\Storages\Journal] | Storage des Cache-Journals -| `cache.storage` | [api:Nette\Caching\Storage] | Primäres Cache-Storage - - -Cache abschalten -================ - -Eine der Möglichkeiten, den Cache in der Anwendung abzuschalten, ist, als Storage [#DevNullStorage] zu setzen: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - -Diese Einstellung hat keinen Einfluss auf das Caching der Templates in Latte oder des DI-Containers, denn diese Bibliotheken nutzen die Services von `nette/caching` nicht und verwalten ihren Cache eigenständig. Ihr Cache [muss im Entwicklungsmodus ohnehin nicht |nette:troubleshooting#Wie schaltet man den Cache während der Entwicklung aus?] abgeschaltet werden. - - -Wenn Sie auf eine neuere Version aktualisieren, sehen Sie sich die Seite [Upgrade |upgrading] an. diff --git a/caching/de/@left-menu.texy b/caching/de/@left-menu.texy deleted file mode 100644 index 15db5d0a89..0000000000 --- a/caching/de/@left-menu.texy +++ /dev/null @@ -1,13 +0,0 @@ -Nette Caching -************* -- [Übersicht |@home] -- [Upgrade|upgrading] - - -Weiterführende Lektüre -********************** -- [Nette Dokumentation |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Best Practices |best-practices:] -- [Fehlerbehebung |nette:troubleshooting] diff --git a/caching/de/@meta.texy b/caching/de/@meta.texy deleted file mode 100644 index b3b806b2ca..0000000000 --- a/caching/de/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokumentation}} diff --git a/caching/de/upgrading.texy b/caching/de/upgrading.texy deleted file mode 100644 index b9955c6376..0000000000 --- a/caching/de/upgrading.texy +++ /dev/null @@ -1,20 +0,0 @@ -Upgrade -******* - - -Upgrade auf Version 3.1 -======================= - -- die Methode `Nette\Caching\Cache::start()` wurde in `capture()` umbenannt - - -Upgrade auf Version 2.4 -======================= - -- die Klasse `Nette\Caching\Storages\FileJournal` steht nicht mehr zur Verfügung - - -Upgrade auf Version 2.3 -======================= - -- die uralte und veraltete ArrayAccess-Syntax `$val = $cache[$key]` bzw. `$cache[$key] = $val` löst `E_USER_DEPRECATED` aus; verwenden Sie stattdessen `$cache->load($key)` und `$cache->save($key, $val)` diff --git a/caching/en/@home.texy b/caching/en/@home.texy deleted file mode 100644 index 844684113b..0000000000 --- a/caching/en/@home.texy +++ /dev/null @@ -1,258 +0,0 @@ -Nette Caching -************* - -<div class=perex> - -Does your application compute the same thing over and over, such as turning an article into HTML, running a slow database query or calling a remote API? Nette Caching remembers the result and hands it back next time in a fraction of the time: - -- One method reads the value from the cache, or computes and stores it when it is missing -- Data expires after a while, or when a file, another item or your own condition changes -- Tags delete exactly the items related to the article you just edited -- A crowd of requests for a missing item does not overload the server -- Stores data in files, Redis, Memcached, SQLite or any PSR-16 cache -- Caches parts of Latte templates - -</div> - - -Installation -============ - -Download and install the package using [Composer|best-practices:composer]: - -```shell -composer require nette/caching -``` - -It requires PHP version 8.4 and supports PHP up to 8.6. - - -Your First Cache -================ - -The easiest way to show it is with an example. Let's say you run a blog. Every time someone opens an article, your code loads it from the database and turns its text into HTML. That takes a while, and the result is the same for every visitor until you edit the article. A perfect candidate for the cache. - -We'll write it as a short script. First, we create two objects: - -```php -use Nette\Caching\Cache; -use Nette\Caching\Storages\FileStorage; - -require __DIR__ . '/vendor/autoload.php'; - -$storage = new FileStorage(__DIR__ . '/temp'); -$cache = new Cache($storage, 'articles'); -``` - -The **storage** decides where the data physically lives. `FileStorage` keeps it in files in the `temp` directory, which must exist. The **cache** is the object you will work with. Its second argument, `'articles'`, is a namespace: it keeps the articles apart from whatever else the application caches, so two parts of the program can use the same key without clashing. - -Now the important part. The cache works like an associative array: you read and write values under keys. The method `load()` does both at once: - -```php -$html = $cache->load("article/$id", function () use ($id) { - return renderArticle($id); // loads the article from the database and turns it into HTML -}); -``` - -Here `$id` is the ID of the article being displayed. The first argument of `load()` is the key, the second is a function called the **generator**. When the key is not in the cache yet, `load()` calls the generator, stores what it returns and returns it to you. Next time, it finds the value in the cache and returns it right away, without calling the generator at all. - -Notice that you never check whether the value is already cached. You only say how to compute it. - -And what if a hundred visitors open the same article at the moment it is not cached yet, for example right after a deployment? Without protection, all hundred requests would render it at once and could overload the server. This is called a [cache stampede |https://en.wikipedia.org/wiki/Cache_stampede]. `FileStorage` prevents it: only the first request runs the generator, the others wait for its result. - -.[tip] -You can cache any value PHP can serialize, not just strings: arrays, objects, numbers. The same goes for keys. - -To read a value without generating it, call `load()` with the key only; it returns `null` when the item is not in the cache. The method `remove()` deletes an item: - -```php -$cache->remove("article/$id"); -``` - - -How Long Is the Data Valid? -=========================== - -Our cached article now never changes. But what if you fix a typo in it? Visitors would keep seeing the old version. You need to tell the cache when an item stops being valid. The simplest way is a time limit: - -```php -use Nette\Caching\Dependencies; - -$html = $cache->load("article/$id", function (Dependencies $dp) use ($id) { - $dp->expire = '1 hour'; - return renderArticle($id); -}); -``` - -The generator now takes a parameter of the type `Dependencies`, and `load()` passes it an object in which the generator describes when the item expires. It is called dependencies because the validity of the item depends on them. Here it is one hour. - -Time is just one option. An item can also expire when a file changes, when another item in the cache changes, or when your own condition says so. All of that is described on the page [Expiration and Invalidation |expiration]. - - -Deleting Exactly What Changed -============================= - -A time limit is a compromise: set it short, and the cache helps little; set it long, and visitors see stale data. It is better to delete the article from the cache exactly when you edit it. That is what **tags** are for. You attach them to an item as arbitrary strings: - -```php -$html = $cache->load("article/$id", function (Dependencies $dp) use ($id) { - $dp->tags = ["article/$id", "comments/$id"]; - return renderArticle($id); -}); -``` - -The page shows the article together with its comments, so we gave it two tags. Then, in the administration, right after you save the edited article to the database, you delete everything tagged with it: - -```php -$cache->clean(tags: ["article/$id"]); -``` - -And the same when someone adds a comment, this time with the tag `comments/$id`. From now on, the cached page disappears exactly when its article or its comments change, and the next visitor gets a freshly rendered one. - -`FileStorage` remembers which item carries which tag in a so-called journal, a small SQLite database. In a Nette application it is set up for you; in our script, we create it and pass it to the storage: - -```php -use Nette\Caching\Storages\SQLiteJournal; - -$journal = new SQLiteJournal(__DIR__ . '/temp/journal.s3db'); -$storage = new FileStorage(__DIR__ . '/temp', $journal); -``` - -Besides tags, you can delete items by priority or a whole namespace at once. See [Expiration and Invalidation |expiration#Deleting Items]. - - -Namespaces -========== - -We already met the namespace `'articles'` in the constructor of the cache. Each part of the application should have its own, so their keys never collide. The comments could use their own cache, for example: - -```php -$commentCache = new Cache($storage, 'comments'); -``` - -The method `derive()` creates a cache in a nested namespace: `$cache->derive('previews')` gives the namespace `previews` inside `articles`. And when you want to throw away everything in a namespace, including the nested ones, call: - -```php -$cache->clean(all: true); -``` - -Other namespaces stay untouched. Only a cache created without a namespace, `new Cache($storage)`, deletes the entire storage this way. - - -Memoization -=========== - -Sometimes you want to cache the result of a function call. In the comment moderation, for example, you show the host name of each author's IP address. Finding it out with `gethostbyaddr()` asks a DNS server and can take seconds. The method `call()` calls the function only once for each argument and then returns the result from the cache: - -```php -$host = $cache->call('gethostbyaddr', $ip); -``` - -Here `$ip` is the IP address of a comment's author. The method `wrap()` does the same, but instead of calling the function right away, it returns a new function you can call later, as many times as you like: - -```php -$getHost = $cache->wrap('gethostbyaddr'); - -$host = $getHost($ip); // asks the DNS server the first time -$host = $getHost($ip); // returns the cached result the second time -``` - - -Caching Part of a Page -====================== - -Not everything you want to cache is a return value. Sometimes it is a block of HTML that a piece of code prints. The method `capture()` catches it: - -```php -if ($capture = $cache->capture('sidebar')) { - // echo ... prints the sidebar with the latest articles - $capture->end(); // stores the printed output in the cache -} -``` - -When the output is already cached, `capture()` prints it and returns `null`, so the block inside `if` does not run at all. Otherwise it starts capturing the output and returns an object whose method `end()` stores everything printed in the meantime. You can pass it dependencies, for example `$capture->end(new Dependencies(tags: ['articles']))`. - - -Caching in Latte -================ - -In [Latte|latte:] templates, it is even easier. Wrap the part you want to cache in the tags `{cache}...{/cache}`: - -```latte -{cache expire: '20 minutes', tags: [articles]} - <h2>Latest Articles</h2> - ... -{/cache} -``` - -The block is invalidated automatically whenever the template changes, including any templates included inside the block. Blocks can be nested; when a nested block is invalidated, its parent block is invalidated too. - -All the parameters are optional. Without `expire`, the block expires after 7 days. If the content depends on a variable, list it at the start, and each value gets its own cached copy: `{cache $article->id}`. And the parameter `if` decides whether to cache at all: - -```latte -{cache $id, if: !$form->isSubmitted()} - {$form} -{/cache} -``` - - -Where the Data Is Stored -======================== - -So far, we have used `FileStorage`, which needs nothing but a directory. Thanks to atomic file operations, it is safe even when many requests read and write at once. But it is just one of several storages: - -|---------------------- -| Storage | Where the data lives -|---------------------- -| `FileStorage` | in files on disk; the default storage -| `RedisStorage` | on a Redis server -| `MemcachedStorage` | on a Memcached server -| `SQLiteStorage` | in an SQLite database file -| `MemoryStorage` | in memory, only until the request ends -| `Psr16Storage` | in any PSR-16 cache -| `DevNullStorage` | nowhere; for tests and for turning the cache off - -You can switch the storage without touching the code that uses the cache. Which one to choose, what each supports and how to configure it, including turning the cache off, is described on the page [Storages |storages]. - - -Cache in a Nette Application -============================ - -In a Nette application, you do not create the storage yourself. Nette provides a `FileStorage` that stores data in the `cache` subdirectory of the directory for [temporary files |application:bootstrapping#Temporary Files], together with a journal. You get it via [dependency injection |dependency-injection:passing-dependencies] by asking for the type `Nette\Caching\Storage`, and create your cache in the constructor: - -```php -use Nette\Caching\Cache; -use Nette\Caching\Storage; - -class ArticleFacade -{ - private Cache $cache; - - - public function __construct(Storage $storage) - { - $this->cache = new Cache($storage, 'articles'); - } -} -``` - -The rest is the same as in our script. Alternatively, you can have the `Cache` object itself passed to the constructor and define it in the configuration: - -```neon -services: - - ArticleFacade( Nette\Caching\Cache(namespace: 'articles') ) -``` - -.[note] -The caches of Latte templates and of the DI container are separate and do not use this storage. They [do not need to be turned off |nette:troubleshooting#How to Disable Cache During Development] during development. - - -What's Next -=========== - -- [Expiration and Invalidation |expiration] - every way to tell the cache when data expires, deleting by tags, priority and namespace -- [Storages |storages] - choosing and configuring a storage, PSR-16 in both directions, writing your own storage -- [Upgrading |upgrading] - what changed in version 4.0 - -{{maintitle: PHP Caching with Nette: Storages, Expiration, Invalidation}} -{{description: Speed up your application by storing expensive results for later. Use the Nette cache in PHP and Latte, change storages and invalidate data correctly.}} diff --git a/caching/en/@left-menu.texy b/caching/en/@left-menu.texy deleted file mode 100644 index aa44a28806..0000000000 --- a/caching/en/@left-menu.texy +++ /dev/null @@ -1,15 +0,0 @@ -Nette Caching -************* -- [Overview |@home] -- [Expiration and Invalidation |expiration] -- [Storages |storages] -- [Upgrading] - - -Further Reading -*************** -- [Nette Documentation |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Best practices |best-practices:] -- [Troubleshooting |nette:troubleshooting] diff --git a/caching/en/@meta.texy b/caching/en/@meta.texy deleted file mode 100644 index 42471908b0..0000000000 --- a/caching/en/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Documentation}} diff --git a/caching/en/expiration.texy b/caching/en/expiration.texy deleted file mode 100644 index 65faf35e11..0000000000 --- a/caching/en/expiration.texy +++ /dev/null @@ -1,241 +0,0 @@ -Expiration and Invalidation -*************************** - -<div class=perex> - -A cache is only useful as long as it does not serve outdated data. This page shows every way to tell Nette Caching when an item stops being valid: - -- after a period of time, or when nobody has read it for a while -- when a file, a PHP constant or another item in the cache changes -- when your own function says so -- or right away, when you delete it by tag, priority or namespace - -</div> - -The examples continue the blog from the [overview |@home]: `$cache` is a cache in the namespace `articles`, `$id` is the ID of an article, and `renderArticle()` stands for the expensive work of loading an article and turning it into HTML. - - -Setting Dependencies -==================== - -**Dependencies** describe when an item expires. They are an object `Nette\Caching\Dependencies`. The most convenient way is to let the generator fill it in. Declare its parameter with the type `Dependencies`, and `load()` passes it an empty object: - -```php -use Nette\Caching\Dependencies; - -$html = $cache->load("article/$id", function (Dependencies $dp) use ($id) { - $dp->expire = '1 hour'; - return renderArticle($id); -}); -``` - -The advantage is that the generator can decide based on what it computed. An article that is still being drafted can expire sooner than a published one, for example. - -When the dependencies are known in advance, you can pass them as the third argument of `load()` instead, or of `save()`, which stores a value directly without a generator. The constructor takes all the properties as named arguments: - -```php -$html = $cache->load("article/$id", fn() => renderArticle($id), new Dependencies(expire: '1 hour')); - -$cache->save("article/$id", $html, new Dependencies(expire: '1 hour', tags: ["article/$id"])); -``` - -Prefer `load()` with a generator to `save()`. When several requests ask for a missing item at once, `FileStorage` and `RedisStorage` let only one of them run the generator; with `save()`, they would all compute the value. - -Dependencies can also be written as an array with `Cache::*` keys, such as `[Cache::Expire => '1 hour', Cache::Tags => ["article/$id"]]`. A generator whose parameter is not typed `Dependencies` receives this array by reference and adds to it: `function (&$dependencies)`. An unknown key or a value of a wrong type in the array throws an exception, so a typo such as `'expir'` does not silently leave the item without expiration. - - -Expiration by Time -================== - -The property `expire` sets when the item expires. It accepts a relative time as a string, a number of seconds, a UNIX timestamp or a `DateTimeInterface` object: - -```php -$dp->expire = '20 minutes'; -$dp->expire = 3600; // one hour -$dp->expire = new DateTimeImmutable('tomorrow'); -``` - -With **sliding expiration**, every read moves the expiration further. An article that visitors keep reading then stays in the cache, while one nobody opens for 20 minutes is dropped: - -```php -$dp->expire = '20 minutes'; -$dp->sliding = true; -``` - -Keep in mind that every read then also updates the item in the storage, which adds some overhead. - - -When a File Changes -=================== - -Data computed from a file should expire when the file changes. Let's say the blog reads its list of categories from the file `categories.neon`. The cached list then depends on that file: - -```php -$categories = $cache->load('categories', function (Dependencies $dp) { - $dp->files[] = __DIR__ . '/categories.neon'; - return loadCategories(); // reads and processes categories.neon -}); -``` - -As soon as you edit `categories.neon`, the cache notices on the next read and calls the generator again. Use absolute paths. An item can depend on several files: `$dp->files = [$file1, $file2]`. - - -When a Constant Changes -======================= - -Similarly, an item can expire when the value of a PHP constant changes. That is handy for a version number you set at deployment, for example, so that every new version starts with a fresh cache: - -```php -$dp->constants[] = 'APP_VERSION'; -``` - - -When Another Item Changes -========================= - -An item can depend on other items in the cache. Let's say the article and its comments are cached separately, under the keys `article/$id` and `comments/$id`, and the whole page put together from them is cached as well. The page must expire whenever one of its parts does: - -```php -$page = $cache->load("page/$id", function (Dependencies $dp) use ($id) { - $dp->items = ["article/$id", "comments/$id"]; - return renderPage($id); // puts together the article and its comments -}); -``` - -When the comments are saved again or removed, the page expires as well. - - -Custom Condition -================ - -When none of the above fits, write your own function. It is called on every read with the arguments you stored, and when it returns `false`, the item expires. Let's say the rendered HTML must not survive an upgrade of PHP: - -```php -function checkPhpVersion(int $version): bool -{ - return $version === PHP_VERSION_ID; -} - -$dp->callbacks[] = ['checkPhpVersion', PHP_VERSION_ID]; -``` - -Each entry is an array: the function, or a static method written as `[ClassName::class, 'method']`, followed by its arguments. The function is called on every read, so it should be fast. - - -Combining Conditions -==================== - -All these conditions can be combined. The item expires as soon as at least one of them is no longer met: - -```php -$dp->expire = '1 day'; -$dp->files[] = __DIR__ . '/categories.neon'; -$dp->constants[] = 'APP_VERSION'; -``` - - -Tags -==== - -Waiting for expiration is a compromise. It is better to delete an item exactly when the data it was made from changes. **Tags** let you do that. They are arbitrary strings you attach to an item: - -```php -$html = $cache->load("article/$id", function (Dependencies $dp) use ($id) { - $dp->tags = ["article/$id", "comments/$id"]; - return renderArticle($id); -}); -``` - -In the administration, right after saving an edited article to the database, you delete everything tagged with it: - -```php -$cache->clean(tags: ["article/$id"]); -``` - -The same goes for adding a comment, this time with the tag `comments/$id`. Notice that the code deleting the items does not need to know their keys, or even how many there are. The article may sit in the cache as a whole page, as a preview on the homepage and in the RSS feed; one call removes them all. - -Tags work across namespaces: `clean(tags: [...])` deletes the matching items in every namespace of the storage, not just in the namespace of `$cache`. - -.[note] -`FileStorage`, `MemcachedStorage` and `Psr16Storage` need a [journal |storages#Journal] for tags; the other storages keep them themselves. - - -Priority -======== - -Each item can be given a priority, a number saying how important it is. When the cache grows too big, you can then delete the less important items first. Let's say previews of old articles are less valuable than the homepage: - -```php -$dp->priority = 10; -``` - -This deletes every item with a priority of 50 or less: - -```php -$cache->clean(priority: 50); -``` - -Items without a priority are never deleted this way. - -.[note] -`FileStorage`, `MemcachedStorage` and `Psr16Storage` need a [journal |storages#Journal] for priorities, `RedisStorage` and `MemoryStorage` keep them themselves, and `SQLiteStorage` does not support them. - - -Deleting Items -============== - -The method `clean()` deletes items by conditions, which you pass as named arguments: - -| Argument | Deletes -|---------------------- -| `tags: [...]` | items with any of the tags -| `priority: 50` | items with a priority of 50 or less -| `namespaces: [...]` | all items in the listed namespaces, including the namespaces derived from them -| `all: true` | everything in the namespace of this cache, including the namespaces derived from it - -The conditions can be combined, and an item matching any of them is deleted: - -```php -$cache->clean(tags: ["article/$id"], priority: 50); -``` - -Let's take a closer look at `all`. It deletes the whole namespace of the cache together with the namespaces created from it by `derive()`, and leaves other namespaces untouched. Only a cache created without a namespace, `new Cache($storage)`, deletes the entire storage this way. - -The conditions can also be written as an array with `Cache::*` keys, such as `$cache->clean([Cache::Tags => ["article/$id"]])`. Just like with dependencies, an unknown key or a value of a wrong type throws an exception. - -Calling `clean()` without any condition throws an exception too, because it is not clear what it should delete. Removing items that have already expired is a matter of the storage: call `$storage->clean([])`. Most storages do it on their own anyway, see [Storages |storages]. - -To delete a single item, use `$cache->remove("article/$id")`. - - -What Each Storage Supports -========================== - -Not every storage can honor every kind of dependency. A storage that cannot honor one throws `Nette\NotSupportedException` instead of silently ignoring it, so you find out right away, not from stale data. `SQLiteStorage`, for example, does not support priorities or dependencies on files. The only exception is `MemoryStorage`, which ignores what it cannot do. The complete overview is in the table on the page [Storages |storages#Choosing a Storage]. - - -Bulk Reading and Writing -======================== - -The homepage of the blog shows previews of ten articles. Reading them from the cache one by one means ten round trips to the storage. The method `bulkLoad()` reads them all at once: you pass it the array `$ids` with their IDs as keys, and it returns an array of values indexed by those keys: - -```php -$previews = $cache->bulkLoad($ids, function ($id, Dependencies $dp) { - $dp->tags = ["article/$id"]; - return renderPreview($id); // renders a short preview of the article -}); -``` - -The generator is called only for the previews that are missing, and it receives the key followed by the dependencies. Without a generator, `bulkLoad($ids)` returns `null` for the missing items. - -The method `bulkSave()` stores several items at once, all with the same dependencies: - -```php -$cache->bulkSave([ - "preview/$id1" => $preview1, - "preview/$id2" => $preview2, -], new Dependencies(expire: '1 hour')); -``` - -{{maintitle: Nette Caching: Expiration, Tags and Cache Invalidation}} -{{description: Make sure the cache never serves outdated data. Expire items by time, file or constant change, and delete them by tags, priority or namespace.}} diff --git a/caching/en/storages.texy b/caching/en/storages.texy deleted file mode 100644 index a9ad5cc3ab..0000000000 --- a/caching/en/storages.texy +++ /dev/null @@ -1,236 +0,0 @@ -Storages -******** - -<div class=perex> - -The storage decides where cached data physically lives: in files on disk, on a Redis or Memcached server, in an SQLite database or in the PSR-16 cache of another framework. You can switch it without touching the code that uses the cache. This page helps you: - -- choose the right storage and see what each one supports -- configure it in a Nette application, or turn the cache off -- use a Nette storage as a PSR-16 cache, and the other way round -- write a storage of your own - -</div> - - -Choosing a Storage -================== - -If you are not sure, keep the default `FileStorage`. It needs nothing but a directory, it is safe when many requests read and write at once, and it supports every kind of dependency. - -Reach for `RedisStorage` when your application runs on several servers. With files, each server would have its own cache, and deleting an article by its tag on one server would leave the stale copies on the others. A Redis server is shared by all of them. - -The storages differ in which [dependencies |expiration] they can honor: - -|------------------------------------------------------------------------------------------------- -| Storage | Tags, priorities | Files, constants, callbacks | Other items | Sliding | Cleaning namespaces | Stampede protection -|------------------------------------------------------------------------------------------------- -| [#FileStorage] | with [#journal] | yes | yes | yes | yes | yes -| [#RedisStorage] | yes | yes | no | yes | yes | yes -| [#MemcachedStorage] | with [#journal] | yes | no | yes | no | no -| [#SQLiteStorage] | tags only | no | no | yes | yes | no -| [#MemoryStorage] | yes | ignored | ignored | yes | yes | no -| [#Psr16Storage] | with [#journal] | yes | no | no | no | no -| [#DevNullStorage] | stores nothing | | | | | - -A storage that cannot honor a dependency throws `Nette\NotSupportedException` instead of silently ignoring it, so you find out right away, not from stale data. The only exception is `MemoryStorage`, which ignores what it cannot do. - -Stampede protection means that when many requests ask for a missing item at once, only the first one runs the generator of `load()` and the others wait for its result. Without it, all of them compute the value at the same time. - - -Configuration -============= - -In a Nette application, the DI container provides a `FileStorage` that stores data in the `cache` subdirectory of the directory for [temporary files |application:bootstrapping#Temporary Files], together with a journal. You get it via [dependency injection |dependency-injection:passing-dependencies] by asking for the type `Nette\Caching\Storage`. - -To use another storage, replace the service `cache.storage` in the configuration: - -```neon -services: - cache.storage: Nette\Caching\Storages\RedisStorage('10.0.0.5') -``` - -The container holds these two services: - -| Name | Type | Description -|---------------------------------------------------------- -| `cache.storage` | [api:Nette\Caching\Storage] | the storage -| `cache.journal` | [api:Nette\Caching\Storages\Journal] | the [#journal] for tags and priorities - - -Turning Off the Cache ---------------------- - -To turn the cache off, for example in tests, use [#DevNullStorage], which stores nothing: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - -This does not affect the caches of Latte templates and of the DI container, which are separate and do not use this storage. They [do not need to be turned off |nette:troubleshooting#How to Disable Cache During Development] during development either. - - -FileStorage -=========== - -`Nette\Caching\Storages\FileStorage` writes each item into a file in the given directory, which must exist. Tags and priorities need a [#journal]: - -```php -$journal = new Nette\Caching\Storages\SQLiteJournal('/path/to/temp/journal.s3db'); -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp', $journal); -``` - -All operations are atomic. You can never read a file another request has not finished writing yet, and nobody can delete it while you are reading it. That makes the storage safe even under heavy load. - -It also protects against the [cache stampede |https://en.wikipedia.org/wiki/Cache_stampede]. When many requests ask for a missing item at once, for example right after a deployment, the first one locks the file and runs the generator. The others wait for the lock and then read the fresh value instead of computing it again. - -Expired files are removed gradually. Now and then, the constructor spends at most 20 ms deleting them, so even a large cache does not noticeably slow down a request. To remove all expired files at once, call `$storage->clean([])`. - -Data is stored with PHP's `serialize()`. If you prefer another format, create a subclass and override the methods `serializeData()` and `unserializeData()`. - - -RedisStorage -============ - -`Nette\Caching\Storages\RedisStorage` stores data on a [Redis |https://redis.io] server. It keeps tags and priorities itself, so it needs no journal, and it protects against the cache stampede just like `FileStorage`. - -.[caution] -Requires the `redis` PHP extension. - -```neon -services: - cache.storage: Nette\Caching\Storages\RedisStorage('10.0.0.5') -``` - -The constructor takes the host and the port, 6379 by default. If you already have a connected `Redis` object, pass it instead of the host. - -All keys get the prefix `nette.cache:`, so the database can be shared with other data; the parameter `prefix` changes it. An empty prefix means that the whole database belongs to the cache, and clearing the cache then empties the database. - -While one process generates a missing item, the others wait for it, at most for 30 seconds by default (the parameter `lockTimeout`). A generator that takes longer only means that another process computes the same value too, never that a wrong value is stored. - -The storage does not support the dependency on other items. - - -MemcachedStorage -================ - -`Nette\Caching\Storages\MemcachedStorage` stores data on a [Memcached |https://memcached.org] server. The constructor takes the host and the port, 11211 by default. Tags and priorities need a [#journal]. - -.[caution] -Requires the `memcached` PHP extension. - -```neon -services: - cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5') -``` - -Memcached cannot list its keys, so the storage cannot clean namespaces. It does not support the dependency on other items either. Clearing the whole cache empties the entire Memcached server, including the data of other applications. - -When Memcached refuses to store an item, typically one larger than its size limit, the storage triggers a warning. - - -SQLiteStorage -============= - -`Nette\Caching\Storages\SQLiteStorage` keeps the whole cache in a single SQLite database file. It stores tags in its own table, so it needs no journal. - -.[caution] -Requires the `pdo` and `pdo_sqlite` PHP extensions. - -```neon -services: - cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db') -``` - -The database runs in the WAL mode, in which reading does not wait for writing. SQLite then creates two more files next to the database, so PHP must be able to write to its directory. - -The storage supports expiration, sliding expiration, tags and cleaning namespaces. It does not support priorities, nor dependencies on files, constants, callbacks and other items. - - -MemoryStorage -============= - -`Nette\Caching\Storages\MemoryStorage` keeps data in a PHP array, so it is lost when the request ends. It is handy in tests and for caching within a single request. - -It honors expiration, sliding expiration, tags, priorities and cleaning namespaces. Dependencies on files, constants, callbacks and other items are silently ignored. Keep that in mind when you use it in tests instead of a real storage: an item that would expire there stays here. - - -Psr16Storage -============ - -`Nette\Bridges\Psr\Psr16Storage` lets you use any [PSR-16 |https://www.php-fig.org/psr/psr-16/] cache as a Nette storage, for example the cache of another framework your application already uses. Tags and priorities need a [#journal]: - -```php -// $simpleCache is any Psr\SimpleCache\CacheInterface -$storage = new Nette\Bridges\Psr\Psr16Storage($simpleCache, journal: $journal); -``` - -PSR-16 allows only some characters in keys, so the storage turns every key into a hash. The parameter `prefix`, at most 32 letters, digits, underscores and dots, is put in front of each of them. - -PSR-16 can neither extend the validity of an item nor list keys, so the storage does not support sliding expiration and cleaning namespaces. It does not support the dependency on other items either. Clearing the whole cache calls `clear()` on the PSR-16 cache, so it also removes the data of anyone else sharing it, whatever the prefix. - - -DevNullStorage -============== - -`Nette\Caching\Storages\DevNullStorage` stores nothing, so every `load()` calls the generator. Use it to [turn the cache off |#Turning Off the Cache]. - - -Journal -======= - -`FileStorage`, `MemcachedStorage` and `Psr16Storage` cannot search their items by tag or priority, so they keep a separate index, the so-called journal. The built-in `Nette\Caching\Storages\SQLiteJournal` stores it in an SQLite database: - -```php -$journal = new Nette\Caching\Storages\SQLiteJournal('/path/to/temp/journal.s3db'); -``` - -.[caution] -Requires the `pdo` and `pdo_sqlite` PHP extensions. - -In a Nette application, the journal is created for you as the service `cache.journal` in the file `journal.s3db` and passed to the storage automatically. Your own implementation of the interface `Nette\Caching\Storages\Journal` can replace it: - -```neon -services: - cache.journal: MyJournal -``` - -Without a journal, these storages work too, but storing an item with tags or a priority throws an exception. - - -Using a Storage as a PSR-16 Cache -================================= - -Some libraries expect a [PSR-16 |https://www.php-fig.org/psr/psr-16/] cache. The adapter `Nette\Bridges\Psr\PsrCacheAdapter` gives them one on top of any Nette storage: - -```php -$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage); - -$psrCache->set('key', 'value', 3600); // stores the value for 1 hour -$value = $psrCache->get('key', 'default'); -``` - -The adapter supports all methods of PSR-16, including `getMultiple()`, `setMultiple()` and `deleteMultiple()`. As PSR-16 requires, it rejects an empty key and a key containing any of the characters `{}()/\@:`, such as `user:1`. - -The opposite direction, a PSR-16 cache used as a Nette storage, is covered by [#Psr16Storage]. - - -Writing Your Own Storage -======================== - -A storage is a class implementing the interface `Nette\Caching\Storage` with four methods: - -- `read(string $key): mixed` returns the stored value, or `null` when the item does not exist or has expired -- `write(string $key, mixed $data, array $dependencies): void` stores the value -- `remove(string $key): void` deletes the item -- `clean(array $conditions): void` deletes items by the conditions of [clean() |expiration#Deleting Items]; an empty array means removing the expired items - -The dependencies arrive as an array with `Cache::*` keys, already simplified by `Cache`: the expiration is a number of seconds from now, files and constants have become callbacks, and other items are given by their storage keys. When reading an item, check its callbacks with `Nette\Caching\Cache::checkCallbacks()`. Throw `Nette\NotSupportedException` for a dependency or condition your storage cannot honor. - -Optional interfaces add more. `Nette\Caching\BulkReader` and `Nette\Caching\BulkWriter` let the storage read and write several items in one round trip. `Nette\Caching\LockingReader` adds the method `readOrGenerate()`, which protects against the cache stampede: it gets the generator of `load()` as a function and has to run it under a lock. The function stores the result itself, through `write()` or `remove()` of the same storage, so these methods must work while the lock is held. - -A short complete example is the source code of [SQLiteStorage |https://github.com/nette/caching/blob/master/src/Caching/Storages/SQLiteStorage.php]. - -{{maintitle: Nette Caching Storages: Files, Redis, Memcached, SQLite, PSR-16}} -{{description: Choose where the Nette cache stores data: files, Redis, Memcached, SQLite or any PSR-16 cache. Compare what each supports and configure it.}} diff --git a/caching/en/upgrading.texy b/caching/en/upgrading.texy deleted file mode 100644 index 274d819ada..0000000000 --- a/caching/en/upgrading.texy +++ /dev/null @@ -1,50 +0,0 @@ -Upgrading -********* - - -Upgrading to Version 4.0 -======================== - -The minimum required PHP version is 8.4. The `{cache}` tag requires Latte 3.1. - -.[tip] -A part of the work can be done for you by [DressCode |tools:coding-standard#Newer PHP and Nette] with the `dresscode/upgrade-nette` package: it rewrites renamed classes and constants and reports what it cannot rewrite. See [Upgrading Nette |https://dresscode.run/en/upgrading-nette]. - -The interfaces `Nette\Caching\IStorage`, `Nette\Caching\IBulkReader` and `Nette\Caching\Storages\IJournal` have been removed; use `Nette\Caching\Storage`, `Nette\Caching\BulkReader` and `Nette\Caching\Storages\Journal`. The service aliases `cacheStorage` and `nette.cacheJournal` have been removed as well; use the services `cache.storage` and `cache.journal`, or autowiring. - -The uppercase constants such as `Cache::EXPIRE` or `Cache::TAGS` and the method `Cache::start()` now trigger `E_USER_DEPRECATED`. Use `Cache::Expire`, `Cache::Tags` and `capture()`. Passing a closure as the data to `Cache::save()` is deprecated, pass it as the generator of `load()`. - -Dependencies can be written as the new object `Nette\Caching\Dependencies`, see [Expiration and Invalidation |expiration]. The array with `Cache::*` keys still works, but an unknown key or a value of a wrong type now throws an exception. Typically, this concerns a priority written as a string, such as `'50'`, which has to be an integer. - -The method `Cache::clean()` takes the conditions as named arguments, such as `$cache->clean(tags: ['article/10'])`; the array is validated the same way as the dependencies. Several things behave differently: - -- `clean()` without any condition throws an exception. To remove expired items, call `clean([])` on the storage. -- `all` on a cache with a namespace removes only that namespace and those derived from it; previously, it cleared the whole storage. To clear everything, call it on a cache without a namespace, or call `$storage->clean([Cache::All => true])`. -- Cleaning by `Cache::Namespaces` also removes the namespaces derived from the given ones by `derive()`. - -A storage throws `Nette\NotSupportedException` for a dependency or a condition it cannot honor, instead of silently ignoring it. `SQLiteStorage` does not support priorities or dependencies on files, constants, callbacks and other items; `MemcachedStorage` does not support cleaning by namespaces. See the overview of [storages |storages#Choosing a Storage]. - -`PsrCacheAdapter` now validates keys as PSR-16 requires, so it rejects keys containing any of `{}()/\@:`, such as `user:1`. - -If you have written your own storage, remove the method `lock()`, which no longer exists, and declare the return type `mixed` of the method `read()`. A storage that should let only one process generate a missing item implements the interface `Nette\Caching\LockingReader`. A subclass of `Cache` that overrides `clean()` must declare its new parameters. - - -Upgrading to Version 3.1 -======================== - -- the method `Nette\Caching\Cache::start()` was renamed to `capture()` - - -Upgrading to Version 2.4 -======================== - -- the class `Nette\Caching\Storages\FileJournal` is no longer available - - -Upgrading to Version 2.3 -======================== - -- the ancient and deprecated ArrayAccess syntax `$val = $cache[$key]` or `$cache[$key] = $val` triggers `E_USER_DEPRECATED`; use `$cache->load($key)` and `$cache->save($key, $val)` instead - -{{maintitle: Upgrading Nette Caching: What Changed in Each Version}} -{{description: What changed in Nette Caching 4.0, 3.1 and older versions, and how to update your code when moving to a newer version.}} diff --git a/caching/es/@home.texy b/caching/es/@home.texy deleted file mode 100644 index 437b6a72ac..0000000000 --- a/caching/es/@home.texy +++ /dev/null @@ -1,502 +0,0 @@ -Nette Caching -************* - -<div class=perex> - -La caché acelera su aplicación guardando datos cuya obtención resultó costosa en su momento, lo que permite acceder a ellos más rápido en el futuro. Veremos: - -- cómo usar la caché -- cómo cambiar el almacenamiento -- cómo invalidar correctamente la caché - -</div> - -Usar la caché en Nette es muy sencillo y, aun así, cubre necesidades de cacheo sofisticadas. Está diseñada para el rendimiento y una durabilidad del 100 %. Incluye adaptadores para los almacenamientos más habituales. Soporta la invalidación por etiquetas, la expiración por tiempo, la protección contra la estampida de caché y más. - - -Instalación -=========== - -Descargue e instale el paquete con [Composer|best-practices:composer]: - -```shell -composer require nette/caching -``` - - -Uso básico -========== - -El elemento central para trabajar con la caché es el objeto [api:Nette\Caching\Cache]. Creamos una instancia suya y le pasamos al constructor un objeto de almacenamiento. Ese objeto de almacenamiento representa el lugar físico donde se guardarán los datos (base de datos, Memcached, archivos en disco, etc.). El objeto de almacenamiento lo obtiene normalmente mediante [dependency injection |dependency-injection:passing-dependencies] pidiendo el tipo `Nette\Caching\Storage`. Lo esencial lo aprenderá en la [sección Almacenamientos |#Almacenamientos]. - -.[warning] -En la versión 3.0 la interfaz todavía llevaba el prefijo `I`, así que el nombre era `Nette\Caching\IStorage`. Además, las constantes de la clase `Cache` se escribían en mayúsculas, p. ej. `Cache::EXPIRE` en lugar de `Cache::Expire`. - -En los ejemplos siguientes damos por hecho que tenemos un alias `Cache` y una instancia de almacenamiento en la variable `$storage`. - -```php -use Nette\Caching\Cache; - -$storage = /* ... */; // instancia de Nette\Caching\Storage -``` - -La caché es, en esencia, un *almacén clave-valor*, es decir, leemos y escribimos los datos con claves, de forma parecida a los arrays asociativos. Las aplicaciones constan de varias partes independientes. Si todas usaran un único almacenamiento (imagine un solo directorio en el disco), tarde o temprano se producirían colisiones de claves. Nette Framework lo resuelve dividiendo el espacio de almacenamiento en espacios de nombres (conceptualmente, como subdirectorios). Cada parte de la aplicación trabaja entonces dentro de su propio espacio de nombres con un nombre único, lo que evita cualquier colisión. - -Indique el nombre del espacio de nombres como segundo argumento del constructor de la clase `Cache`: - -```php -$cache = new Cache($storage, 'Full Html Pages'); -``` - -Si hace falta, de una instancia existente puede derivar una caché nueva acotada a un subespacio de nombres con el método `derive()`: - -```php -$subCache = $cache->derive('Images'); -``` - -Ahora podemos usar el objeto `$cache` para leer de la caché y escribir en ella. Para ambas cosas sirve el método `load()`. El primer argumento es la clave y el segundo es un callback de PHP que se invoca si la clave no se encuentra en la caché. El callback genera el valor, lo devuelve, y el método `load()` lo guarda en la caché: - -```php -$value = $cache->load($key, function () use ($key) { - $computedValue = /* ... */; // cálculo costoso - return $computedValue; -}); -``` - -Si se omite el segundo parámetro (`$value = $cache->load($key)`), `load()` devuelve `null` si el elemento no se encuentra en la caché. - -.[tip] -Lo estupendo es que se pueden cachear todas las estructuras serializables, no solo cadenas. Lo mismo vale para las claves. - -Para borrar un elemento de la caché, use el método `remove()`: - -```php -$cache->remove($key); -``` - -También puede guardar un elemento en la caché con el método `$cache->save($key, $data, ?array $dependencies = null)`. Pero, por lo general, es preferible el enfoque con `load()` mostrado arriba. - - -Memoización -=========== - -La memoización consiste en cachear el resultado de la llamada a una función o método, de modo que la próxima vez que se llame con los mismos argumentos se devuelva el resultado cacheado en lugar de volver a calcularlo. - -Los métodos y las funciones se pueden llamar de forma memoizada con `call(callable $callback, ...$args)`: - -```php -$result = $cache->call('gethostbyaddr', $ip); -``` - -Así, la función `gethostbyaddr()` se llama solo una vez por cada argumento `$ip` único. Las llamadas siguientes con el mismo `$ip` devolverán el valor cacheado. - -También es posible crear un envoltorio memoizado de un método o una función y llamarlo más tarde: - -```php -function factorial($num) -{ - return /* ... */; -} - -$memoizedFactorial = $cache->wrap('factorial'); - -$result = $memoizedFactorial(5); // la primera vez lo calcula -$result = $memoizedFactorial(5); // la segunda vez lo devuelve de la caché -``` - - -Expiración e invalidación -========================= - -Al usar la caché hay que resolver la cuestión de cuándo dejan de ser válidos los datos guardados anteriormente. Nette Framework proporciona mecanismos para limitar la validez de los datos o para borrarlos explícitamente (en la terminología del framework, "invalidación"). - -La validez de los datos se establece al guardarlos, normalmente con el tercer parámetro del método `save()`, p. ej.: - -```php -$cache->save($key, $value, [ - $cache::Expire => '20 minutes', -]); -``` - -Como alternativa se puede establecer con el parámetro `$dependencies` que se pasa por referencia al callback del método `load()`, p. ej.: - -```php -$value = $cache->load($key, function (&$dependencies) { - $dependencies[Cache::Expire] = '20 minutes'; - return /* ... */; -}); -``` - -O con el tercer parámetro del propio método `load()`, p. ej.: - -```php -$value = $cache->load($key, function () { - return /* ... */; -}, [Cache::Expire => '20 minutes']); -``` - -En los ejemplos siguientes usaremos la segunda variante, con la variable `$dependencies` dentro del callback. - - -Expiración ----------- - -La forma más sencilla de expiración es un límite de tiempo. Esto cachea los datos con una validez de 20 minutos: - -```php -// acepta también un número de segundos o una marca de tiempo UNIX -$dependencies[Cache::Expire] = '20 minutes'; -``` - -Si quiere que el periodo de validez se prolongue con cada lectura (expiración deslizante), lo consigue así, pero tenga en cuenta que eso aumenta la sobrecarga de la caché: - -```php -$dependencies[Cache::Sliding] = true; -``` - -Una opción útil es hacer que los datos expiren cuando se modifique un archivo concreto o alguno de varios archivos. Es útil, por ejemplo, al cachear datos derivados de procesar esos archivos. Use rutas absolutas. - -```php -$dependencies[Cache::Files] = '/path/to/data.yaml'; -// o -$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; -``` - -Podemos hacer que un elemento de la caché expire cuando expire otro elemento concreto (o alguno de varios). Es útil al cachear, por ejemplo, una página HTML entera y sus fragmentos bajo claves distintas. Cuando cambia un fragmento, hay que invalidar toda la página. Si los fragmentos están guardados bajo claves como `frag1` y `frag2`, use: - -```php -$dependencies[Cache::Items] = ['frag1', 'frag2']; -``` - -La expiración también se puede controlar con funciones propias o métodos estáticos. Se llaman en cada lectura para determinar si el elemento sigue siendo válido. Por ejemplo, podemos hacer que un elemento expire siempre que cambie la versión de PHP. Cree una función que compare la versión actual con un parámetro y, al guardar, añada a las dependencias un array con el formato `[nombre de la función, ...argumentos]`: - -```php -function checkPhpVersion($ver): bool -{ - return $ver === PHP_VERSION_ID; -} - -$dependencies[Cache::Callbacks] = [ - ['checkPhpVersion', PHP_VERSION_ID] // expira cuando checkPhpVersion(...) === false -]; -``` - -Naturalmente, todos estos criterios se pueden combinar. El elemento de la caché expira si al menos uno de los criterios deja de cumplirse. - -```php -$dependencies[Cache::Expire] = '20 minutes'; -$dependencies[Cache::Files] = '/path/to/data.yaml'; -``` - - -Invalidación con etiquetas --------------------------- - -Las etiquetas proporcionan un mecanismo de invalidación muy útil. A cada elemento guardado en la caché le podemos asignar una lista de etiquetas (cadenas arbitrarias). Por ejemplo, supongamos que tenemos una página HTML que muestra un artículo y sus comentarios, y que queremos cachear. Al guardarla indicamos las etiquetas pertinentes: - -```php -$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"]; -``` - -Pasemos ahora a la sección de administración. Ahí tenemos un formulario para editar los artículos. Junto con guardar el artículo en la base de datos, llamamos al método `clean()` para borrar los elementos cacheados según su etiqueta: - -```php -$cache->clean([ - $cache::Tags => ["article/$articleId"], -]); -``` - -Del mismo modo, al añadir un comentario nuevo (o al editarlo) tenemos que acordarnos de invalidar la etiqueta correspondiente: - -```php -$cache->clean([ - $cache::Tags => ["comments/$articleId"], -]); -``` - -¿Qué hemos conseguido? Que nuestra caché HTML se invalide (se borre) siempre que cambie el artículo asociado o sus comentarios. Al editar el artículo con ID = 10 se invalida la etiqueta `article/10` y se borra la página HTML cacheada que lleva esa etiqueta. Lo mismo ocurre al añadir un comentario nuevo bajo el artículo correspondiente. - -.[note] -Las etiquetas requieren un [#Journal]. - - -Invalidación por prioridad --------------------------- - -A los distintos elementos de la caché les podemos asignar prioridades. Eso permite un borrado controlado, por ejemplo cuando la caché supera cierto límite de tamaño: - -```php -$dependencies[Cache::Priority] = 50; -``` - -Para borrar todos los elementos con una prioridad igual o menor que 100: - -```php -$cache->clean([ - $cache::Priority => 100, -]); -``` - -.[note] -Las prioridades requieren el llamado [#Journal]. - - -Vaciar la caché ---------------- - -El parámetro `Cache::All` lo borra todo: - -```php -$cache->clean([ - $cache::All => true, -]); -``` - - -Lectura masiva -============== - -Para leer y escribir en la caché de forma masiva, use el método `bulkLoad()`. Pásele un array de claves y le devolverá un array con los valores correspondientes: - -```php -$values = $cache->bulkLoad($keys); -``` - -El método `bulkLoad()` funciona de forma parecida a `load()` y también acepta un segundo parámetro con un callback. Ese callback recibe la clave del elemento que se está generando: - -```php -$values = $cache->bulkLoad($keys, function ($key, &$dependencies) { - $computedValue = /* ... */; // cálculo costoso - return $computedValue; -}); -``` - -Al revés, para escribir varios elementos a la vez use el método `bulkSave()`, que toma un array de pares `clave => valor` y, opcionalmente, las dependencias: - -```php -$cache->bulkSave([ - $key1 => $value1, - $key2 => $value2, -], [Cache::Expire => '20 minutes']); -``` - - -Uso con PSR-16 .{data-version:3.3.1} -==================================== - -Para usar Nette Cache con una interfaz PSR-16 puede aprovechar `PsrCacheAdapter`. Permite una integración fluida entre Nette Cache y cualquier código o biblioteca que espere una implementación de caché compatible con PSR-16. - -```php -$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage); -``` - -Ahora puede usar `$psrCache` como una caché PSR-16 estándar: - -```php -$psrCache->set('key', 'value', 3600); // guarda el valor durante 1 hora -$value = $psrCache->get('key', 'default'); -``` - -El adaptador soporta todos los métodos definidos en PSR-16, incluidos `getMultiple()`, `setMultiple()` y `deleteMultiple()`. - - -Cachear la salida -================= - -La salida se puede capturar y cachear de forma muy elegante: - -```php -if ($capture = $cache->capture($key)) { - - // echo ... imprime algunos datos - - $capture->end(); // guarda la salida en la caché -} -``` - -Si la salida ya está en la caché, el método `capture()` la imprime y devuelve `null`, así que el bloque de la condición `if` se salta. En caso contrario empieza a bufferizar la salida y devuelve un objeto `$capture`, que usa para guardar por fin los datos capturados en la caché con su método `end()`. - -.[note] -En la versión 3.0 este método se llamaba `$cache->start()`. - - -Cachear en Latte -================ - -Cachear en las plantillas de [Latte|latte:] es muy sencillo. Basta con envolver la parte de la plantilla que quiere cachear con las etiquetas `{cache}...{/cache}`. La caché se invalida automáticamente siempre que cambia el archivo fuente de la plantilla (incluidas las plantillas incluidas dentro del bloque cacheado). Las etiquetas `{cache}` se pueden anidar. Cuando se invalida un bloque anidado (p. ej. mediante una etiqueta), también se invalida su bloque padre. - -Dentro de la etiqueta puede indicar las claves a las que se vinculará la entrada de la caché (aquí, la variable `$id`), establecer un tiempo de expiración y definir [etiquetas de invalidación |#Invalidación con etiquetas]. - -```latte -{cache $id, expire: '20 minutes', tags: [tag1, tag2]} - ... -{/cache} -``` - -Todos estos parámetros son opcionales, así que no tiene que indicar la expiración, las etiquetas ni siquiera las claves. - -El uso de la caché también se puede condicionar con `if`: el contenido se cacheará solo si se cumple la condición: - -```latte -{cache $id, if: !$form->isSubmitted()} - {$form} -{/cache} -``` - - -Almacenamientos -=============== - -Un almacenamiento es un objeto que representa el lugar físico donde se guardan los datos. Podemos usar una base de datos, un servidor Memcached o el almacenamiento más a mano: archivos en disco. - -|---------------------- -| Almacenamiento | Descripción -|---------------------- -| [#FileStorage] | Almacenamiento predeterminado, guarda la caché en archivos en disco. -| [#MemcachedStorage] | Usa un servidor `Memcached` para guardar los datos. -| [#MemoryStorage] | Los datos se guardan temporalmente en memoria (se pierden al terminar la petición). -| [#SQLiteStorage] | Los datos se guardan en un archivo de base de datos SQLite. -| [#DevNullStorage] | Los datos no se guardan realmente; útil para hacer pruebas. - -El objeto de almacenamiento lo obtiene mediante [dependency injection |dependency-injection:passing-dependencies] pidiendo el tipo `Nette\Caching\Storage`. De forma predeterminada, Nette proporciona un objeto `FileStorage` que guarda los datos en el subdirectorio `cache` dentro del directorio de [archivos temporales |application:bootstrapping#Archivos temporales]. - -Puede cambiar el almacenamiento predeterminado en la configuración: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - - -FileStorage ------------ - -Escribe las entradas de la caché en archivos en disco. El almacenamiento `Nette\Caching\Storages\FileStorage` está muy optimizado para el rendimiento y, sobre todo, garantiza la atomicidad plena de las operaciones. ¿Qué significa eso? Que al usar la caché no puede ocurrir que lea un archivo que otro hilo todavía no ha terminado de escribir, ni que alguien lo borre mientras lo está leyendo. Por tanto, usar este almacenamiento de caché es completamente seguro. - -Este almacenamiento incluye además una función integrada importante que evita un aumento extremo del uso de CPU cuando la caché se vacía o todavía está "fría" (es decir, aún no creada). Se conoce como prevención de la "estampida de caché":https://en.wikipedia.org/wiki/Cache_stampede. Ocurre cuando varias peticiones concurrentes piden a la vez el mismo elemento cacheado (p. ej. el resultado de una consulta SQL costosa). Si el elemento no está en la caché en ese momento, todos esos procesos podrían empezar a ejecutar la misma operación costosa (como la consulta SQL). Eso multiplica la carga del servidor, e incluso puede ocurrir que ningún hilo consiga responder dentro del límite de tiempo, la caché no llegue a crearse y la aplicación se caiga. Por suerte, la caché de Nette se ocupa de eso: cuando hay varias peticiones concurrentes del mismo elemento, solo el primer hilo lo genera. Los demás hilos esperan y usan después el resultado generado por el primero. - -Ejemplo de creación de un `FileStorage`: - -```php -// el almacenamiento será el directorio '/path/to/temp' del disco -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); -``` - - -MemcachedStorage ----------------- - -El servidor [Memcached |https://memcached.org] es un sistema distribuido de alto rendimiento para cachear objetos en memoria. Su adaptador en Nette es `Nette\Caching\Storages\MemcachedStorage`. En la configuración indique la dirección IP del servidor y el puerto si difiere del estándar 11211. - -.[caution] -Requiere la extensión de PHP `memcached`. - -```neon -services: - cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5') -``` - - -MemoryStorage -------------- - -`Nette\Caching\Storages\MemoryStorage` es un almacenamiento que mantiene los datos dentro de un array de PHP. Por consiguiente, los datos se pierden al terminar la petición. - - -SQLiteStorage -------------- - -La base de datos SQLite, junto con el adaptador `Nette\Caching\Storages\SQLiteStorage`, proporciona una forma de cachear los datos dentro de un único archivo en disco. La configuración indica la ruta a ese archivo de base de datos. - -.[caution] -Requiere las extensiones de PHP `pdo` y `pdo_sqlite`. - -```neon -services: - cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db') -``` - - -DevNullStorage --------------- - -Una implementación especial de almacenamiento es `Nette\Caching\Storages\DevNullStorage`, que no guarda ningún dato. Por eso resulta adecuada para hacer pruebas cuando quiere eliminar los efectos del cacheo. - - -Usar la caché en el código -========================== - -Al usar la caché en su código hay dos enfoques principales. El primero es obtener el objeto de almacenamiento mediante [dependency injection |dependency-injection:passing-dependencies] y crear después usted mismo el objeto `Cache`: - -```php -use Nette; - -class ClassOne -{ - private Nette\Caching\Cache $cache; - - public function __construct(Nette\Caching\Storage $storage) - { - $this->cache = new Nette\Caching\Cache($storage, 'my-namespace'); - } -} -``` - -La segunda opción es pedir directamente el objeto `Cache`: - -```php -class ClassTwo -{ - public function __construct( - private Nette\Caching\Cache $cache, - ) { - } -} -``` - -El objeto `Cache` hay que definirlo entonces en la configuración, por ejemplo así: - -```neon -services: - - ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') ) -``` - - -Journal -======= - -Nette guarda la información sobre las etiquetas y las prioridades en el llamado journal. De forma predeterminada, para ello se usa SQLite mediante el archivo `journal.s3db`, y **se requieren las extensiones de PHP `pdo` y `pdo_sqlite`.** - -Puede cambiar la implementación del journal en la configuración: - -```neon -services: - cache.journal: MyJournal -``` - - -Servicios DI -============ - -Estos servicios se añaden al contenedor DI: - -| Nombre | Tipo | Descripción -|---------------------------------------------------------- -| `cache.journal` | [api:Nette\Caching\Storages\Journal] | El almacenamiento del journal de la caché -| `cache.storage` | [api:Nette\Caching\Storage] | El almacenamiento principal de la caché - - -Desactivar la caché -=================== - -Una forma de desactivar el cacheo en su aplicación es establecer el almacenamiento a [#DevNullStorage]: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - -Este ajuste no afecta al cacheo de las plantillas de Latte ni del contenedor DI, ya que esas bibliotecas no usan los servicios de `nette/caching` y gestionan sus cachés por su cuenta. Además, sus cachés [normalmente no hace falta desactivarlas |nette:troubleshooting#¿Cómo desactivar la caché durante el desarrollo?] en modo de desarrollo. - - -Si está actualizando a una versión más reciente, vea la página de [actualización |upgrading]. diff --git a/caching/es/@left-menu.texy b/caching/es/@left-menu.texy deleted file mode 100644 index d775b39d5e..0000000000 --- a/caching/es/@left-menu.texy +++ /dev/null @@ -1,13 +0,0 @@ -Nette Caching -************* -- [Introducción |@home] -- [Actualización|upgrading] - - -Lecturas adicionales -******************** -- [Documentación de Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Buenas prácticas |best-practices:] -- [Solución de problemas |nette:troubleshooting] diff --git a/caching/es/@meta.texy b/caching/es/@meta.texy deleted file mode 100644 index 3798d9cda4..0000000000 --- a/caching/es/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentación de Nette}} diff --git a/caching/es/upgrading.texy b/caching/es/upgrading.texy deleted file mode 100644 index f2789edd82..0000000000 --- a/caching/es/upgrading.texy +++ /dev/null @@ -1,20 +0,0 @@ -Actualización -************* - - -Actualización a la versión 3.1 -============================== - -- el método `Nette\Caching\Cache::start()` se renombró a `capture()` - - -Actualización a la versión 2.4 -============================== - -- la clase `Nette\Caching\Storages\FileJournal` ya no está disponible - - -Actualización a la versión 2.3 -============================== - -- la antiquísima y obsoleta sintaxis ArrayAccess `$val = $cache[$key]` o `$cache[$key] = $val` dispara `E_USER_DEPRECATED`; use `$cache->load($key)` y `$cache->save($key, $val)` en su lugar diff --git a/caching/fr/@home.texy b/caching/fr/@home.texy deleted file mode 100644 index 0c3d1f9a4d..0000000000 --- a/caching/fr/@home.texy +++ /dev/null @@ -1,502 +0,0 @@ -Nette Caching -************* - -<div class=perex> - -Le cache accélère votre application en conservant des données dont l'obtention a été coûteuse, pour y accéder plus vite la prochaine fois. Nous allons voir : - -- comment utiliser le cache -- comment changer de stockage -- comment invalider correctement le cache - -</div> - -L'utilisation du cache dans Nette est très simple, tout en couvrant des besoins de mise en cache sophistiqués. Il est conçu pour la performance et une durabilité de 100 %. Il embarque des adaptateurs pour les stockages les plus répandus. Il prend en charge l'invalidation par tags, l'expiration dans le temps, la protection contre le cache stampede, et bien plus. - - -Installation -============ - -Téléchargez et installez le paquet à l'aide de [Composer|best-practices:composer] : - -```shell -composer require nette/caching -``` - - -Utilisation de base -=================== - -L'élément central du travail avec le cache est l'objet [api:Nette\Caching\Cache]. Nous en créons une instance en passant au constructeur un objet de stockage. Cet objet représente l'endroit physique où les données seront conservées (base de données, Memcached, fichiers sur le disque, etc.). Vous obtenez d'ordinaire l'objet de stockage par [injection de dépendances |dependency-injection:passing-dependencies] en demandant le type `Nette\Caching\Storage`. Vous apprendrez l'essentiel dans la [section Stockages |#Stockages]. - -.[warning] -Dans la version 3.0, l'interface portait encore le préfixe `I`, son nom était donc `Nette\Caching\IStorage`. De plus, les constantes de la classe `Cache` s'écrivaient en majuscules, par ex. `Cache::EXPIRE` au lieu de `Cache::Expire`. - -Dans les exemples qui suivent, supposons que nous avons un alias `Cache` et une instance de stockage dans la variable `$storage`. - -```php -use Nette\Caching\Cache; - -$storage = /* ... */; // instance of Nette\Caching\Storage -``` - -Le cache est essentiellement un *magasin clé-valeur* : nous y lisons et écrivons à l'aide de clés, comme dans un tableau associatif. Or une application se compose de plusieurs parties indépendantes. Si toutes utilisaient un seul stockage (imaginez un unique répertoire sur le disque), des collisions de clés finiraient par se produire. Nette Framework résout cela en découpant l'espace de stockage en espaces de noms (conceptuellement, des sous-répertoires). Chaque partie de l'application travaille alors dans son propre espace de noms au nom unique, et aucune collision n'est possible. - -Indiquez le nom de l'espace de noms en second argument du constructeur de la classe `Cache` : - -```php -$cache = new Cache($storage, 'Full Html Pages'); -``` - -Au besoin, vous pouvez dériver d'une instance existante un nouveau cache limité à un sous-espace de noms avec la méthode `derive()` : - -```php -$subCache = $cache->derive('Images'); -``` - -Nous pouvons désormais utiliser l'objet `$cache` pour lire dans le cache et y écrire. La méthode `load()` sert aux deux. Le premier argument est la clé, le second un callback PHP appelé si la clé n'est pas trouvée dans le cache. Le callback produit la valeur, la renvoie, et la méthode `load()` la met en cache : - -```php -$value = $cache->load($key, function () use ($key) { - $computedValue = /* ... */; // expensive computation - return $computedValue; -}); -``` - -Si le second paramètre est omis (`$value = $cache->load($key)`), `load()` renvoie `null` quand l'élément n'est pas trouvé dans le cache. - -.[tip] -Il est appréciable que toute structure sérialisable puisse être mise en cache, pas seulement les chaînes. Cela vaut aussi pour les clés. - -Pour supprimer un élément du cache, utilisez la méthode `remove()` : - -```php -$cache->remove($key); -``` - -Vous pouvez aussi enregistrer un élément dans le cache avec la méthode `$cache->save($key, $data, ?array $dependencies = null)`. Cela dit, l'approche par `load()` montrée plus haut est généralement préférable. - - -Mémoïsation -=========== - -La mémoïsation consiste à mettre en cache le résultat d'un appel de fonction ou de méthode, si bien qu'au prochain appel avec les mêmes arguments, le résultat en cache est renvoyé au lieu d'être recalculé. - -Les méthodes et fonctions peuvent être appelées de façon mémoïsée avec `call(callable $callback, ...$args)` : - -```php -$result = $cache->call('gethostbyaddr', $ip); -``` - -La fonction `gethostbyaddr()` n'est ainsi appelée qu'une seule fois pour chaque argument `$ip` distinct. Les appels suivants avec le même `$ip` renverront la valeur en cache. - -Il est également possible de créer autour d'une méthode ou d'une fonction un emballage mémoïsé, appelable plus tard : - -```php -function factorial($num) -{ - return /* ... */; -} - -$memoizedFactorial = $cache->wrap('factorial'); - -$result = $memoizedFactorial(5); // calculates it the first time -$result = $memoizedFactorial(5); // returns from cache the second time -``` - - -Expiration et invalidation -========================== - -Quand on utilise le cache, il faut répondre à la question du moment où les données enregistrées cessent d'être valides. Nette Framework offre des mécanismes pour limiter la validité des données ou les supprimer explicitement (ce que la terminologie du framework appelle "invalidation"). - -La validité des données se définit au moment de l'enregistrement, d'ordinaire par le troisième paramètre de la méthode `save()`, par ex. : - -```php -$cache->save($key, $value, [ - $cache::Expire => '20 minutes', -]); -``` - -Elle peut aussi se définir par le paramètre `$dependencies` passé par référence au callback de la méthode `load()`, par ex. : - -```php -$value = $cache->load($key, function (&$dependencies) { - $dependencies[Cache::Expire] = '20 minutes'; - return /* ... */; -}); -``` - -Ou encore par le 3e paramètre de la méthode `load()` elle-même, par ex. : - -```php -$value = $cache->load($key, function () { - return /* ... */; -}, [Cache::Expire => '20 minutes']); -``` - -Dans les exemples qui suivent, nous partirons de la deuxième variante, avec la variable `$dependencies` dans le callback. - - -Expiration ----------- - -La forme la plus simple d'expiration est la limite de temps. Ceci met les données en cache avec une validité de 20 minutes : - -```php -// accepts number of seconds or a UNIX timestamp as well -$dependencies[Cache::Expire] = '20 minutes'; -``` - -Si vous voulez que la durée de validité se prolonge à chaque lecture (expiration glissante), vous pouvez procéder ainsi, en sachant que cela alourdit le cache : - -```php -$dependencies[Cache::Sliding] = true; -``` - -Une option utile est de faire expirer les données lorsqu'un fichier précis, ou l'un de plusieurs fichiers, est modifié. C'est pratique par exemple quand on met en cache des données issues du traitement de ces fichiers. Utilisez des chemins absolus. - -```php -$dependencies[Cache::Files] = '/path/to/data.yaml'; -// or -$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; -``` - -Nous pouvons faire expirer un élément du cache quand un autre élément précis (ou l'un de plusieurs autres) expire. C'est utile lorsqu'on met en cache, par exemple, une page HTML entière et ses fragments sous des clés différentes. Quand un fragment change, toute la page doit être invalidée. Si les fragments sont stockés sous les clés `frag1` et `frag2`, utilisez : - -```php -$dependencies[Cache::Items] = ['frag1', 'frag2']; -``` - -L'expiration peut aussi être pilotée par des fonctions ou des méthodes statiques à vous. Elles sont appelées à chaque lecture pour déterminer si l'élément est encore valide. Nous pouvons par exemple faire expirer un élément dès que la version de PHP change. Créez une fonction qui compare la version actuelle avec un paramètre, et à l'enregistrement, ajoutez aux dépendances un tableau de la forme `[nom de la fonction, ...arguments]` : - -```php -function checkPhpVersion($ver): bool -{ - return $ver === PHP_VERSION_ID; -} - -$dependencies[Cache::Callbacks] = [ - ['checkPhpVersion', PHP_VERSION_ID] // expire when checkPhpVersion(...) === false -]; -``` - -Tous ces critères peuvent naturellement être combinés. L'élément du cache expire dès qu'au moins un critère n'est plus rempli. - -```php -$dependencies[Cache::Expire] = '20 minutes'; -$dependencies[Cache::Files] = '/path/to/data.yaml'; -``` - - -Invalidation par tags ---------------------- - -Les tags offrent un mécanisme d'invalidation très pratique. Nous pouvons attribuer à chaque élément enregistré dans le cache une liste de tags (des chaînes quelconques). Supposons par exemple que nous ayons une page HTML affichant un article et ses commentaires, que nous voulons mettre en cache. À l'enregistrement, nous indiquons les tags correspondants : - -```php -$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"]; -``` - -Passons maintenant dans l'administration. Nous y avons un formulaire d'édition des articles. En même temps que l'enregistrement de l'article en base, nous appelons la méthode `clean()` pour supprimer du cache les éléments portant le tag : - -```php -$cache->clean([ - $cache::Tags => ["article/$articleId"], -]); -``` - -De même, à l'ajout d'un nouveau commentaire (ou à sa modification), nous devons penser à invalider le tag correspondant : - -```php -$cache->clean([ - $cache::Tags => ["comments/$articleId"], -]); -``` - -Qu'avons-nous gagné ? Notre cache HTML sera désormais invalidé (supprimé) chaque fois que l'article associé ou ses commentaires changent. À l'édition de l'article d'ID = 10, le tag `article/10` est invalidé et la page HTML en cache portant ce tag est supprimée. Il en va de même lorsqu'un nouveau commentaire est ajouté sous l'article concerné. - -.[note] -Les tags nécessitent un [#Journal]. - - -Invalidation par priorité -------------------------- - -Nous pouvons attribuer des priorités aux différents éléments du cache. Cela permet une suppression contrôlée, par exemple quand le cache dépasse une certaine taille : - -```php -$dependencies[Cache::Priority] = 50; -``` - -Pour supprimer tous les éléments dont la priorité est inférieure ou égale à 100 : - -```php -$cache->clean([ - $cache::Priority => 100, -]); -``` - -.[note] -Les priorités nécessitent ce qu'on appelle un [#Journal]. - - -Vider le cache --------------- - -Le paramètre `Cache::All` vide tout : - -```php -$cache->clean([ - $cache::All => true, -]); -``` - - -Lecture en masse -================ - -Pour lire et écrire en masse dans le cache, utilisez la méthode `bulkLoad()`. Passez-lui un tableau de clés et elle renvoie un tableau des valeurs correspondantes : - -```php -$values = $cache->bulkLoad($keys); -``` - -La méthode `bulkLoad()` fonctionne comme `load()` et accepte elle aussi un callback en second paramètre. Ce callback reçoit la clé de l'élément à produire : - -```php -$values = $cache->bulkLoad($keys, function ($key, &$dependencies) { - $computedValue = /* ... */; // expensive computation - return $computedValue; -}); -``` - -À l'inverse, pour écrire plusieurs éléments d'un coup, utilisez la méthode `bulkSave()`, qui prend un tableau de paires `clé => valeur` et des dépendances facultatives : - -```php -$cache->bulkSave([ - $key1 => $value1, - $key2 => $value2, -], [Cache::Expire => '20 minutes']); -``` - - -Utilisation avec PSR-16 .{data-version:3.3.1} -============================================= - -Pour utiliser Nette Cache avec une interface PSR-16, vous pouvez recourir à `PsrCacheAdapter`. Il permet une intégration sans heurt entre Nette Cache et tout code ou bibliothèque attendant une implémentation de cache compatible PSR-16. - -```php -$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage); -``` - -Vous pouvez désormais utiliser `$psrCache` comme un cache PSR-16 standard : - -```php -$psrCache->set('key', 'value', 3600); // stores the value for 1 hour -$value = $psrCache->get('key', 'default'); -``` - -L'adaptateur prend en charge toutes les méthodes définies par PSR-16, y compris `getMultiple()`, `setMultiple()` et `deleteMultiple()`. - - -Mise en cache de la sortie -========================== - -La sortie peut être capturée et mise en cache très élégamment : - -```php -if ($capture = $cache->capture($key)) { - - // echo ... printing some data - - $capture->end(); // save the output to the cache -} -``` - -Si la sortie est déjà présente dans le cache, la méthode `capture()` l'affiche et renvoie `null` : le bloc de la condition `if` est donc sauté. Sinon, elle commence à mettre la sortie en tampon et renvoie un objet `$capture`, dont la méthode `end()` vous sert à enregistrer finalement les données capturées dans le cache. - -.[note] -Dans la version 3.0, cette méthode s'appelait `$cache->start()`. - - -Mise en cache dans Latte -======================== - -La mise en cache dans les templates [Latte|latte:] est très simple. Il suffit d'entourer la portion de template à mettre en cache par les tags `{cache}...{/cache}`. Le cache est automatiquement invalidé dès que le fichier source du template change (y compris tout template inclus dans le bloc mis en cache). Les tags `{cache}` peuvent être imbriqués. Quand un bloc imbriqué est invalidé (par ex. via un tag), son bloc parent l'est aussi. - -Dans le tag, vous pouvez indiquer les clés auxquelles l'entrée du cache sera liée (ici la variable `$id`), définir une durée d'expiration et fixer des [tags d'invalidation |#Invalidation par tags]. - -```latte -{cache $id, expire: '20 minutes', tags: [tag1, tag2]} - ... -{/cache} -``` - -Tous ces paramètres sont facultatifs : vous n'avez à indiquer ni l'expiration, ni les tags, ni même les clés. - -L'usage du cache peut aussi être conditionné par `if` : le contenu ne sera mis en cache que si la condition est remplie. - -```latte -{cache $id, if: !$form->isSubmitted()} - {$form} -{/cache} -``` - - -Stockages -========= - -Un stockage est un objet représentant l'endroit physique où les données sont conservées. Nous pouvons utiliser une base de données, un serveur Memcached, ou le stockage le plus immédiatement disponible : des fichiers sur le disque. - -|---------------------- -| Stockage | Description -|---------------------- -| [#FileStorage] | Stockage par défaut, enregistre le cache dans des fichiers sur le disque. -| [#MemcachedStorage] | Utilise un serveur `Memcached` pour le stockage. -| [#MemoryStorage] | Les données sont conservées temporairement en mémoire (perdues à la fin de la requête). -| [#SQLiteStorage] | Les données sont conservées dans un fichier de base SQLite. -| [#DevNullStorage] | Les données ne sont en réalité pas conservées ; utile pour les tests. - -Vous obtenez l'objet de stockage par [injection de dépendances |dependency-injection:passing-dependencies] en demandant le type `Nette\Caching\Storage`. Par défaut, Nette fournit un objet `FileStorage` qui conserve les données dans le sous-répertoire `cache` du répertoire des [fichiers temporaires |application:bootstrapping#Fichiers Temporaires]. - -Vous pouvez changer le stockage par défaut dans la configuration : - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - - -FileStorage ------------ - -Écrit les entrées du cache dans des fichiers sur le disque. Le stockage `Nette\Caching\Storages\FileStorage` est très optimisé pour la performance et, surtout, garantit l'atomicité complète des opérations. Qu'est-ce que cela signifie ? Avec ce cache, il ne peut pas arriver que vous lisiez un fichier qu'un autre thread n'a pas fini d'écrire, ni que quelqu'un le supprime pendant que vous le lisez. Son utilisation est donc parfaitement sûre. - -Ce stockage embarque en outre une fonction importante qui empêche une envolée extrême de la charge CPU quand le cache est vidé ou encore "froid" (c'est-à-dire pas encore constitué). C'est la prévention du "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede. Le phénomène survient quand plusieurs requêtes concurrentes demandent en même temps le même élément du cache (par ex. le résultat d'une requête SQL coûteuse). Si l'élément n'est pas en cache à cet instant, tous ces processus peuvent se mettre à exécuter la même opération coûteuse (la requête SQL). La charge du serveur en est démultipliée, et il peut même arriver qu'aucun thread ne réponde dans le temps imparti, que le cache ne se constitue pas et que l'application s'effondre. Heureusement, le cache de Nette gère cela : quand plusieurs requêtes concurrentes portent sur le même élément, seul le premier thread le produit. Les autres attendent, puis utilisent le résultat produit par le premier. - -Exemple de création d'un `FileStorage` : - -```php -// the storage will be the directory '/path/to/temp' on disk -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); -``` - - -MemcachedStorage ----------------- - -Le serveur [Memcached |https://memcached.org] est un système distribué et très performant de mise en cache d'objets en mémoire. Son adaptateur dans Nette est `Nette\Caching\Storages\MemcachedStorage`. Dans la configuration, indiquez l'adresse IP du serveur et son port s'il diffère du 11211 standard. - -.[caution] -Nécessite l'extension PHP `memcached`. - -```neon -services: - cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5') -``` - - -MemoryStorage -------------- - -`Nette\Caching\Storages\MemoryStorage` est un stockage qui garde les données dans un tableau PHP. Elles sont donc perdues à la fin de la requête. - - -SQLiteStorage -------------- - -La base de données SQLite, avec l'adaptateur `Nette\Caching\Storages\SQLiteStorage`, offre un moyen de mettre les données en cache dans un unique fichier sur le disque. La configuration indique le chemin de ce fichier de base. - -.[caution] -Nécessite les extensions PHP `pdo` et `pdo_sqlite`. - -```neon -services: - cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db') -``` - - -DevNullStorage --------------- - -Une implémentation particulière est `Nette\Caching\Storages\DevNullStorage`, qui ne conserve en réalité aucune donnée. Elle convient donc aux tests, quand vous voulez éliminer les effets du cache. - - -Utilisation du cache dans le code -================================= - -Pour utiliser le cache dans votre code, il existe deux approches principales. La première consiste à obtenir l'objet de stockage par [injection de dépendances |dependency-injection:passing-dependencies], puis à créer vous-même l'objet `Cache` : - -```php -use Nette; - -class ClassOne -{ - private Nette\Caching\Cache $cache; - - public function __construct(Nette\Caching\Storage $storage) - { - $this->cache = new Nette\Caching\Cache($storage, 'my-namespace'); - } -} -``` - -La seconde consiste à se faire passer directement l'objet `Cache` : - -```php -class ClassTwo -{ - public function __construct( - private Nette\Caching\Cache $cache, - ) { - } -} -``` - -L'objet `Cache` doit alors être défini dans la configuration, par exemple ainsi : - -```neon -services: - - ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') ) -``` - - -Journal -======= - -Nette conserve les informations sur les tags et les priorités dans ce qu'on appelle un journal. SQLite est utilisé par défaut à cette fin, via le fichier `journal.s3db`, et **les extensions PHP `pdo` et `pdo_sqlite` sont requises.** - -Vous pouvez changer l'implémentation du journal dans la configuration : - -```neon -services: - cache.journal: MyJournal -``` - - -Services DI -=========== - -Ces services sont ajoutés au conteneur DI : - -| Nom | Type | Description -|---------------------------------------------------------- -| `cache.journal` | [api:Nette\Caching\Storages\Journal] | Le stockage du journal du cache -| `cache.storage` | [api:Nette\Caching\Storage] | Le stockage principal du cache - - -Désactiver le cache -=================== - -Une façon de désactiver la mise en cache dans votre application est de régler le stockage sur [#DevNullStorage] : - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - -Ce réglage n'affecte ni la mise en cache des templates Latte ni celle du conteneur DI, car ces bibliothèques n'utilisent pas les services de `nette/caching` et gèrent leur cache par elles-mêmes. Par ailleurs, il n'est en général [pas nécessaire de désactiver leur cache |nette:troubleshooting#Comment désactiver le cache pendant le développement ?] en mode développement. - - -Si vous passez à une version plus récente, consultez la page [mise à niveau |upgrading]. diff --git a/caching/fr/@left-menu.texy b/caching/fr/@left-menu.texy deleted file mode 100644 index 36b3c82411..0000000000 --- a/caching/fr/@left-menu.texy +++ /dev/null @@ -1,13 +0,0 @@ -Nette Caching -************* -- [Introduction |@home] -- [Mise à niveau|upgrading] - - -Pour aller plus loin -******************** -- [Documentation Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Bonnes pratiques |best-practices:] -- [Résolution de problèmes |nette:troubleshooting] diff --git a/caching/fr/@meta.texy b/caching/fr/@meta.texy deleted file mode 100644 index 72ae4b8db8..0000000000 --- a/caching/fr/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentation Nette}} diff --git a/caching/fr/upgrading.texy b/caching/fr/upgrading.texy deleted file mode 100644 index 1d6e687fce..0000000000 --- a/caching/fr/upgrading.texy +++ /dev/null @@ -1,20 +0,0 @@ -Mise à niveau -************* - - -Passage à la version 3.1 -======================== - -- la méthode `Nette\Caching\Cache::start()` a été renommée en `capture()` - - -Passage à la version 2.4 -======================== - -- la classe `Nette\Caching\Storages\FileJournal` n'est plus disponible - - -Passage à la version 2.3 -======================== - -- l'antique syntaxe ArrayAccess dépréciée `$val = $cache[$key]` ou `$cache[$key] = $val` déclenche `E_USER_DEPRECATED` ; utilisez `$cache->load($key)` et `$cache->save($key, $val)` à la place diff --git a/caching/it/@home.texy b/caching/it/@home.texy deleted file mode 100644 index 371a69ab1b..0000000000 --- a/caching/it/@home.texy +++ /dev/null @@ -1,502 +0,0 @@ -Nette Caching -************* - -<div class=perex> - -La cache accelera la vostra applicazione conservando i dati che una volta erano costosi da ottenere, così in futuro vi si accede più in fretta. Vedremo: - -- come usare la cache -- come cambiare il backend di storage -- come invalidare correttamente la cache - -</div> - -Usare la cache in Nette è molto semplice, eppure copre esigenze di caching sofisticate. È progettata per le prestazioni e per una durabilità del 100%. Comprende adattatori per i backend di storage più diffusi. Supporta l'invalidazione tramite tag, la scadenza temporale, la protezione contro il cache stampede e altro. - - -Installazione -============= - -Il pacchetto si scarica e si installa con [Composer|best-practices:composer]: - -```shell -composer require nette/caching -``` - - -Uso di base -=========== - -L'elemento centrale per lavorare con la cache è l'oggetto [api:Nette\Caching\Cache]. Ne creiamo un'istanza passando al costruttore un oggetto di storage. Questo oggetto rappresenta il luogo fisico in cui i dati verranno salvati (database, Memcached, file su disco ecc.). L'oggetto di storage lo ottenete di solito con la [dependency injection |dependency-injection:passing-dependencies] chiedendo il tipo `Nette\Caching\Storage`. L'essenziale lo imparerete nella [sezione sugli storage |#Storage]. - -.[warning] -Nella versione 3.0 l'interfaccia aveva ancora il prefisso `I`, quindi si chiamava `Nette\Caching\IStorage`. Inoltre le costanti della classe `Cache` si scrivevano in maiuscolo, per esempio `Cache::EXPIRE` invece di `Cache::Expire`. - -Per gli esempi seguenti supponiamo di avere un alias `Cache` e un'istanza di storage nella variabile `$storage`. - -```php -use Nette\Caching\Cache; - -$storage = /* ... */; // istanza di Nette\Caching\Storage -``` - -La cache è in sostanza un *key-value store*, cioè leggiamo e scriviamo i dati con delle chiavi, come negli array associativi. Le applicazioni sono composte da più parti indipendenti. Se tutte le parti usassero un unico storage (immaginate un'unica directory su disco), prima o poi si verificherebbero collisioni di chiavi. Il Nette Framework lo risolve dividendo lo spazio dello storage in namespace (concettualmente come delle sottodirectory). Ogni parte dell'applicazione lavora poi nel proprio namespace con un nome univoco e non si verifica alcuna collisione. - -Il nome del namespace si indica come secondo argomento del costruttore della classe `Cache`: - -```php -$cache = new Cache($storage, 'Full Html Pages'); -``` - -Se serve, da un'istanza esistente potete derivare una nuova cache limitata a un sotto-namespace con il metodo `derive()`: - -```php -$subCache = $cache->derive('Images'); -``` - -Ora possiamo usare l'oggetto `$cache` per leggere dalla cache e scriverci. A entrambi gli scopi serve il metodo `load()`. Il primo argomento è la chiave, il secondo un callback PHP che viene richiamato se la chiave non si trova nella cache. Il callback genera il valore, lo restituisce e il metodo `load()` lo mette in cache: - -```php -$value = $cache->load($key, function () use ($key) { - $computedValue = /* ... */; // calcolo costoso - return $computedValue; -}); -``` - -Se il secondo parametro viene omesso (`$value = $cache->load($key)`), `load()` restituisce `null` se l'elemento non si trova nella cache. - -.[tip] -È ottimo che si possano mettere in cache non solo stringhe, ma qualsiasi struttura serializzabile. Lo stesso vale per le chiavi. - -Per cancellare un elemento dalla cache serve il metodo `remove()`: - -```php -$cache->remove($key); -``` - -Un elemento si può salvare in cache anche con il metodo `$cache->save($key, $data, ?array $dependencies = null)`. In generale però si preferisce l'approccio con `load()` mostrato sopra. - - -Memoizzazione -============= - -La memoizzazione consiste nel mettere in cache il risultato della chiamata di una funzione o di un metodo, così che alla chiamata successiva con gli stessi argomenti venga restituito il risultato dalla cache invece di ricalcolarlo. - -Metodi e funzioni si possono chiamare in modo memoizzato con `call(callable $callback, ...$args)`: - -```php -$result = $cache->call('gethostbyaddr', $ip); -``` - -La funzione `gethostbyaddr()` viene quindi chiamata una sola volta per ogni argomento `$ip` diverso. Le chiamate successive con lo stesso `$ip` restituiranno il valore dalla cache. - -Si può anche creare un wrapper memoizzato attorno a un metodo o a una funzione, da chiamare in seguito: - -```php -function factorial($num) -{ - return /* ... */; -} - -$memoizedFactorial = $cache->wrap('factorial'); - -$result = $memoizedFactorial(5); // la prima volta lo calcola -$result = $memoizedFactorial(5); // la seconda volta lo restituisce dalla cache -``` - - -Scadenza e invalidazione -======================== - -Quando si usa la cache bisogna affrontare la questione di quando i dati salvati in precedenza diventano non validi. Il Nette Framework offre meccanismi per limitare la validità dei dati o per cancellarli esplicitamente (nella terminologia del framework si parla di "invalidazione"). - -La validità dei dati si imposta al momento del salvataggio, tipicamente con il terzo parametro del metodo `save()`, per esempio: - -```php -$cache->save($key, $value, [ - $cache::Expire => '20 minutes', -]); -``` - -In alternativa si può impostare con il parametro `$dependencies` passato per riferimento al callback nel metodo `load()`, per esempio: - -```php -$value = $cache->load($key, function (&$dependencies) { - $dependencies[Cache::Expire] = '20 minutes'; - return /* ... */; -}); -``` - -Oppure usando il terzo parametro del metodo `load()` stesso, per esempio: - -```php -$value = $cache->load($key, function () { - return /* ... */; -}, [Cache::Expire => '20 minutes']); -``` - -Negli esempi seguenti supporremo la seconda variante, quella che usa la variabile `$dependencies` dentro il callback. - - -Scadenza --------- - -La forma più semplice di scadenza è il limite di tempo. Questo mette in cache i dati con una validità di 20 minuti: - -```php -// accetta anche il numero di secondi oppure un timestamp UNIX -$dependencies[Cache::Expire] = '20 minutes'; -``` - -Se volete che il periodo di validità si prolunghi a ogni lettura (scadenza scorrevole), potete ottenerlo così, ma tenete presente che questo aumenta il carico della cache: - -```php -$dependencies[Cache::Sliding] = true; -``` - -Un'opzione utile è far scadere i dati quando un determinato file, o uno di più file, viene modificato. Torna utile per esempio quando si mettono in cache dati derivati dall'elaborazione di questi file. Usate percorsi assoluti. - -```php -$dependencies[Cache::Files] = '/path/to/data.yaml'; -// oppure -$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; -``` - -Possiamo far scadere un elemento della cache quando scade un altro elemento determinato (o uno di più altri). Torna utile quando si mettono in cache per esempio un'intera pagina HTML e i suoi frammenti sotto chiavi diverse. Quando un frammento cambia, tutta la pagina va invalidata. Se i frammenti sono salvati sotto chiavi come `frag1` e `frag2`, usate: - -```php -$dependencies[Cache::Items] = ['frag1', 'frag2']; -``` - -La scadenza si può governare anche con funzioni o metodi statici personalizzati. Vengono chiamati a ogni lettura per stabilire se l'elemento è ancora valido. Possiamo per esempio far scadere un elemento ogni volta che cambia la versione di PHP. Create una funzione che confronta la versione attuale con un parametro e, al salvataggio, aggiungete alle dipendenze un array nel formato `[nome della funzione, ...argomenti]`: - -```php -function checkPhpVersion($ver): bool -{ - return $ver === PHP_VERSION_ID; -} - -$dependencies[Cache::Callbacks] = [ - ['checkPhpVersion', PHP_VERSION_ID] // scade quando checkPhpVersion(...) === false -]; -``` - -Naturalmente tutti questi criteri si possono combinare. L'elemento della cache scade se almeno un criterio non è più soddisfatto. - -```php -$dependencies[Cache::Expire] = '20 minutes'; -$dependencies[Cache::Files] = '/path/to/data.yaml'; -``` - - -Invalidazione tramite tag -------------------------- - -I tag offrono un meccanismo di invalidazione molto utile. A ogni elemento salvato nella cache possiamo assegnare un elenco di tag (stringhe qualsiasi). Supponiamo per esempio di avere una pagina HTML che mostra un articolo e i suoi commenti, che vogliamo mettere in cache. Al salvataggio indichiamo i tag pertinenti: - -```php -$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"]; -``` - -Passiamo ora alla sezione di amministrazione. Qui abbiamo un form per modificare gli articoli. Insieme al salvataggio dell'articolo nel database chiamiamo il metodo `clean()` per cancellare gli elementi della cache in base al loro tag: - -```php -$cache->clean([ - $cache::Tags => ["article/$articleId"], -]); -``` - -Allo stesso modo, quando aggiungiamo un nuovo commento (o ne modifichiamo uno), dobbiamo ricordarci di invalidare il tag corrispondente: - -```php -$cache->clean([ - $cache::Tags => ["comments/$articleId"], -]); -``` - -Che cosa abbiamo ottenuto? La nostra cache HTML verrà ora invalidata (cancellata) ogni volta che cambiano l'articolo associato o i suoi commenti. Quando si modifica l'articolo con ID = 10, viene invalidato il tag `article/10` e la pagina HTML in cache che porta questo tag viene cancellata. Lo stesso accade quando viene aggiunto un nuovo commento sotto l'articolo in questione. - -.[note] -I tag richiedono un [#Journal]. - - -Invalidazione per priorità --------------------------- - -Ai singoli elementi della cache possiamo assegnare delle priorità. Questo permette una cancellazione controllata, per esempio quando la cache supera un certo limite di dimensione: - -```php -$dependencies[Cache::Priority] = 50; -``` - -Per cancellare tutti gli elementi con priorità minore o uguale a 100: - -```php -$cache->clean([ - $cache::Priority => 100, -]); -``` - -.[note] -Le priorità richiedono il cosiddetto [#Journal]. - - -Svuotare la cache ------------------ - -Il parametro `Cache::All` cancella tutto: - -```php -$cache->clean([ - $cache::All => true, -]); -``` - - -Lettura in blocco -================= - -Per leggere e scrivere nella cache in blocco serve il metodo `bulkLoad()`. Passategli un array di chiavi e restituisce un array dei valori corrispondenti: - -```php -$values = $cache->bulkLoad($keys); -``` - -Il metodo `bulkLoad()` funziona in modo simile a `load()` e accetta anch'esso un secondo parametro callback. Questo callback riceve la chiave dell'elemento che si sta generando: - -```php -$values = $cache->bulkLoad($keys, function ($key, &$dependencies) { - $computedValue = /* ... */; // calcolo costoso - return $computedValue; -}); -``` - -Al contrario, per scrivere più elementi in una volta serve il metodo `bulkSave()`, che accetta un array di coppie `chiave => valore` ed eventuali dipendenze: - -```php -$cache->bulkSave([ - $key1 => $value1, - $key2 => $value2, -], [Cache::Expire => '20 minutes']); -``` - - -Uso con PSR-16 .{data-version:3.3.1} -==================================== - -Per usare Nette Cache con l'interfaccia PSR-16 potete sfruttare il `PsrCacheAdapter`. Permette un'integrazione fluida tra Nette Cache e qualsiasi codice o libreria che si aspetta un'implementazione di cache compatibile con PSR-16. - -```php -$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage); -``` - -Ora potete usare `$psrCache` come una normale cache PSR-16: - -```php -$psrCache->set('key', 'value', 3600); // salva il valore per 1 ora -$value = $psrCache->get('key', 'default'); -``` - -L'adattatore supporta tutti i metodi definiti in PSR-16, compresi `getMultiple()`, `setMultiple()` e `deleteMultiple()`. - - -Cache dell'output -================= - -L'output si può catturare e mettere in cache in modo molto elegante: - -```php -if ($capture = $cache->capture($key)) { - - // echo ... stampa di alcuni dati - - $capture->end(); // salva l'output nella cache -} -``` - -Se l'output è già presente nella cache, il metodo `capture()` lo stampa e restituisce `null`, quindi il blocco della condizione `if` viene saltato. Altrimenti comincia a bufferizzare l'output e restituisce un oggetto `$capture`, che usate per salvare infine nella cache i dati catturati con il suo metodo `end()`. - -.[note] -Nella versione 3.0 questo metodo si chiamava `$cache->start()`. - - -Cache in Latte -============== - -La cache nei template [Latte|latte:] è molto semplice. Basta racchiudere la parte di template che volete mettere in cache tra i tag `{cache}...{/cache}`. La cache viene invalidata automaticamente ogni volta che il file sorgente del template cambia (compresi i template inclusi dentro il blocco messo in cache). I tag `{cache}` si possono annidare. Quando un blocco annidato viene invalidato (per esempio tramite un tag), viene invalidato anche il blocco genitore. - -Dentro il tag potete indicare le chiavi a cui la voce di cache sarà legata (qui la variabile `$id`), impostare un tempo di scadenza e definire i [tag di invalidazione |#Invalidazione tramite tag]. - -```latte -{cache $id, expire: '20 minutes', tags: [tag1, tag2]} - ... -{/cache} -``` - -Tutti questi parametri sono facoltativi, quindi non dovete indicare né la scadenza, né i tag, né le chiavi. - -L'uso della cache si può anche rendere condizionale con `if`: il contenuto verrà messo in cache solo se la condizione è soddisfatta: - -```latte -{cache $id, if: !$form->isSubmitted()} - {$form} -{/cache} -``` - - -Storage -======= - -Uno storage è un oggetto che rappresenta il luogo fisico in cui i dati vengono salvati. Possiamo usare un database, un server Memcached, oppure lo storage più a portata di mano: i file su disco. - -|---------------------- -| Storage | Descrizione -|---------------------- -| [#FileStorage] | Storage predefinito, salva la cache in file su disco. -| [#MemcachedStorage] | Usa un server `Memcached` per lo storage. -| [#MemoryStorage] | I dati sono conservati temporaneamente in memoria (si perdono alla fine della richiesta). -| [#SQLiteStorage] | I dati sono conservati in un file di database SQLite. -| [#DevNullStorage] | I dati non vengono affatto salvati; utile per i test. - -L'oggetto di storage lo ottenete con la [dependency injection |dependency-injection:passing-dependencies] chiedendo il tipo `Nette\Caching\Storage`. Per impostazione predefinita Nette offre un oggetto `FileStorage` che salva i dati nella sottodirectory `cache` dentro la directory dei [file temporanei |application:bootstrapping#File temporanei]. - -Lo storage predefinito lo potete cambiare nella configurazione: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - - -FileStorage ------------ - -Scrive le voci di cache in file su disco. Lo storage `Nette\Caching\Storages\FileStorage` è molto ottimizzato per le prestazioni e, cosa fondamentale, garantisce la piena atomicità delle operazioni. Che cosa significa? Che usando la cache non può capitare di leggere un file che un altro thread non ha ancora finito di scrivere, o che qualcuno lo cancelli mentre lo state leggendo. Usare questo storage di cache è quindi del tutto sicuro. - -Questo storage comprende anche un'importante funzione integrata che impedisce un'impennata estrema dell'uso della CPU quando la cache viene svuotata o è ancora "fredda" (cioè non ancora creata). Si tratta della prevenzione del cosiddetto "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede. Si verifica quando più richieste concorrenti chiedono contemporaneamente lo stesso elemento della cache (per esempio il risultato di una query SQL costosa). Se l'elemento in quel momento non è in cache, tutti questi processi potrebbero cominciare a eseguire la stessa operazione costosa (come la query SQL). Questo moltiplica il carico del server e può perfino capitare che nessun thread riesca a rispondere entro il limite di tempo, che la cache non venga creata e che l'applicazione si blocchi. Per fortuna la cache di Nette se ne occupa: quando ci sono più richieste concorrenti per lo stesso elemento, solo il primo thread lo genera. Gli altri thread aspettano e poi usano il risultato generato dal primo. - -Esempio di creazione di un `FileStorage`: - -```php -// lo storage sarà la directory '/path/to/temp' su disco -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); -``` - - -MemcachedStorage ----------------- - -Il server [Memcached |https://memcached.org] è un sistema distribuito di caching di oggetti in memoria ad alte prestazioni. Il suo adattatore in Nette è `Nette\Caching\Storages\MemcachedStorage`. Nella configurazione indicate l'indirizzo IP del server e la porta, se differisce dalla standard 11211. - -.[caution] -Richiede l'estensione PHP `memcached`. - -```neon -services: - cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5') -``` - - -MemoryStorage -------------- - -`Nette\Caching\Storages\MemoryStorage` è uno storage che tiene i dati in un array PHP. Di conseguenza i dati si perdono alla fine della richiesta. - - -SQLiteStorage -------------- - -Il database SQLite, insieme all'adattatore `Nette\Caching\Storages\SQLiteStorage`, offre un modo di mettere in cache i dati in un unico file su disco. La configurazione indica il percorso di questo file di database. - -.[caution] -Richiede le estensioni PHP `pdo` e `pdo_sqlite`. - -```neon -services: - cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db') -``` - - -DevNullStorage --------------- - -Un'implementazione particolare di storage è `Nette\Caching\Storages\DevNullStorage`, che non salva affatto i dati. È quindi adatta agli scopi di test, quando volete eliminare gli effetti della cache. - - -Usare la cache nel codice -========================= - -Quando usate la cache nel vostro codice avete due approcci principali. Il primo è ottenere l'oggetto di storage con la [dependency injection |dependency-injection:passing-dependencies] e poi creare da soli l'oggetto `Cache`: - -```php -use Nette; - -class ClassOne -{ - private Nette\Caching\Cache $cache; - - public function __construct(Nette\Caching\Storage $storage) - { - $this->cache = new Nette\Caching\Cache($storage, 'my-namespace'); - } -} -``` - -La seconda possibilità è farsi passare direttamente l'oggetto `Cache`: - -```php -class ClassTwo -{ - public function __construct( - private Nette\Caching\Cache $cache, - ) { - } -} -``` - -L'oggetto `Cache` va poi definito nella configurazione, per esempio così: - -```neon -services: - - ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') ) -``` - - -Journal -======= - -Nette conserva le informazioni su tag e priorità in un cosiddetto journal. Per impostazione predefinita a questo scopo si usa SQLite tramite il file `journal.s3db` e **sono necessarie le estensioni PHP `pdo` e `pdo_sqlite`.** - -L'implementazione del journal la potete cambiare nella configurazione: - -```neon -services: - cache.journal: MyJournal -``` - - -Servizi DI -========== - -Al container DI vengono aggiunti questi servizi: - -| Nome | Tipo | Descrizione -|---------------------------------------------------------- -| `cache.journal` | [api:Nette\Caching\Storages\Journal] | Lo storage del journal della cache -| `cache.storage` | [api:Nette\Caching\Storage] | Lo storage principale della cache - - -Disattivare la cache -==================== - -Un modo di disattivare la cache nella vostra applicazione è impostare come backend di storage [#DevNullStorage]: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - -Questa impostazione non influisce sulla cache dei template Latte né su quella del container DI, perché queste librerie non usano i servizi di `nette/caching` e gestiscono la propria cache in modo indipendente. Inoltre le loro cache [di solito non hanno bisogno di essere disattivate |nette:troubleshooting#Come disattivare la cache durante lo sviluppo?] in modalità di sviluppo. - - -Se state aggiornando a una versione più recente, guardate la pagina [aggiornamento |upgrading]. diff --git a/caching/it/@left-menu.texy b/caching/it/@left-menu.texy deleted file mode 100644 index 80fd21c6c6..0000000000 --- a/caching/it/@left-menu.texy +++ /dev/null @@ -1,13 +0,0 @@ -Nette Caching -************* -- [Panoramica |@home] -- [Aggiornamento|upgrading] - - -Letture consigliate -******************* -- [Documentazione di Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Best practice |best-practices:] -- [Risoluzione dei problemi |nette:troubleshooting] diff --git a/caching/it/@meta.texy b/caching/it/@meta.texy deleted file mode 100644 index 4647d0c8a2..0000000000 --- a/caching/it/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentazione Nette}} diff --git a/caching/it/upgrading.texy b/caching/it/upgrading.texy deleted file mode 100644 index aace8026cb..0000000000 --- a/caching/it/upgrading.texy +++ /dev/null @@ -1,20 +0,0 @@ -Aggiornamento -************* - - -Aggiornamento alla versione 3.1 -=============================== - -- il metodo `Nette\Caching\Cache::start()` è stato rinominato in `capture()` - - -Aggiornamento alla versione 2.4 -=============================== - -- la classe `Nette\Caching\Storages\FileJournal` non è più disponibile - - -Aggiornamento alla versione 2.3 -=============================== - -- l'antica e deprecata sintassi ArrayAccess `$val = $cache[$key]` oppure `$cache[$key] = $val` provoca `E_USER_DEPRECATED`; usate invece `$cache->load($key)` e `$cache->save($key, $val)` diff --git a/caching/ja/@home.texy b/caching/ja/@home.texy deleted file mode 100644 index 5108136eb1..0000000000 --- a/caching/ja/@home.texy +++ /dev/null @@ -1,502 +0,0 @@ -Nette Caching -************* - -<div class=perex> - -キャッシュは、かつて取り出すのに計算の手間がかかったデータを保存しておき、次からは速く取り出せるようにして、アプリケーションを速くします。ここでは次のことを扱います。 - -- キャッシュの使い方 -- 保管の仕組みの変え方 -- キャッシュを正しく無効にする方法 - -</div> - -Nette でのキャッシュの使い方はごく分かりやすく、それでいて洗練された必要にも応えます。性能と 100% の堅牢さを目指して作られています。よく使われる保管の仕組みのアダプタが備わっています。タグによる無効化、時間による期限切れ、キャッシュスタンピードへの守りなどに対応しています。 - - -インストール -====== - -パッケージは [Composer|best-practices:composer]でダウンロードしてインストールします。 - -```shell -composer require nette/caching -``` - - -基本の使い方 -====== - -キャッシュを扱う中心の要素は [api:Nette\Caching\Cache]オブジェクトです。そのインスタンスを作り、コンストラクタに保管の仕組みのオブジェクトを渡します。この保管のオブジェクトは、データが実際に置かれる場所(データベース、Memcached、ディスクのファイルなど)を表します。ふつうは [dependency injection |dependency-injection:passing-dependencies]で `Nette\Caching\Storage` の型を求めて手に入れます。基本は[保管の仕組みの節 |#保管の仕組み]で学べます。 - -.[warning] -バージョン 3.0 では、このインターフェースにはまだ `I` の接頭辞が付いていて、名前は `Nette\Caching\IStorage` でした。さらに `Cache` クラスの定数は大文字で書かれていて、たとえば `Cache::Expire` ではなく `Cache::EXPIRE` でした。 - -以下の例では、別名 `Cache` があり、`$storage` の変数に保管のインスタンスが入っているものとします。 - -```php -use Nette\Caching\Cache; - -$storage = /* ... */; // Nette\Caching\Storage のインスタンス -``` - -キャッシュは要するに *キーと値の保管場所* で、連想配列のようにキーでデータを読み書きします。アプリケーションは独立した複数の部分から成ります。すべての部分がひとつの保管場所(ディスクのひとつのディレクトリを想像してください)を使えば、遅かれ早かれキーの衝突が起きます。Nette Framework はこれを、保管の空間を名前空間(考え方としては下位のディレクトリのようなもの)に分けることで解決します。アプリケーションのそれぞれの部分は、一意の名前を持つ自分の名前空間の中で働くので、衝突は起きません。 - -名前空間の名前は `Cache` クラスのコンストラクタの第 2 引数で指定します。 - -```php -$cache = new Cache($storage, 'Full Html Pages'); -``` - -必要なら、既存のインスタンスから `derive()` メソッドで下位の名前空間に絞った新しいキャッシュを作れます。 - -```php -$subCache = $cache->derive('Images'); -``` - -これで `$cache` のオブジェクトでキャッシュを読み書きできます。`load()` メソッドが両方の役目を果たします。第 1 引数はキー、第 2 引数はそのキーがキャッシュに見つからないときに呼ばれる PHP のコールバックです。コールバックが値を作って返し、`load()` メソッドがそれを蓄えます。 - -```php -$value = $cache->load($key, function () use ($key) { - $computedValue = /* ... */; // 手間のかかる計算 - return $computedValue; -}); -``` - -第 2 パラメータを省くと(`$value = $cache->load($key)`)、その項目がキャッシュに見つからなければ `load()` は `null` を返します。 - -.[tip] -文字列だけでなく、直列化できるどんな構造も蓄えられるのはうれしいところです。キーについても同じです。 - -キャッシュから項目を消すには `remove()` メソッドを使います。 - -```php -$cache->remove($key); -``` - -項目は `$cache->save($key, $data, ?array $dependencies = null)` メソッドでも保存できます。とはいえ、上で見た `load()` のやり方のほうがふつうは好まれます。 - - -メモ化 -=== - -メモ化とは、関数やメソッドの呼び出しの結果を蓄えて、次に同じ引数で呼ばれたときに計算し直さず蓄えた結果を返すことです。 - -メソッドと関数は `call(callable $callback, ...$args)` でメモ化して呼べます。 - -```php -$result = $cache->call('gethostbyaddr', $ip); -``` - -こうすると `gethostbyaddr()` の関数は、`$ip` の引数の値ごとに一度だけ呼ばれます。同じ `$ip` での次の呼び出しは、蓄えられた値を返します。 - -メソッドや関数のメモ化された包みを作って、あとで呼ぶこともできます。 - -```php -function factorial($num) -{ - return /* ... */; -} - -$memoizedFactorial = $cache->wrap('factorial'); - -$result = $memoizedFactorial(5); // 一度めは計算します -$result = $memoizedFactorial(5); // 二度めはキャッシュから返します -``` - - -期限切れと無効化 -======== - -キャッシュを使うなら、前に保存したデータがいつ古くなるかという問題に向き合う必要があります。Nette Framework は、データの有効さを制限したり、はっきり消したりするしくみを備えています(フレームワークの言い方では「無効化」と呼びます)。 - -データの有効さは保存のときに決めます。ふつうは `save()` メソッドの第 3 パラメータを使います。たとえば次のようにです。 - -```php -$cache->save($key, $value, [ - $cache::Expire => '20 minutes', -]); -``` - -あるいは、`load()` メソッドのコールバックに参照で渡される `$dependencies` のパラメータで決められます。たとえば次のようにです。 - -```php -$value = $cache->load($key, function (&$dependencies) { - $dependencies[Cache::Expire] = '20 minutes'; - return /* ... */; -}); -``` - -あるいは `load()` メソッド自身の第 3 パラメータでも決められます。たとえば次のようにです。 - -```php -$value = $cache->load($key, function () { - return /* ... */; -}, [Cache::Expire => '20 minutes']); -``` - -以下の例では、コールバックの中で `$dependencies` の変数を使う 2 つめの形を前提にします。 - - -期限切れ ----- - -いちばん単純な期限切れの形は時間の制限です。次はデータを 20 分の有効期間で蓄えます。 - -```php -// 秒数や UNIX タイムスタンプも受け取ります -$dependencies[Cache::Expire] = '20 minutes'; -``` - -読むたびに有効期間を延ばしたいなら(スライド式の期限切れ)、次のようにできます。ただしキャッシュの手間が増えることに注意してください。 - -```php -$dependencies[Cache::Sliding] = true; -``` - -特定のファイル、あるいはいくつかのファイルのどれかが変わったときにデータを期限切れにするのも便利な選択肢です。たとえばそれらのファイルを処理して得たデータを蓄えるときに役立ちます。絶対パスを使ってください。 - -```php -$dependencies[Cache::Files] = '/path/to/data.yaml'; -// または -$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; -``` - -ほかの特定の項目(あるいはいくつかのうちのどれか)が期限切れになったときに、キャッシュの項目を期限切れにできます。たとえば HTML のページ全体とその断片を違うキーで蓄えるときに役立ちます。断片が変わったら、ページ全体を無効にすべきです。断片が `frag1` と `frag2` のキーで蓄えられているなら、次のようにします。 - -```php -$dependencies[Cache::Items] = ['frag1', 'frag2']; -``` - -期限切れは独自の関数や静的メソッドでも決められます。それらは読むたびに呼ばれ、その項目がまだ有効かを判断します。たとえば PHP のバージョンが変わるたびに項目を期限切れにできます。今のバージョンをパラメータと比べる関数を作り、保存のときに `[関数名, ...引数]` の形の配列を依存関係に足します。 - -```php -function checkPhpVersion($ver): bool -{ - return $ver === PHP_VERSION_ID; -} - -$dependencies[Cache::Callbacks] = [ - ['checkPhpVersion', PHP_VERSION_ID] // checkPhpVersion(...) === false のとき期限切れ -]; -``` - -もちろん、これらの基準はすべて組み合わせられます。少なくともひとつの基準が満たされなくなると、そのキャッシュの項目は期限切れになります。 - -```php -$dependencies[Cache::Expire] = '20 minutes'; -$dependencies[Cache::Files] = '/path/to/data.yaml'; -``` - - -タグによる無効化 --------- - -タグはとても役に立つ無効化のしくみです。キャッシュに蓄えるそれぞれの項目に、タグ(好きな文字列)の一覧を割り当てられます。たとえば、記事とそのコメントを表示する HTML のページを蓄えたいとしましょう。保存のときに、それに合うタグを指定します。 - -```php -$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"]; -``` - -では管理画面へ移りましょう。ここには記事を編集するフォームがあります。記事をデータベースへ保存するのと合わせて、`clean()` メソッドを呼んでタグをもとに蓄えた項目を消します。 - -```php -$cache->clean([ - $cache::Tags => ["article/$articleId"], -]); -``` - -同じように、新しいコメントを足す(あるいは編集する)ときも、対応するタグを無効にするのを忘れてはいけません。 - -```php -$cache->clean([ - $cache::Tags => ["comments/$articleId"], -]); -``` - -これで何が実現したのでしょうか。私たちの HTML のキャッシュは、それに結び付いた記事やそのコメントが変わるたびに無効化(削除)されるようになりました。ID = 10 の記事を編集すると、タグ `article/10` が無効になり、このタグを持つ蓄えられた HTML のページが消されます。その記事の下に新しいコメントが足されたときも同じです。 - -.[note] -タグには[ジャーナル |#ジャーナル]が要ります。 - - -優先度による無効化 ---------- - -キャッシュの項目ごとに優先度を割り当てられます。おかげで、たとえばキャッシュがある大きさの制限を超えたときに、制御された形で消せます。 - -```php -$dependencies[Cache::Priority] = 50; -``` - -優先度が 100 以下のすべての項目を消すには次のようにします。 - -```php -$cache->clean([ - $cache::Priority => 100, -]); -``` - -.[note] -優先度にはいわゆる[ジャーナル |#ジャーナル]が要ります。 - - -キャッシュを空にする ----------- - -`Cache::All` のパラメータはすべてを空にします。 - -```php -$cache->clean([ - $cache::All => true, -]); -``` - - -まとめて読む -====== - -キャッシュをまとめて読み書きするには `bulkLoad()` メソッドを使います。キーの配列を渡すと、それに対応する値の配列が返ります。 - -```php -$values = $cache->bulkLoad($keys); -``` - -`bulkLoad()` メソッドは `load()` と同じように働き、第 2 パラメータのコールバックも受け取ります。このコールバックは、作られる項目のキーを受け取ります。 - -```php -$values = $cache->bulkLoad($keys, function ($key, &$dependencies) { - $computedValue = /* ... */; // 手間のかかる計算 - return $computedValue; -}); -``` - -逆に複数の項目を一度に書くには `bulkSave()` メソッドを使います。これは `キー => 値` の組の配列と、省略できる依存関係を受け取ります。 - -```php -$cache->bulkSave([ - $key1 => $value1, - $key2 => $value2, -], [Cache::Expire => '20 minutes']); -``` - - -PSR-16 との組み合わせ .{data-version:3.3.1} -==================================== - -Nette Cache を PSR-16 のインターフェースで使うには、`PsrCacheAdapter` を使えます。これで Nette Cache と、PSR-16 に合うキャッシュの実装を期待するどんなコードやライブラリともなめらかに組み合わせられます。 - -```php -$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage); -``` - -これで `$psrCache` を標準の PSR-16 のキャッシュとして使えます。 - -```php -$psrCache->set('key', 'value', 3600); // 値を 1 時間蓄えます -$value = $psrCache->get('key', 'default'); -``` - -このアダプタは、`getMultiple()`、`setMultiple()`、`deleteMultiple()` も含めて PSR-16 が定めるすべてのメソッドに対応しています。 - - -出力の蓄え -===== - -出力はとても優雅に捕まえて蓄えられます。 - -```php -if ($capture = $cache->capture($key)) { - - // echo ... なんらかのデータを出力します - - $capture->end(); // 出力をキャッシュへ保存します -} -``` - -出力がすでにキャッシュにあれば、`capture()` メソッドはそれを出力して `null` を返すので、`if` の中身は飛ばされます。そうでなければ出力の蓄えを始め、`$capture` のオブジェクトを返します。捕まえたデータは、その `end()` メソッドで最後にキャッシュへ保存します。 - -.[note] -バージョン 3.0 では、このメソッドの名前は `$cache->start()` でした。 - - -Latte でのキャッシュ -============= - -[Latte|latte:]のテンプレートでのキャッシュはとても簡単です。蓄えたいテンプレートの部分を `{cache}...{/cache}` のタグで包むだけです。もとのテンプレートのファイルが変わると(蓄えられたかたまりの中で取り込まれたテンプレートも含みます)、キャッシュは自動的に無効になります。`{cache}` のタグは入れ子にできます。入れ子のかたまりが(たとえばタグで)無効になると、その親のかたまりも無効になります。 - -タグの中では、そのキャッシュの項目を結び付けるキー(ここでは変数 `$id`)を指定したり、有効期限を決めたり、[無効化のタグ |#タグによる無効化]を定めたりできます。 - -```latte -{cache $id, expire: '20 minutes', tags: [tag1, tag2]} - ... -{/cache} -``` - -これらのパラメータはすべて省略できるので、有効期限もタグもキーも指定しなくてかまいません。 - -キャッシュを使うかどうかを `if` で条件にもできます。条件が満たされたときにだけ中身が蓄えられます。 - -```latte -{cache $id, if: !$form->isSubmitted()} - {$form} -{/cache} -``` - - -保管の仕組み -====== - -保管の仕組みは、データが実際に置かれる場所を表すオブジェクトです。データベース、Memcached のサーバー、あるいはもっとも手近な保管場所であるディスクのファイルを使えます。 - -|---------------------- -| 保管の仕組み | 説明 -|---------------------- -| [#FileStorage] | 既定の保管の仕組み。キャッシュをディスクのファイルに保存します。 -| [#MemcachedStorage] | 保管に `Memcached` のサーバーを使います。 -| [#MemoryStorage] | データを一時的にメモリに置きます(リクエストの終わりに失われます)。 -| [#SQLiteStorage] | データを SQLite のデータベースのファイルに置きます。 -| [#DevNullStorage] | データは実際には保存されません。テストに役立ちます。 - -保管のオブジェクトは [dependency injection |dependency-injection:passing-dependencies]で `Nette\Caching\Storage` の型を求めて手に入れます。既定では、Nette は[一時ファイル |application:bootstrapping#一時ファイル]のディレクトリの中の `cache` の下位のディレクトリにデータを置く `FileStorage` オブジェクトを与えます。 - -既定の保管の仕組みは設定で変えられます。 - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - - -FileStorage ------------ - -キャッシュの項目をディスクのファイルに書きます。`Nette\Caching\Storages\FileStorage` の保管の仕組みは性能のために念入りに最適化されていて、そして何より操作の完全なアトミック性を保証します。それはどういうことでしょうか。このキャッシュを使っているとき、ほかのスレッドがまだ完全に書き終えていないファイルを読んでしまうことも、読んでいる最中に誰かがそれを消してしまうことも起こりえません。ですからこの保管の仕組みを使うのは完全に安全です。 - -この保管の仕組みには、キャッシュが消されたときや、まだ「冷たい」(つまりまだ作られていない)ときに CPU の使用が跳ね上がるのを防ぐ、大事な組み込みの機能もあります。これは "キャッシュスタンピード":https://en.wikipedia.org/wiki/Cache_stampede への守りとして知られています。これは、同時に走る複数のリクエストが同じキャッシュの項目(たとえば手間のかかる SQL のクエリの結果)を同時に求めたときに起こります。その項目がそのときキャッシュになければ、これらのプロセスがすべて同じ手間のかかる処理(SQL のクエリなど)を始めてしまうかもしれません。これはサーバーの負荷を何倍にもし、どのスレッドも制限時間の中で応えられず、キャッシュも作られず、アプリケーションが落ちることさえあります。幸い Nette のキャッシュはこれに対処します。同じ項目への同時のリクエストが複数あるとき、それを作るのは最初のスレッドだけです。ほかのスレッドは待って、そのあと最初のスレッドが作った結果を使います。 - -`FileStorage` を作る例です。 - -```php -// 保管場所はディスクの '/path/to/temp' のディレクトリになります -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); -``` - - -MemcachedStorage ----------------- - -[Memcached |https://memcached.org]のサーバーは、高い性能を持つ分散型のメモリのオブジェクトのキャッシュのしくみです。Nette でのそのアダプタは `Nette\Caching\Storages\MemcachedStorage` です。設定では、サーバーの IP アドレスと、標準の 11211 と違うならポートを指定します。 - -.[caution] -PHP の `memcached` 拡張が要ります。 - -```neon -services: - cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5') -``` - - -MemoryStorage -------------- - -`Nette\Caching\Storages\MemoryStorage` は、データを PHP の配列の中に持つ保管の仕組みです。ですからリクエストが終わるとデータは失われます。 - - -SQLiteStorage -------------- - -SQLite のデータベースと `Nette\Caching\Storages\SQLiteStorage` のアダプタは、ディスクのひとつのファイルの中にデータを蓄える方法を与えます。設定ではそのデータベースのファイルへのパスを指定します。 - -.[caution] -PHP の `pdo` と `pdo_sqlite` の拡張が要ります。 - -```neon -services: - cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db') -``` - - -DevNullStorage --------------- - -特別な保管の実装が `Nette\Caching\Storages\DevNullStorage` で、これは実際にはデータを何も保存しません。ですからキャッシュの影響をなくしたいテストの用途に向いています。 - - -コードでキャッシュを使う -============ - -コードでキャッシュを使うやり方は主に 2 つあります。ひとつめは、[dependency injection |dependency-injection:passing-dependencies]で保管のオブジェクトを手に入れて、`Cache` のオブジェクトを自分で作ることです。 - -```php -use Nette; - -class ClassOne -{ - private Nette\Caching\Cache $cache; - - public function __construct(Nette\Caching\Storage $storage) - { - $this->cache = new Nette\Caching\Cache($storage, 'my-namespace'); - } -} -``` - -もうひとつは、`Cache` のオブジェクトを直接求めることです。 - -```php -class ClassTwo -{ - public function __construct( - private Nette\Caching\Cache $cache, - ) { - } -} -``` - -その場合、`Cache` のオブジェクトは設定で定めなければなりません。たとえば次のようにです。 - -```neon -services: - - ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') ) -``` - - -ジャーナル -===== - -Nette はタグと優先度の情報を、いわゆるジャーナルに保存します。既定では `journal.s3db` のファイルを通して SQLite が使われ、**PHP の `pdo` と `pdo_sqlite` の拡張が要ります**。 - -ジャーナルの実装は設定で変えられます。 - -```neon -services: - cache.journal: MyJournal -``` - - -DI のサービス -======== - -DI コンテナには次のサービスが足されます。 - -| 名前 | 型 | 説明 -|---------------------------------------------------------- -| `cache.journal` | [api:Nette\Caching\Storages\Journal] | キャッシュのジャーナルの保管 -| `cache.storage` | [api:Nette\Caching\Storage] | 主なキャッシュの保管 - - -キャッシュを切る -======== - -アプリケーションでキャッシュを切るひとつの方法は、保管の仕組みを [#DevNullStorage]にすることです。 - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - -この設定は、Latte のテンプレートや DI コンテナのキャッシュには影響しません。これらのライブラリは `nette/caching` のサービスを使わず、自分でキャッシュを管理するからです。しかも開発モードでは、それらのキャッシュを[ふつう切る必要はありません |nette:troubleshooting#開発中にキャッシュを切るには]。 - - -新しい版へ上げるなら、[アップグレード |upgrading]のページをご覧ください。 diff --git a/caching/ja/@left-menu.texy b/caching/ja/@left-menu.texy deleted file mode 100644 index f536857307..0000000000 --- a/caching/ja/@left-menu.texy +++ /dev/null @@ -1,13 +0,0 @@ -Nette Caching -************* -- [概要 |@home] -- [アップグレード|upgrading] - - -関連情報 -**** -- [Nette ドキュメント |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [ベストプラクティス |best-practices:] -- [トラブルシューティング |nette:troubleshooting] diff --git a/caching/ja/@meta.texy b/caching/ja/@meta.texy deleted file mode 100644 index 43b85f3cac..0000000000 --- a/caching/ja/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette ドキュメント}} diff --git a/caching/ja/upgrading.texy b/caching/ja/upgrading.texy deleted file mode 100644 index d3d13ba310..0000000000 --- a/caching/ja/upgrading.texy +++ /dev/null @@ -1,20 +0,0 @@ -アップグレード -******* - - -バージョン 3.1 へのアップグレード -=================== - -- `Nette\Caching\Cache::start()` メソッドは `capture()` に改名されました - - -バージョン 2.4 へのアップグレード -=================== - -- クラス `Nette\Caching\Storages\FileJournal` はもう使えません - - -バージョン 2.3 へのアップグレード -=================== - -- 古くから非推奨だった ArrayAccess の書き方 `$val = $cache[$key]` や `$cache[$key] = $val` は `E_USER_DEPRECATED` を出します。代わりに `$cache->load($key)` と `$cache->save($key, $val)` を使ってください diff --git a/caching/meta.json b/caching/meta.json deleted file mode 100644 index ae86ba6a38..0000000000 --- a/caching/meta.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": "4.x", - "repo": "nette/caching", - "composer": "nette/caching", - "api": "https://api.nette.org/caching/" -} diff --git a/caching/pl/@home.texy b/caching/pl/@home.texy deleted file mode 100644 index df503eda45..0000000000 --- a/caching/pl/@home.texy +++ /dev/null @@ -1,502 +0,0 @@ -Nette Caching -************* - -<div class=perex> - -Cache przyspiesza Twoją aplikację, przechowując dane, których uzyskanie było kiedyś kosztowne obliczeniowo, i umożliwiając szybszy dostęp do nich w przyszłości. Omówimy: - -- jak używać cache -- jak zmienić magazyn zaplecza -- jak poprawnie inwalidować cache - -</div> - -Używanie cache w Nette jest bardzo proste, a mimo to pokrywa zaawansowane potrzeby cache'owania. Zaprojektowane jest z myślą o wydajności i 100-procentowej trwałości. Zawiera adaptery do najczęstszych magazynów zaplecza. Wspiera inwalidację po tagach, wygasanie po czasie, ochronę przed cache stampede i więcej. - - -Instalacja -========== - -Pobierz i zainstaluj pakiet za pomocą [Composera|best-practices:composer]: - -```shell -composer require nette/caching -``` - - -Podstawowe użycie -================= - -Kluczowym elementem pracy z cache jest obiekt [api:Nette\Caching\Cache]. Tworzymy jego instancję, przekazując konstruktorowi obiekt magazynu zaplecza. Obiekt magazynu reprezentuje fizyczne miejsce, w którym dane będą przechowywane (baza danych, Memcached, pliki na dysku itd.). Obiekt magazynu uzyskujesz zwykle przez [wstrzykiwanie zależności |dependency-injection:passing-dependencies], prosząc o typ `Nette\Caching\Storage`. Najważniejsze rzeczy poznasz w [sekcji o magazynach |#Magazyny]. - -.[warning] -W wersji 3.0 interfejs miał jeszcze przedrostek `I`, więc nazywał się `Nette\Caching\IStorage`. Poza tym stałe klasy `Cache` zapisywane były wielkimi literami, np. `Cache::EXPIRE` zamiast `Cache::Expire`. - -Dla poniższych przykładów załóżmy, że mamy alias `Cache` i instancję magazynu w zmiennej `$storage`. - -```php -use Nette\Caching\Cache; - -$storage = /* ... */; // instancja Nette\Caching\Storage -``` - -Cache to w istocie *magazyn klucz-wartość*, czyli czytamy i zapisujemy dane za pomocą kluczy, podobnie jak w tablicach asocjacyjnych. Aplikacje składają się z wielu niezależnych części. Gdyby wszystkie części używały jednego magazynu (wyobraź sobie jeden katalog na dysku), prędzej czy później doszłoby do kolizji kluczy. Nette Framework rozwiązuje to, dzieląc przestrzeń magazynu na przestrzenie nazw (koncepcyjnie jak podkatalogi). Każda część aplikacji pracuje wtedy we własnej przestrzeni nazw o unikalnej nazwie, co zapobiega jakimkolwiek kolizjom. - -Nazwę przestrzeni nazw podaj jako drugi argument konstruktora klasy `Cache`: - -```php -$cache = new Cache($storage, 'Full Html Pages'); -``` - -W razie potrzeby możesz z istniejącej instancji wyprowadzić nową cache ograniczoną do podprzestrzeni nazw metodą `derive()`: - -```php -$subCache = $cache->derive('Images'); -``` - -Teraz możemy używać obiektu `$cache` do czytania z cache i zapisywania do niej. Obu celom służy metoda `load()`. Pierwszym argumentem jest klucz, a drugim callback PHP wywoływany, gdy klucza nie ma w cache. Callback generuje wartość, zwraca ją, a metoda `load()` ją buforuje: - -```php -$value = $cache->load($key, function () use ($key) { - $computedValue = /* ... */; // kosztowne obliczenie - return $computedValue; -}); -``` - -Jeśli drugi parametr zostanie pominięty (`$value = $cache->load($key)`), `load()` zwraca `null`, gdy pozycji nie ma w cache. - -.[tip] -Świetne jest to, że buforować można dowolne struktury dające się serializować, nie tylko ciągi. To samo dotyczy kluczy. - -Do usunięcia pozycji z cache służy metoda `remove()`: - -```php -$cache->remove($key); -``` - -Pozycję możesz też zapisać do cache metodą `$cache->save($key, $data, ?array $dependencies = null)`. Zwykle preferowane jest jednak pokazane wyżej podejście z `load()`. - - -Memoizacja -========== - -Memoizacja polega na buforowaniu wyniku wywołania funkcji albo metody, żeby przy kolejnym wywołaniu z tymi samymi argumentami zwracany był wynik zbuforowany zamiast liczony ponownie. - -Metody i funkcje można wywoływać w sposób memoizowany za pomocą `call(callable $callback, ...$args)`: - -```php -$result = $cache->call('gethostbyaddr', $ip); -``` - -Funkcja `gethostbyaddr()` wywoływana jest więc tylko raz dla każdego unikalnego argumentu `$ip`. Kolejne wywołania z tym samym `$ip` zwrócą wartość z cache. - -Można też utworzyć memoizowany wrapper wokół metody albo funkcji, który można potem wywoływać: - -```php -function factorial($num) -{ - return /* ... */; -} - -$memoizedFactorial = $cache->wrap('factorial'); - -$result = $memoizedFactorial(5); // za pierwszym razem liczy -$result = $memoizedFactorial(5); // za drugim zwraca z cache -``` - - -Wygasanie i inwalidacja -======================= - -Przy używaniu cache trzeba rozwiązać kwestię tego, kiedy wcześniej zapisane dane stają się nieważne. Nette Framework daje mechanizmy ograniczania ważności danych albo jawnego ich usuwania (w terminologii frameworku nazywanego "inwalidacją"). - -Ważność danych ustawia się w momencie zapisu, zwykle trzecim parametrem metody `save()`, np.: - -```php -$cache->save($key, $value, [ - $cache::Expire => '20 minutes', -]); -``` - -Alternatywnie można ustawić ją parametrem `$dependencies` przekazywanym przez referencję do callbacku w metodzie `load()`, np.: - -```php -$value = $cache->load($key, function (&$dependencies) { - $dependencies[Cache::Expire] = '20 minutes'; - return /* ... */; -}); -``` - -Albo za pomocą 3. parametru samej metody `load()`, np.: - -```php -$value = $cache->load($key, function () { - return /* ... */; -}, [Cache::Expire => '20 minutes']); -``` - -W poniższych przykładach będziemy zakładać drugi wariant, wykorzystujący zmienną `$dependencies` wewnątrz callbacku. - - -Wygasanie ---------- - -Najprostszą formą wygasania jest limit czasowy. To buforuje dane z ważnością 20 minut: - -```php -// przyjmuje też liczbę sekund albo uniksowy timestamp -$dependencies[Cache::Expire] = '20 minutes'; -``` - -Jeśli chcesz, żeby okres ważności przedłużał się przy każdym odczycie (wygasanie przesuwane), możesz osiągnąć to tak, ale miej świadomość, że zwiększa to narzut cache: - -```php -$dependencies[Cache::Sliding] = true; -``` - -Przydatną opcją jest sprawienie, żeby dane wygasały przy modyfikacji konkretnego pliku albo jednego z kilku plików. Przydaje się to na przykład przy buforowaniu danych powstałych z przetwarzania tych plików. Używaj ścieżek absolutnych. - -```php -$dependencies[Cache::Files] = '/path/to/data.yaml'; -// albo -$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; -``` - -Możemy sprawić, żeby pozycja cache wygasała, gdy wygaśnie inna konkretna pozycja (albo jedna z kilku innych). Przydaje się to przy buforowaniu na przykład całej strony HTML i jej fragmentów pod różnymi kluczami. Gdy fragment się zmieni, cała strona powinna zostać zinwalidowana. Jeśli fragmenty przechowywane są pod kluczami `frag1` i `frag2`, użyj: - -```php -$dependencies[Cache::Items] = ['frag1', 'frag2']; -``` - -Wygasaniem można też sterować własnymi funkcjami albo metodami statycznymi. Wywoływane są przy każdym odczycie, żeby ustalić, czy pozycja jest nadal ważna. Możemy na przykład sprawić, żeby pozycja wygasała zawsze, gdy zmieni się wersja PHP. Utwórz funkcję porównującą bieżącą wersję z parametrem, a przy zapisie dodaj do zależności tablicę w formacie `[nazwa funkcji, ...argumenty]`: - -```php -function checkPhpVersion($ver): bool -{ - return $ver === PHP_VERSION_ID; -} - -$dependencies[Cache::Callbacks] = [ - ['checkPhpVersion', PHP_VERSION_ID] // wygaśnie, gdy checkPhpVersion(...) === false -]; -``` - -Naturalnie wszystkie te kryteria można łączyć. Pozycja cache wygasa, jeśli przynajmniej jedno kryterium przestaje być spełnione. - -```php -$dependencies[Cache::Expire] = '20 minutes'; -$dependencies[Cache::Files] = '/path/to/data.yaml'; -``` - - -Inwalidacja tagami ------------------- - -Tagi dają bardzo przydatny mechanizm inwalidacji. Każdej pozycji zapisanej w cache możemy przypisać listę tagów (dowolnych ciągów). Załóżmy na przykład, że mamy stronę HTML wyświetlającą artykuł i jego komentarze, którą chcemy buforować. Przy zapisie podajemy odpowiednie tagi: - -```php -$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"]; -``` - -Przejdźmy teraz do sekcji administracyjnej. Mamy tu formularz do edycji artykułów. Razem z zapisem artykułu do bazy danych wywołujemy metodę `clean()`, żeby usunąć zbuforowane pozycje na podstawie ich tagu: - -```php -$cache->clean([ - $cache::Tags => ["article/$articleId"], -]); -``` - -Podobnie przy dodawaniu nowego komentarza (albo edycji istniejącego) musimy pamiętać o zinwalidowaniu odpowiedniego tagu: - -```php -$cache->clean([ - $cache::Tags => ["comments/$articleId"], -]); -``` - -Co osiągnęliśmy? Nasza cache HTML będzie teraz inwalidowana (usuwana) zawsze, gdy zmieni się powiązany artykuł albo jego komentarze. Przy edycji artykułu o ID = 10 inwalidowany jest tag `article/10`, a zbuforowana strona HTML nosząca ten tag zostaje usunięta. To samo dzieje się przy dodaniu nowego komentarza pod danym artykułem. - -.[note] -Tagi wymagają [dziennika |#Dziennik]. - - -Inwalidacja po priorytecie --------------------------- - -Poszczególnym pozycjom cache możemy przypisać priorytety. Pozwala to na kontrolowane usuwanie, na przykład gdy cache przekroczy określony limit rozmiaru: - -```php -$dependencies[Cache::Priority] = 50; -``` - -Żeby usunąć wszystkie pozycje o priorytecie równym 100 albo mniejszym: - -```php -$cache->clean([ - $cache::Priority => 100, -]); -``` - -.[note] -Priorytety wymagają tak zwanego [dziennika |#Dziennik]. - - -Czyszczenie cache ------------------ - -Parametr `Cache::All` czyści wszystko: - -```php -$cache->clean([ - $cache::All => true, -]); -``` - - -Odczyt masowy -============= - -Do masowego odczytu i zapisu do cache służy metoda `bulkLoad()`. Przekaż jej tablicę kluczy, a zwróci tablicę odpowiadających wartości: - -```php -$values = $cache->bulkLoad($keys); -``` - -Metoda `bulkLoad()` działa podobnie do `load()`, przyjmuje też drugi parametr będący callbackiem. Callback ten otrzymuje klucz generowanej pozycji: - -```php -$values = $cache->bulkLoad($keys, function ($key, &$dependencies) { - $computedValue = /* ... */; // kosztowne obliczenie - return $computedValue; -}); -``` - -Odwrotnie, żeby zapisać wiele pozycji naraz, użyj metody `bulkSave()`, która przyjmuje tablicę par `klucz => wartość` i opcjonalne zależności: - -```php -$cache->bulkSave([ - $key1 => $value1, - $key2 => $value2, -], [Cache::Expire => '20 minutes']); -``` - - -Użycie z PSR-16 .{data-version:3.3.1} -===================================== - -Żeby używać Nette Cache z interfejsem PSR-16, możesz wykorzystać `PsrCacheAdapter`. Umożliwia płynną integrację między Nette Cache a dowolnym kodem albo biblioteką oczekującą implementacji cache zgodnej z PSR-16. - -```php -$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage); -``` - -Teraz możesz używać `$psrCache` jako standardowej cache PSR-16: - -```php -$psrCache->set('key', 'value', 3600); // zapisuje wartość na 1 godzinę -$value = $psrCache->get('key', 'default'); -``` - -Adapter wspiera wszystkie metody zdefiniowane w PSR-16, wraz z `getMultiple()`, `setMultiple()` i `deleteMultiple()`. - - -Buforowanie wyjścia -=================== - -Wyjście można bardzo elegancko przechwycić i zbuforować: - -```php -if ($capture = $cache->capture($key)) { - - // echo ... wypisanie jakichś danych - - $capture->end(); // zapisuje wyjście do cache -} -``` - -Jeśli wyjście jest już w cache, metoda `capture()` wypisuje je i zwraca `null`, więc blok warunku `if` zostaje pominięty. W przeciwnym razie zaczyna buforować wyjście i zwraca obiekt `$capture`, którym ostatecznie zapisujesz przechwycone dane do cache metodą `end()`. - -.[note] -W wersji 3.0 metoda ta nazywała się `$cache->start()`. - - -Buforowanie w Latte -=================== - -Buforowanie w szablonach [Latte|latte:] jest bardzo proste. Wystarczy opakować część szablonu, którą chcesz buforować, tagami `{cache}...{/cache}`. Cache inwalidowana jest automatycznie zawsze, gdy zmieni się źródłowy plik szablonu (wraz z wszystkimi szablonami dołączonymi wewnątrz buforowanego bloku). Tagi `{cache}` można zagnieżdżać. Gdy zagnieżdżony blok zostanie zinwalidowany (np. tagiem), inwalidowany jest też jego blok nadrzędny. - -Wewnątrz tagu możesz podać klucze, do których wpis cache będzie przypisany (tutaj zmienna `$id`), ustawić czas wygaśnięcia i zdefiniować [tagi inwalidacyjne |#Inwalidacja tagami]. - -```latte -{cache $id, expire: '20 minutes', tags: [tag1, tag2]} - ... -{/cache} -``` - -Wszystkie te parametry są opcjonalne, więc nie musisz podawać ani wygaśnięcia, ani tagów, ani nawet kluczy. - -Użycie buforowania można też uzależnić od warunku za pomocą `if`: treść zostanie zbuforowana tylko wtedy, gdy warunek jest spełniony: - -```latte -{cache $id, if: !$form->isSubmitted()} - {$form} -{/cache} -``` - - -Magazyny -======== - -Magazyn to obiekt reprezentujący fizyczne miejsce przechowywania danych. Możemy użyć bazy danych, serwera Memcached albo najłatwiej dostępnego magazynu: plików na dysku. - -|---------------------- -| Magazyn | Opis -|---------------------- -| [#FileStorage] | Magazyn domyślny, zapisuje cache do plików na dysku. -| [#MemcachedStorage] | Do przechowywania używa serwera `Memcached`. -| [#MemoryStorage] | Dane przechowywane są tymczasowo w pamięci (giną na końcu żądania). -| [#SQLiteStorage] | Dane przechowywane są w pliku bazy SQLite. -| [#DevNullStorage] | Dane nie są faktycznie przechowywane; przydatne w testach. - -Obiekt magazynu uzyskujesz przez [wstrzykiwanie zależności |dependency-injection:passing-dependencies], prosząc o typ `Nette\Caching\Storage`. Domyślnie Nette dostarcza obiekt `FileStorage`, który przechowuje dane w podkatalogu `cache` w katalogu na [pliki tymczasowe |application:bootstrapping#Pliki tymczasowe]. - -Domyślny magazyn możesz zmienić w konfiguracji: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - - -FileStorage ------------ - -Zapisuje wpisy cache do plików na dysku. Magazyn `Nette\Caching\Storages\FileStorage` jest mocno zoptymalizowany pod kątem wydajności i, co kluczowe, zapewnia pełną atomowość operacji. Co to znaczy? Przy używaniu cache nie może się zdarzyć, że odczytasz plik, którego inny wątek jeszcze nie dopisał do końca, albo że ktoś usunie go, gdy Ty go czytasz. Używanie tego magazynu cache jest więc całkowicie bezpieczne. - -Magazyn ten zawiera też ważną wbudowaną funkcję zapobiegającą ekstremalnemu skokowi zużycia CPU, gdy cache zostanie wyczyszczona albo jest jeszcze "zimna" (czyli jeszcze nie powstała). Znane jest to jako zapobieganie "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede. Zjawisko to występuje, gdy wiele równoległych żądań prosi jednocześnie o tę samą pozycję z cache (np. o wynik kosztownego zapytania SQL). Jeśli pozycji akurat nie ma w cache, wszystkie te procesy mogą zacząć wykonywać tę samą kosztowną operację (jak zapytanie SQL). Zwielokrotnia to obciążenie serwera, a może się nawet zdarzyć, że żaden wątek nie zdąży odpowiedzieć w limicie czasu, cache nie powstanie, a aplikacja może się wysypać. Na szczęście cache Nette sobie z tym radzi: gdy pojawia się wiele równoległych żądań o tę samą pozycję, generuje ją tylko pierwszy wątek. Pozostałe wątki czekają, a potem używają wyniku wygenerowanego przez pierwszy. - -Przykład utworzenia `FileStorage`: - -```php -// magazynem będzie katalog '/path/to/temp' na dysku -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); -``` - - -MemcachedStorage ----------------- - -Serwer [Memcached |https://memcached.org] to wysokowydajny rozproszony system buforowania obiektów w pamięci. Jego adapterem w Nette jest `Nette\Caching\Storages\MemcachedStorage`. W konfiguracji podaj adres IP serwera i port, jeśli różni się od standardowego 11211. - -.[caution] -Wymaga rozszerzenia PHP `memcached`. - -```neon -services: - cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5') -``` - - -MemoryStorage -------------- - -`Nette\Caching\Storages\MemoryStorage` to magazyn trzymający dane w tablicy PHP. Dane giną więc, gdy żądanie się kończy. - - -SQLiteStorage -------------- - -Baza SQLite wraz z adapterem `Nette\Caching\Storages\SQLiteStorage` daje sposób buforowania danych w jednym pliku na dysku. Konfiguracja podaje ścieżkę do tego pliku bazy. - -.[caution] -Wymaga rozszerzeń PHP `pdo` i `pdo_sqlite`. - -```neon -services: - cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db') -``` - - -DevNullStorage --------------- - -Szczególną implementacją magazynu jest `Nette\Caching\Storages\DevNullStorage`, który faktycznie nie przechowuje żadnych danych. Nadaje się więc do celów testowych, gdy chcesz wyeliminować wpływ buforowania. - - -Używanie cache w kodzie -======================= - -Przy używaniu buforowania w swoim kodzie masz dwa główne podejścia. Pierwsze to uzyskanie obiektu magazynu przez [wstrzykiwanie zależności |dependency-injection:passing-dependencies], a potem samodzielne utworzenie obiektu `Cache`: - -```php -use Nette; - -class ClassOne -{ - private Nette\Caching\Cache $cache; - - public function __construct(Nette\Caching\Storage $storage) - { - $this->cache = new Nette\Caching\Cache($storage, 'my-namespace'); - } -} -``` - -Druga możliwość to poproszenie bezpośrednio o obiekt `Cache`: - -```php -class ClassTwo -{ - public function __construct( - private Nette\Caching\Cache $cache, - ) { - } -} -``` - -Obiekt `Cache` trzeba wtedy zdefiniować w konfiguracji, na przykład tak: - -```neon -services: - - ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') ) -``` - - -Dziennik -======== - -Nette przechowuje informacje o tagach i priorytetach w tak zwanym dzienniku. Domyślnie używany jest do tego SQLite przez plik `journal.s3db`, a **wymagane są rozszerzenia PHP `pdo` i `pdo_sqlite`.** - -Implementację dziennika możesz zmienić w konfiguracji: - -```neon -services: - cache.journal: MyJournal -``` - - -Usługi DI -========= - -Do kontenera DI dodawane są te usługi: - -| Nazwa | Typ | Opis -|---------------------------------------------------------- -| `cache.journal` | [api:Nette\Caching\Storages\Journal] | Magazyn dziennika cache -| `cache.storage` | [api:Nette\Caching\Storage] | Główny magazyn cache - - -Wyłączenie cache -================ - -Jednym ze sposobów wyłączenia buforowania w Twojej aplikacji jest ustawienie magazynu zaplecza na [#DevNullStorage]: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - -Ustawienie to nie wpływa na buforowanie szablonów Latte ani kontenera DI, bo biblioteki te nie korzystają z usług `nette/caching` i zarządzają swoją cache niezależnie. Poza tym ich cache [zwykle nie trzeba wyłączać |nette:troubleshooting#Jak wyłączyć cache w trakcie tworzenia?] w trybie deweloperskim. - - -Jeśli aktualizujesz do nowszej wersji, zajrzyj na stronę [aktualizacji |upgrading]. diff --git a/caching/pl/@left-menu.texy b/caching/pl/@left-menu.texy deleted file mode 100644 index 4aafd59703..0000000000 --- a/caching/pl/@left-menu.texy +++ /dev/null @@ -1,13 +0,0 @@ -Nette Caching -************* -- [Przegląd |@home] -- [Aktualizacja|upgrading] - - -Dalsza lektura -************** -- [Dokumentacja Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Dobre praktyki |best-practices:] -- [Rozwiązywanie problemów |nette:troubleshooting] diff --git a/caching/pl/@meta.texy b/caching/pl/@meta.texy deleted file mode 100644 index 61ac92d1af..0000000000 --- a/caching/pl/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Dokumentacja Nette}} diff --git a/caching/pl/upgrading.texy b/caching/pl/upgrading.texy deleted file mode 100644 index 1c10c21580..0000000000 --- a/caching/pl/upgrading.texy +++ /dev/null @@ -1,20 +0,0 @@ -Aktualizacja -************ - - -Aktualizacja do wersji 3.1 -========================== - -- metoda `Nette\Caching\Cache::start()` została przemianowana na `capture()` - - -Aktualizacja do wersji 2.4 -========================== - -- klasa `Nette\Caching\Storages\FileJournal` nie jest już dostępna - - -Aktualizacja do wersji 2.3 -========================== - -- pradawna i przestarzała składnia ArrayAccess `$val = $cache[$key]` albo `$cache[$key] = $val` wywołuje `E_USER_DEPRECATED`; używaj zamiast tego `$cache->load($key)` i `$cache->save($key, $val)` diff --git a/caching/ru/@home.texy b/caching/ru/@home.texy deleted file mode 100644 index e98ef446bf..0000000000 --- a/caching/ru/@home.texy +++ /dev/null @@ -1,502 +0,0 @@ -Nette Caching -************* - -<div class=perex> - -Кеш ускоряет ваше приложение, сохраняя данные, получение которых однажды обошлось дорого, и позволяя быстрее обращаться к ним в дальнейшем. Мы разберём: - -- как использовать кеш -- как сменить хранилище -- как правильно сбрасывать кеш - -</div> - -Использовать кеш в Nette очень просто, и при этом он покрывает продуманные потребности кеширования. Он рассчитан на производительность и стопроцентную надёжность. В нём есть адаптеры для самых распространённых хранилищ. Он поддерживает сброс по тегам, истечение по времени, защиту от cache stampede и не только. - - -Установка -========= - -Скачайте и установите пакет с помощью [Composer|best-practices:composer]: - -```shell -composer require nette/caching -``` - - -Основы использования -==================== - -Основной элемент для работы с кешем - объект [api:Nette\Caching\Cache]. Мы создаём его экземпляр, передавая в конструктор объект хранилища. Этот объект хранилища представляет физическое место, где будут храниться данные (база данных, Memcached, файлы на диске и т. д.). Обычно вы получаете объект хранилища через [внедрение зависимостей |dependency-injection:passing-dependencies], запросив тип `Nette\Caching\Storage`. Самое важное вы узнаете в разделе [Хранилища |#Хранилища]. - -.[warning] -В версии 3.0 у интерфейса ещё была приставка `I`, так что имя было `Nette\Caching\IStorage`. Кроме того, константы класса `Cache` записывались прописными буквами, например `Cache::EXPIRE` вместо `Cache::Expire`. - -В следующих примерах будем считать, что у нас есть псевдоним `Cache` и экземпляр хранилища в переменной `$storage`. - -```php -use Nette\Caching\Cache; - -$storage = /* ... */; // экземпляр Nette\Caching\Storage -``` - -Кеш - это по сути *хранилище ключ-значение*, то есть мы читаем и пишем данные по ключам, как в ассоциативном массиве. Приложения состоят из нескольких независимых частей. Если бы все части использовали одно хранилище (представьте себе один каталог на диске), рано или поздно возникло бы столкновение ключей. Nette Framework решает это разделением пространства хранилища на пространства имён (по сути на подкаталоги). Каждая часть приложения тогда работает в собственном пространстве имён с уникальным именем, и никаких столкновений возникнуть не может. - -Имя пространства имён укажите вторым аргументом конструктора класса `Cache`: - -```php -$cache = new Cache($storage, 'Full Html Pages'); -``` - -При необходимости от существующего экземпляра можно вывести новый кеш, ограниченный подпространством имён, методом `derive()`: - -```php -$subCache = $cache->derive('Images'); -``` - -Теперь мы можем использовать объект `$cache` для чтения из кеша и записи в него. Обеим задачам служит метод `load()`. Первый аргумент - ключ, а второй - PHP-callback, который вызывается, если ключ в кеше не найден. Callback порождает значение, возвращает его, а метод `load()` его кеширует: - -```php -$value = $cache->load($key, function () use ($key) { - $computedValue = /* ... */; // дорогое вычисление - return $computedValue; -}); -``` - -Если второй параметр опущен (`$value = $cache->load($key)`), `load()` возвращает `null`, когда элемента в кеше нет. - -.[tip] -Прекрасно, что кешировать можно любые сериализуемые структуры, а не только строки. То же относится и к ключам. - -Чтобы удалить элемент из кеша, используйте метод `remove()`: - -```php -$cache->remove($key); -``` - -Сохранить элемент в кеш можно и методом `$cache->save($key, $data, ?array $dependencies = null)`. Однако подход с `load()`, показанный выше, обычно предпочтительнее. - - -Мемоизация -========== - -Мемоизация состоит в кешировании результата вызова функции или метода, так что при следующем вызове с теми же аргументами вместо повторного вычисления возвращается закешированный результат. - -Методы и функции можно вызывать мемоизированно с помощью `call(callable $callback, ...$args)`: - -```php -$result = $cache->call('gethostbyaddr', $ip); -``` - -Функция `gethostbyaddr()` тем самым вызывается только один раз для каждого уникального аргумента `$ip`. Последующие вызовы с тем же `$ip` вернут закешированное значение. - -Можно создать и мемоизированную обёртку вокруг метода или функции, которую затем вызывать: - -```php -function factorial($num) -{ - return /* ... */; -} - -$memoizedFactorial = $cache->wrap('factorial'); - -$result = $memoizedFactorial(5); // в первый раз вычисляет -$result = $memoizedFactorial(5); // во второй раз возвращает из кеша -``` - - -Истечение и сброс -================= - -При использовании кеширования нужно решить вопрос о том, когда ранее сохранённые данные становятся недействительными. Nette Framework предоставляет механизмы для ограничения срока действия данных или их явного удаления (на языке фреймворка это называется "инвалидацией"). - -Срок действия данных задаётся в момент сохранения, обычно третьим параметром метода `save()`, например: - -```php -$cache->save($key, $value, [ - $cache::Expire => '20 minutes', -]); -``` - -Как вариант, его можно задать через параметр `$dependencies`, передаваемый по ссылке в callback метода `load()`, например: - -```php -$value = $cache->load($key, function (&$dependencies) { - $dependencies[Cache::Expire] = '20 minutes'; - return /* ... */; -}); -``` - -Либо через 3-й параметр самого метода `load()`, например: - -```php -$value = $cache->load($key, function () { - return /* ... */; -}, [Cache::Expire => '20 minutes']); -``` - -В следующих примерах будем исходить из второго варианта, использующего переменную `$dependencies` внутри callback'а. - - -Истечение ---------- - -Простейший вид истечения - ограничение по времени. Так данные кешируются со сроком действия 20 минут: - -```php -// принимает и количество секунд, и метку времени UNIX -$dependencies[Cache::Expire] = '20 minutes'; -``` - -Если вы хотите, чтобы срок действия продлевался при каждом чтении (скользящее истечение), добиться этого можно так, но учтите, что это увеличивает накладные расходы кеша: - -```php -$dependencies[Cache::Sliding] = true; -``` - -Полезная возможность - дать данным истечь при изменении определённого файла или одного из нескольких файлов. Это удобно, например, при кешировании данных, полученных обработкой этих файлов. Используйте абсолютные пути. - -```php -$dependencies[Cache::Files] = '/path/to/data.yaml'; -// либо -$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; -``` - -Мы можем сделать так, чтобы элемент кеша истёк, когда истечёт другой конкретный элемент (или один из нескольких). Это удобно, когда мы кешируем, например, целую HTML-страницу и её фрагменты под разными ключами. Когда фрагмент меняется, вся страница должна быть сброшена. Если фрагменты сохранены под ключами вроде `frag1` и `frag2`, используйте: - -```php -$dependencies[Cache::Items] = ['frag1', 'frag2']; -``` - -Истечением можно управлять и с помощью собственных функций или статических методов. Они вызываются при каждом чтении, чтобы определить, действителен ли элемент ещё. Например, мы можем сделать так, чтобы элемент истекал всякий раз при смене версии PHP. Создайте функцию, которая сравнивает текущую версию с параметром, и при сохранении добавьте в зависимости массив вида `[имя функции, ...аргументы]`: - -```php -function checkPhpVersion($ver): bool -{ - return $ver === PHP_VERSION_ID; -} - -$dependencies[Cache::Callbacks] = [ - ['checkPhpVersion', PHP_VERSION_ID] // истечёт, когда checkPhpVersion(...) === false -]; -``` - -Естественно, все эти условия можно сочетать. Элемент кеша истекает, если перестало выполняться хотя бы одно из них. - -```php -$dependencies[Cache::Expire] = '20 minutes'; -$dependencies[Cache::Files] = '/path/to/data.yaml'; -``` - - -Сброс по тегам --------------- - -Теги дают очень удобный механизм сброса. Каждому сохранённому в кеш элементу мы можем присвоить список тегов (произвольных строк). Например, допустим, у нас есть HTML-страница, показывающая статью и комментарии к ней, которую мы хотим закешировать. При сохранении мы указываем соответствующие теги: - -```php -$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"]; -``` - -Теперь перейдём в административную часть. Здесь у нас есть форма редактирования статей. Вместе с сохранением статьи в базу данных мы вызываем метод `clean()`, чтобы удалить закешированные элементы по их тегу: - -```php -$cache->clean([ - $cache::Tags => ["article/$articleId"], -]); -``` - -Точно так же при добавлении нового комментария (или его редактировании) мы должны не забыть сбросить соответствующий тег: - -```php -$cache->clean([ - $cache::Tags => ["comments/$articleId"], -]); -``` - -Чего мы добились? Наш HTML-кеш теперь будет сбрасываться (удаляться) всякий раз, когда меняется связанная статья или комментарии к ней. При редактировании статьи с ID = 10 сбрасывается тег `article/10`, и закешированная HTML-страница, несущая этот тег, удаляется. То же самое происходит при добавлении нового комментария под соответствующей статьёй. - -.[note] -Теги требуют [журнала |#Журнал]. - - -Сброс по приоритету -------------------- - -Отдельным элементам кеша можно присвоить приоритеты. Это позволяет управляемо удалять их, например когда кеш превышает определённый предел размера: - -```php -$dependencies[Cache::Priority] = 50; -``` - -Чтобы удалить все элементы с приоритетом, равным 100 или меньше: - -```php -$cache->clean([ - $cache::Priority => 100, -]); -``` - -.[note] -Приоритеты требуют так называемого [журнала |#Журнал]. - - -Очистка кеша ------------- - -Параметр `Cache::All` очищает всё: - -```php -$cache->clean([ - $cache::All => true, -]); -``` - - -Массовое чтение -=============== - -Для массового чтения из кеша и записи в него служит метод `bulkLoad()`. Передайте ему массив ключей, и он вернёт массив соответствующих значений: - -```php -$values = $cache->bulkLoad($keys); -``` - -Метод `bulkLoad()` работает похоже на `load()` и тоже принимает вторым параметром callback. Этот callback получает ключ порождаемого элемента: - -```php -$values = $cache->bulkLoad($keys, function ($key, &$dependencies) { - $computedValue = /* ... */; // дорогое вычисление - return $computedValue; -}); -``` - -И наоборот, чтобы записать сразу несколько элементов, используйте метод `bulkSave()`, который принимает массив пар `ключ => значение` и необязательные зависимости: - -```php -$cache->bulkSave([ - $key1 => $value1, - $key2 => $value2, -], [Cache::Expire => '20 minutes']); -``` - - -Использование с PSR-16 .{data-version:3.3.1} -============================================ - -Чтобы использовать Nette Cache с интерфейсом PSR-16, можно воспользоваться `PsrCacheAdapter`. Он даёт бесшовную интеграцию между Nette Cache и любым кодом или библиотекой, ожидающей реализацию кеша, совместимую с PSR-16. - -```php -$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage); -``` - -Теперь вы можете использовать `$psrCache` как обычный кеш PSR-16: - -```php -$psrCache->set('key', 'value', 3600); // сохраняет значение на 1 час -$value = $psrCache->get('key', 'default'); -``` - -Адаптер поддерживает все методы, определённые в PSR-16, включая `getMultiple()`, `setMultiple()` и `deleteMultiple()`. - - -Кеширование вывода -================== - -Вывод можно очень изящно перехватить и закешировать: - -```php -if ($capture = $cache->capture($key)) { - - // echo ... выводим какие-то данные - - $capture->end(); // сохраняем вывод в кеш -} -``` - -Если вывод уже есть в кеше, метод `capture()` его выводит и возвращает `null`, так что блок условия `if` пропускается. Иначе он начинает буферизовать вывод и возвращает объект `$capture`, которым вы в конце сохраняете перехваченные данные в кеш через его метод `end()`. - -.[note] -В версии 3.0 этот метод назывался `$cache->start()`. - - -Кеширование в Latte -=================== - -Кеширование в шаблонах [Latte|latte:] совсем просто. Достаточно обернуть часть шаблона, которую вы хотите закешировать, тегами `{cache}...{/cache}`. Кеш автоматически сбрасывается всякий раз, когда меняется исходный файл шаблона (включая любые шаблоны, подключённые внутри кешируемого блока). Теги `{cache}` можно вкладывать. Когда сбрасывается вложенный блок (например, по тегу), сбрасывается и родительский блок. - -Внутри тега можно указать ключи, к которым будет привязана запись кеша (здесь переменная `$id`), задать время истечения и определить [теги сброса |#Сброс по тегам]. - -```latte -{cache $id, expire: '20 minutes', tags: [tag1, tag2]} - ... -{/cache} -``` - -Все эти параметры необязательны, так что указывать ни истечение, ни теги, ни даже ключи не обязательно. - -Использование кеширования можно сделать и условным через `if`: содержимое будет закешировано, только если условие выполнено: - -```latte -{cache $id, if: !$form->isSubmitted()} - {$form} -{/cache} -``` - - -Хранилища -========= - -Хранилище - объект, представляющий физическое место, где хранятся данные. Мы можем использовать базу данных, сервер Memcached или самое доступное хранилище: файлы на диске. - -|---------------------- -| Хранилище | Описание -|---------------------- -| [#FileStorage] | Хранилище по умолчанию, сохраняет кеш в файлы на диске. -| [#MemcachedStorage] | Для хранения использует сервер `Memcached`. -| [#MemoryStorage] | Данные временно хранятся в памяти (теряются в конце запроса). -| [#SQLiteStorage] | Данные хранятся в файле базы данных SQLite. -| [#DevNullStorage] | Данные на самом деле не хранятся; полезно для тестирования. - -Объект хранилища вы получаете через [внедрение зависимостей |dependency-injection:passing-dependencies], запросив тип `Nette\Caching\Storage`. По умолчанию Nette предоставляет объект `FileStorage`, который хранит данные в подкаталоге `cache` внутри каталога для [временных файлов |application:bootstrapping#Временные файлы]. - -Изменить хранилище по умолчанию можно в конфигурации: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - - -FileStorage ------------ - -Записывает записи кеша в файлы на диске. Хранилище `Nette\Caching\Storages\FileStorage` сильно оптимизировано по производительности и, что принципиально важно, обеспечивает полную атомарность операций. Что это значит? При использовании кеша не может случиться, что вы прочитаете файл, который другой поток ещё не дописал до конца, или что кто-то удалит его, пока вы читаете. Поэтому использование этого хранилища кеша совершенно безопасно. - -В этом хранилище есть и важная встроенная возможность, предотвращающая чрезмерный всплеск нагрузки на процессор, когда кеш очищен или ещё "холодный" (то есть ещё не создан). Это защита от так называемого "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede. Он возникает, когда несколько одновременных запросов разом просят один и тот же элемент кеша (например, результат дорогого SQL-запроса). Если элемента в кеше в этот момент нет, все эти процессы могли бы начать выполнять одну и ту же дорогую операцию (тот же SQL-запрос). Это многократно увеличивает нагрузку на сервер, и может даже случиться, что ни один поток не успеет ответить в отведённое время, кеш не создастся, а приложение рухнет. К счастью, кеш Nette с этим справляется: при нескольких одновременных запросах на один и тот же элемент порождает его только первый поток. Остальные потоки ждут и затем используют результат, порождённый первым. - -Пример создания `FileStorage`: - -```php -// хранилищем будет каталог '/path/to/temp' на диске -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); -``` - - -MemcachedStorage ----------------- - -Сервер [Memcached |https://memcached.org] - высокопроизводительная распределённая система кеширования объектов в памяти. Его адаптер в Nette - `Nette\Caching\Storages\MemcachedStorage`. В конфигурации укажите IP-адрес сервера и порт, если он отличается от стандартного 11211. - -.[caution] -Требует PHP-расширения `memcached`. - -```neon -services: - cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5') -``` - - -MemoryStorage -------------- - -`Nette\Caching\Storages\MemoryStorage` - хранилище, которое держит данные в массиве PHP. Соответственно, данные теряются в конце запроса. - - -SQLiteStorage -------------- - -База данных SQLite вместе с адаптером `Nette\Caching\Storages\SQLiteStorage` даёт способ кешировать данные в одном файле на диске. В конфигурации указывается путь к этому файлу базы данных. - -.[caution] -Требует PHP-расширений `pdo` и `pdo_sqlite`. - -```neon -services: - cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db') -``` - - -DevNullStorage --------------- - -Особая реализация хранилища - `Nette\Caching\Storages\DevNullStorage`, которая на самом деле никаких данных не хранит. Поэтому она подходит для тестирования, когда вы хотите исключить влияние кеширования. - - -Использование кеша в коде -========================= - -При использовании кеширования в своём коде есть два основных подхода. Первый - получить объект хранилища через [внедрение зависимостей |dependency-injection:passing-dependencies] и затем создать объект `Cache` самому: - -```php -use Nette; - -class ClassOne -{ - private Nette\Caching\Cache $cache; - - public function __construct(Nette\Caching\Storage $storage) - { - $this->cache = new Nette\Caching\Cache($storage, 'my-namespace'); - } -} -``` - -Второй вариант - запросить объект `Cache` напрямую: - -```php -class ClassTwo -{ - public function __construct( - private Nette\Caching\Cache $cache, - ) { - } -} -``` - -Объект `Cache` тогда нужно определить в конфигурации, например так: - -```neon -services: - - ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') ) -``` - - -Журнал -====== - -Сведения о тегах и приоритетах Nette хранит в так называемом журнале. По умолчанию для этого используется SQLite через файл `journal.s3db`, и **требуются PHP-расширения `pdo` и `pdo_sqlite`**. - -Изменить реализацию журнала можно в конфигурации: - -```neon -services: - cache.journal: MyJournal -``` - - -Сервисы DI -========== - -Эти сервисы добавляются в DI-контейнер: - -| Имя | Тип | Описание -|---------------------------------------------------------- -| `cache.journal` | [api:Nette\Caching\Storages\Journal] | Хранилище журнала кеша -| `cache.storage` | [api:Nette\Caching\Storage] | Основное хранилище кеша - - -Отключение кеша -=============== - -Один из способов отключить кеширование в приложении - задать в качестве хранилища [#DevNullStorage]: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - -Эта настройка не влияет на кеширование шаблонов Latte или DI-контейнера, потому что эти библиотеки не используют сервисы `nette/caching` и управляют своим кешем самостоятельно. Кроме того, их кеш [обычно не нужно отключать |nette:troubleshooting#Как отключить кеш при разработке?] в режиме разработки. - - -Если вы переходите на более новую версию, посмотрите страницу [обновления |upgrading]. diff --git a/caching/ru/@left-menu.texy b/caching/ru/@left-menu.texy deleted file mode 100644 index 251951e6cd..0000000000 --- a/caching/ru/@left-menu.texy +++ /dev/null @@ -1,13 +0,0 @@ -Nette Caching -************* -- [Обзор |@home] -- [Обновление|upgrading] - - -Дополнительные материалы -************************ -- [Документация Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Лучшие практики |best-practices:] -- [Устранение неполадок |nette:troubleshooting] diff --git a/caching/ru/@meta.texy b/caching/ru/@meta.texy deleted file mode 100644 index 7f329adfce..0000000000 --- a/caching/ru/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документация Nette}} diff --git a/caching/ru/upgrading.texy b/caching/ru/upgrading.texy deleted file mode 100644 index d013c0ef2e..0000000000 --- a/caching/ru/upgrading.texy +++ /dev/null @@ -1,20 +0,0 @@ -Обновление -********** - - -Обновление до версии 3.1 -======================== - -- метод `Nette\Caching\Cache::start()` переименован в `capture()` - - -Обновление до версии 2.4 -======================== - -- класс `Nette\Caching\Storages\FileJournal` больше не доступен - - -Обновление до версии 2.3 -======================== - -- древний и устаревший синтаксис ArrayAccess `$val = $cache[$key]` или `$cache[$key] = $val` вызывает `E_USER_DEPRECATED`; используйте вместо него `$cache->load($key)` и `$cache->save($key, $val)` diff --git a/caching/tr/@home.texy b/caching/tr/@home.texy deleted file mode 100644 index e74c679329..0000000000 --- a/caching/tr/@home.texy +++ /dev/null @@ -1,502 +0,0 @@ -Nette Caching -************* - -<div class=perex> - -Önbellek, bir zamanlar hesaplaması pahalı olan veriyi saklayarak uygulamanızı hızlandırır ve gelecekte ona daha hızlı erişilmesini sağlar. Şunları ele alacağız: - -- önbelleğin nasıl kullanılacağı -- depo arka ucunun nasıl değiştirileceği -- önbelleğin doğru biçimde nasıl geçersiz kılınacağı - -</div> - -Nette'de önbelleği kullanmak çok basittir, yine de gelişmiş önbellekleme gereksinimlerini karşılar. Başarım ve %100 dayanıklılık için tasarlanmıştır. En yaygın depo arka uçları için adaptörler içerir. Etikete dayalı geçersiz kılmayı, zaman aşımını, cache stampede'e karşı korumayı ve dahasını destekler. - - -Kurulum -======= - -Paketi [Composer|best-practices:composer] kullanarak indirin ve kurun: - -```shell -composer require nette/caching -``` - - -Temel Kullanım -============== - -Önbellekle çalışmanın çekirdek unsuru [api:Nette\Caching\Cache] nesnesidir. Onun bir örneğini oluşturur ve yapıcıya bir depo arka ucu nesnesi aktarırız. Bu depo nesnesi, verinin saklanacağı fiziksel konumu temsil eder (veritabanı, Memcached, diskteki dosyalar vb.). Depo nesnesini genellikle `Nette\Caching\Storage` tipini isteyerek [bağımlılık enjeksiyonuyla |dependency-injection:passing-dependencies] elde edersiniz. Temelleri [Depolar bölümünde |#Depolar] öğreneceksiniz. - -.[warning] -3.0 sürümünde arayüzün hâlâ `I` öneki vardı, dolayısıyla adı `Nette\Caching\IStorage` idi. Ayrıca `Cache` sınıfının sabitleri büyük harfle yazılıyordu, örneğin `Cache::Expire` yerine `Cache::EXPIRE`. - -Aşağıdaki örnekler için `Cache` adında bir alias'ımız ve `$storage` değişkeninde bir depo örneğimiz olduğunu varsayın. - -```php -use Nette\Caching\Cache; - -$storage = /* ... */; // Nette\Caching\Storage örneği -``` - -Önbellek özünde bir *anahtar-değer deposudur*; yani veriyi, ilişkisel dizilere benzer biçimde anahtarlarla okur ve yazarız. Uygulamalar birden çok bağımsız parçadan oluşur. Tüm parçalar tek bir depoyu kullansaydı (diskte tek bir dizin düşünün), er ya da geç anahtar çakışmaları olurdu. Nette Framework bunu, depo alanını ad alanlarına (kavramsal olarak alt dizinler gibi) bölerek çözer. Uygulamanın her parçası böylece benzersiz bir adla kendi ad alanında çalışır ve hiçbir çakışma olmaz. - -Ad alanı adını `Cache` sınıfı yapıcısının ikinci argümanı olarak belirtin: - -```php -$cache = new Cache($storage, 'Full Html Pages'); -``` - -Gerekirse, var olan bir örnekten `derive()` metodunu kullanarak bir alt ad alanına kapsamlanmış yeni bir önbellek türetebilirsiniz: - -```php -$subCache = $cache->derive('Images'); -``` - -Artık `$cache` nesnesini önbellekten okumak ve ona yazmak için kullanabiliriz. `load()` metodu her iki amaca da hizmet eder. İlk argüman anahtar, ikincisi ise anahtar önbellekte bulunamazsa çağrılan bir PHP callback'idir. Callback değeri üretir, onu döndürür ve `load()` metodu onu önbelleğe alır: - -```php -$value = $cache->load($key, function () use ($key) { - $computedValue = /* ... */; // pahalı hesaplama - return $computedValue; -}); -``` - -İkinci parametre atlanırsa (`$value = $cache->load($key)`), öğe önbellekte bulunamadığında `load()` metodu `null` döndürür. - -.[tip] -Yalnızca dizelerin değil, serileştirilebilir her yapının önbelleğe alınabilmesi harikadır. Aynısı anahtarlar için de geçerlidir. - -Bir öğeyi önbellekten silmek için `remove()` metodunu kullanın: - -```php -$cache->remove($key); -``` - -Bir öğeyi önbelleğe `$cache->save($key, $data, ?array $dependencies = null)` metoduyla da kaydedebilirsiniz. Ancak yukarıda gösterilen `load()` yaklaşımı genellikle yeğlenir. - - -Memoization -=========== - -Memoization, bir fonksiyon ya da metot çağrısının sonucunu önbelleğe almaktır; böylece bir dahaki sefere aynı argümanlarla çağrıldığında yeniden hesaplanmak yerine önbellekteki sonuç döndürülür. - -Metotlar ve fonksiyonlar, `call(callable $callback, ...$args)` kullanılarak memoize edilmiş biçimde çağrılabilir: - -```php -$result = $cache->call('gethostbyaddr', $ip); -``` - -`gethostbyaddr()` fonksiyonu böylece her benzersiz `$ip` argümanı için yalnızca bir kez çağrılır. Aynı `$ip` ile sonraki çağrılar önbellekteki değeri döndürür. - -Bir metodun ya da fonksiyonun etrafında, sonradan çağrılabilecek memoize edilmiş bir sarmalayıcı oluşturmak da mümkündür: - -```php -function factorial($num) -{ - return /* ... */; -} - -$memoizedFactorial = $cache->wrap('factorial'); - -$result = $memoizedFactorial(5); // ilk seferde hesaplar -$result = $memoizedFactorial(5); // ikinci seferde önbellekten döndürür -``` - - -Süre Dolması ve Geçersiz Kılma -============================== - -Önbellekleme kullanırken, daha önce saklanmış verinin ne zaman geçersiz olacağı sorusunu ele almak gerekir. Nette Framework, verinin geçerliliğini sınırlamak ya da onu açıkça silmek için mekanizmalar sunar (framework'ün terminolojisinde buna "geçersiz kılma" denir). - -Verinin geçerliliği kaydetme sırasında, genellikle `save()` metodunun üçüncü parametresiyle ayarlanır; örneğin: - -```php -$cache->save($key, $value, [ - $cache::Expire => '20 minutes', -]); -``` - -Alternatif olarak, `load()` metodundaki callback'e başvuruyla aktarılan `$dependencies` parametresiyle ayarlanabilir; örneğin: - -```php -$value = $cache->load($key, function (&$dependencies) { - $dependencies[Cache::Expire] = '20 minutes'; - return /* ... */; -}); -``` - -Ya da `load()` metodunun kendi 3. parametresi kullanılarak; örneğin: - -```php -$value = $cache->load($key, function () { - return /* ... */; -}, [Cache::Expire => '20 minutes']); -``` - -Aşağıdaki örneklerde, callback içinde `$dependencies` değişkenini kullanan ikinci çeşidi varsayacağız. - - -Süre Dolması ------------- - -Süre dolmasının en basit biçimi bir zaman sınırıdır. Bu, veriyi 20 dakikalık geçerlilikle önbelleğe alır: - -```php -// saniye sayısını ya da bir UNIX zaman damgasını da kabul eder -$dependencies[Cache::Expire] = '20 minutes'; -``` - -Geçerlilik süresinin her okumada uzamasını istiyorsanız (kayan süre dolması), bunu şöyle sağlayabilirsiniz, ama bunun önbellek ek yükünü artırdığını bilin: - -```php -$dependencies[Cache::Sliding] = true; -``` - -Yararlı bir seçenek, belirli bir dosya ya da birkaç dosyadan biri değiştirildiğinde verinin süresini doldurmaktır. Bu, örneğin bu dosyaların işlenmesinden türeyen veriyi önbelleğe alırken yararlıdır. Mutlak yollar kullanın. - -```php -$dependencies[Cache::Files] = '/path/to/data.yaml'; -// ya da -$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; -``` - -Bir önbellek öğesinin, başka belirli bir öğenin (ya da birkaçından birinin) süresi dolduğunda süresini doldurmasını sağlayabiliriz. Bu, örneğin bir HTML sayfasının tamamını ve parçalarını farklı anahtarlarla önbelleğe alırken yararlıdır. Bir parça değiştiğinde sayfanın tamamı geçersiz kılınmalıdır. Parçalar `frag1` ve `frag2` gibi anahtarlarla saklanıyorsa şunu kullanın: - -```php -$dependencies[Cache::Items] = ['frag1', 'frag2']; -``` - -Süre dolması, özel fonksiyonlar ya da statik metotlar kullanılarak da denetlenebilir. Bunlar, öğenin hâlâ geçerli olup olmadığını saptamak için her okumada çağrılır. Örneğin, PHP sürümü her değiştiğinde bir öğenin süresini doldurabiliriz. Geçerli sürümü bir parametreyle karşılaştıran bir fonksiyon oluşturun ve kaydederken bağımlılıklara `[fonksiyon adı, ...argümanlar]` biçiminde bir dizi ekleyin: - -```php -function checkPhpVersion($ver): bool -{ - return $ver === PHP_VERSION_ID; -} - -$dependencies[Cache::Callbacks] = [ - ['checkPhpVersion', PHP_VERSION_ID] // checkPhpVersion(...) === false olduğunda süresi dolar -]; -``` - -Doğal olarak tüm bu ölçütler birleştirilebilir. Ölçütlerden en az biri artık karşılanmıyorsa önbellek öğesinin süresi dolar. - -```php -$dependencies[Cache::Expire] = '20 minutes'; -$dependencies[Cache::Files] = '/path/to/data.yaml'; -``` - - -Etiketlerle Geçersiz Kılma --------------------------- - -Etiketler çok yararlı bir geçersiz kılma mekanizması sunar. Önbellekte saklanan her öğeye bir etiket listesi (rastgele dizeler) atayabiliriz. Örneğin, önbelleğe almak istediğimiz, bir makaleyi ve yorumlarını gösteren bir HTML sayfamız olduğunu varsayalım. Kaydederken ilgili etiketleri belirtiriz: - -```php -$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"]; -``` - -Şimdi yönetim bölümüne geçelim. Burada makaleleri düzenlemek için bir formumuz var. Makaleyi veritabanına kaydetmenin yanı sıra, önbellekteki öğeleri etiketlerine göre silmek için `clean()` metodunu çağırırız: - -```php -$cache->clean([ - $cache::Tags => ["article/$articleId"], -]); -``` - -Benzer biçimde, yeni bir yorum eklerken (ya da bir yorumu düzenlerken) ilgili etiketi geçersiz kılmayı unutmamalıyız: - -```php -$cache->clean([ - $cache::Tags => ["comments/$articleId"], -]); -``` - -Ne başardık? HTML önbelleğimiz artık ilişkili makale ya da yorumları her değiştiğinde geçersiz kılınacak (silinecek). ID = 10 olan makale düzenlendiğinde `article/10` etiketi geçersiz kılınır ve bu etiketi taşıyan önbellekteki HTML sayfası silinir. Aynısı, ilgili makalenin altına yeni bir yorum eklendiğinde de olur. - -.[note] -Etiketler bir [#Journal] gerektirir. - - -Önceliğe Göre Geçersiz Kılma ----------------------------- - -Tek tek önbellek öğelerine öncelik atayabiliriz. Bu, örneğin önbellek belirli bir boyut sınırını aştığında denetimli silme olanağı sağlar: - -```php -$dependencies[Cache::Priority] = 50; -``` - -Önceliği 100'e eşit ya da ondan küçük olan tüm öğeleri silmek için: - -```php -$cache->clean([ - $cache::Priority => 100, -]); -``` - -.[note] -Öncelikler [#Journal] denen şeyi gerektirir. - - -Önbelleği Temizleme -------------------- - -`Cache::All` parametresi her şeyi temizler: - -```php -$cache->clean([ - $cache::All => true, -]); -``` - - -Toplu Okuma -=========== - -Önbellekten toplu okuma ve ona toplu yazma için `bulkLoad()` metodunu kullanın. Ona bir anahtar dizisi aktarın; o da karşılık gelen değerlerden oluşan bir dizi döndürür: - -```php -$values = $cache->bulkLoad($keys); -``` - -`bulkLoad()` metodu `load()` metoduna benzer çalışır ve ikinci bir callback parametresi de kabul eder. Bu callback, üretilmekte olan öğenin anahtarını alır: - -```php -$values = $cache->bulkLoad($keys, function ($key, &$dependencies) { - $computedValue = /* ... */; // pahalı hesaplama - return $computedValue; -}); -``` - -Tersine, birden çok öğeyi tek seferde yazmak için, `key => value` çiftlerinden oluşan bir dizi ve isteğe bağlı bağımlılıklar alan `bulkSave()` metodunu kullanın: - -```php -$cache->bulkSave([ - $key1 => $value1, - $key2 => $value2, -], [Cache::Expire => '20 minutes']); -``` - - -PSR-16 ile Kullanım .{data-version:3.3.1} -========================================= - -Nette Cache'i bir PSR-16 arayüzüyle kullanmak için `PsrCacheAdapter` sınıfından yararlanabilirsiniz. Bu, Nette Cache ile PSR-16 uyumlu bir önbellek gerçekleştirimi bekleyen her kod ya da kütüphane arasında kusursuz entegrasyon sağlar. - -```php -$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage); -``` - -Artık `$psrCache` nesnesini standart bir PSR-16 önbelleği olarak kullanabilirsiniz: - -```php -$psrCache->set('key', 'value', 3600); // değeri 1 saat saklar -$value = $psrCache->get('key', 'default'); -``` - -Adaptör, `getMultiple()`, `setMultiple()` ve `deleteMultiple()` dahil PSR-16'da tanımlı tüm metotları destekler. - - -Çıktı Önbellekleme -================== - -Çıktı çok zarif biçimde yakalanıp önbelleğe alınabilir: - -```php -if ($capture = $cache->capture($key)) { - - // echo ... bir miktar veri yazdırılıyor - - $capture->end(); // çıktıyı önbelleğe kaydet -} -``` - -Çıktı önbellekte zaten varsa, `capture()` metodu onu yazdırır ve `null` döndürür, dolayısıyla `if` koşulunun bloğu atlanır. Aksi hâlde çıktıyı arabelleğe almaya başlar ve bir `$capture` nesnesi döndürür; yakalanan veriyi sonunda `end()` metoduyla önbelleğe kaydetmek için onu kullanırsınız. - -.[note] -3.0 sürümünde bu metodun adı `$cache->start()` idi. - - -Latte'de Önbellekleme -===================== - -[Latte|latte:] şablonlarında önbellekleme çok basittir. Şablonun önbelleğe almak istediğiniz bölümünü `{cache}...{/cache}` etiketleriyle sarmanız yeter. Kaynak şablon dosyası (önbelleğe alınan bloğun içine eklenen şablonlar dahil) her değiştiğinde önbellek otomatik geçersiz kılınır. `{cache}` etiketleri iç içe olabilir. İç içe bir blok geçersiz kılındığında (örneğin bir etiket aracılığıyla), onun ana bloğu da geçersiz kılınır. - -Etiketin içinde, önbellek girdisinin bağlanacağı anahtarları (burada `$id` değişkeni) belirtebilir, bir süre dolma zamanı ayarlayabilir ve [geçersiz kılma etiketleri |#Etiketlerle Geçersiz Kılma] tanımlayabilirsiniz. - -```latte -{cache $id, expire: '20 minutes', tags: [tag1, tag2]} - ... -{/cache} -``` - -Tüm bu parametreler isteğe bağlıdır, dolayısıyla süre dolmasını, etiketleri, hatta anahtarları belirtmeniz gerekmez. - -Önbelleklemenin kullanımı `if` ile koşullu da kılınabilir; içerik yalnızca koşul karşılanırsa önbelleğe alınır: - -```latte -{cache $id, if: !$form->isSubmitted()} - {$form} -{/cache} -``` - - -Depolar -======= - -Depo, verinin saklandığı fiziksel konumu temsil eden bir nesnedir. Bir veritabanı, bir Memcached sunucusu ya da en kolay erişilebilen depoyu, yani diskteki dosyaları kullanabiliriz. - -|---------------------- -| Depo | Açıklama -|---------------------- -| [#FileStorage] | Varsayılan depo, önbelleği diskteki dosyalara kaydeder. -| [#MemcachedStorage] | Saklama için bir `Memcached` sunucusu kullanır. -| [#MemoryStorage] | Veri bellekte geçici olarak saklanır (istek bitince yiter). -| [#SQLiteStorage] | Veri bir SQLite veritabanı dosyasında saklanır. -| [#DevNullStorage] | Veri aslında saklanmaz; sınama için yararlıdır. - -Depo nesnesini, `Nette\Caching\Storage` tipini isteyerek [bağımlılık enjeksiyonuyla |dependency-injection:passing-dependencies] elde edersiniz. Nette varsayılan olarak, veriyi [geçici dosyalar |application:bootstrapping#Geçici dosyalar] dizinindeki `cache` alt dizininde saklayan bir `FileStorage` nesnesi sağlar. - -Varsayılan depoyu yapılandırmada değiştirebilirsiniz: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - - -FileStorage ------------ - -Önbellek girdilerini diskteki dosyalara yazar. `Nette\Caching\Storages\FileStorage` deposu başarım açısından epey iyileştirilmiştir ve en önemlisi, işlemlerin tam atomikliğini güvence altına alır. Bu ne demek? Önbelleği kullanırken, başka bir iş parçacığının henüz tümüyle yazmadığı bir dosyayı okumanız ya da siz okurken birinin onu silmesi olanaksızdır. Bu yüzden bu önbellek deposunu kullanmak tümüyle güvenlidir. - -Bu depo ayrıca, önbellek temizlendiğinde ya da hâlâ "soğuk" olduğunda (yani henüz oluşturulmadığında) CPU kullanımında aşırı bir sıçramayı önleyen önemli, yerleşik bir özellik içerir. Buna "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede önleme denir. Bu durum, birden çok eşzamanlı istek aynı önbellek öğesini (örneğin pahalı bir SQL sorgusunun sonucunu) aynı anda istediğinde ortaya çıkar. Öğe o an önbellekte değilse, tüm bu süreçler aynı pahalı işlemi (SQL sorgusu gibi) yürütmeye başlayabilir. Bu, sunucu yükünü katlar ve hiçbir iş parçacığının zaman sınırı içinde yanıt verememesi, önbelleğin oluşmaması ve uygulamanın çökmesi bile olabilir. Neyse ki Nette'in önbelleği bunu ele alır: aynı öğe için birden çok eşzamanlı istek yapıldığında yalnızca ilk iş parçacığı onu üretir. Diğer iş parçacıkları bekler ve sonra ilkinin ürettiği sonucu kullanır. - -`FileStorage` oluşturma örneği: - -```php -// depo, diskteki '/path/to/temp' dizini olacak -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); -``` - - -MemcachedStorage ----------------- - -[Memcached |https://memcached.org] sunucusu, yüksek başarımlı, dağıtık bir bellek nesnesi önbellekleme sistemidir. Nette'deki adaptörü `Nette\Caching\Storages\MemcachedStorage` sınıfıdır. Yapılandırmada sunucunun IP adresini ve standart 11211'den farklıysa portunu belirtin. - -.[caution] -`memcached` PHP uzantısını gerektirir. - -```neon -services: - cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5') -``` - - -MemoryStorage -------------- - -`Nette\Caching\Storages\MemoryStorage`, veriyi bir PHP dizisinin içinde tutan bir depodur. Dolayısıyla istek bittiğinde veri yiter. - - -SQLiteStorage -------------- - -SQLite veritabanı, `Nette\Caching\Storages\SQLiteStorage` adaptörüyle birlikte, veriyi diskteki tek bir dosyada önbelleğe almanın bir yolunu sunar. Yapılandırma, bu veritabanı dosyasının yolunu belirtir. - -.[caution] -`pdo` ve `pdo_sqlite` PHP uzantılarını gerektirir. - -```neon -services: - cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db') -``` - - -DevNullStorage --------------- - -Özel bir depo gerçekleştirimi, aslında hiçbir veri saklamayan `Nette\Caching\Storages\DevNullStorage` sınıfıdır. Bu yüzden önbelleklemenin etkilerini ortadan kaldırmak istediğinizde sınama amaçları için uygundur. - - -Kodda Önbelleği Kullanma -======================== - -Kodunuzda önbellekleme kullanırken iki ana yaklaşım vardır. İlki, depo nesnesini [bağımlılık enjeksiyonuyla |dependency-injection:passing-dependencies] elde etmek ve sonra `Cache` nesnesini kendiniz oluşturmaktır: - -```php -use Nette; - -class ClassOne -{ - private Nette\Caching\Cache $cache; - - public function __construct(Nette\Caching\Storage $storage) - { - $this->cache = new Nette\Caching\Cache($storage, 'my-namespace'); - } -} -``` - -İkinci seçenek, doğrudan `Cache` nesnesini istemektir: - -```php -class ClassTwo -{ - public function __construct( - private Nette\Caching\Cache $cache, - ) { - } -} -``` - -`Cache` nesnesi o zaman yapılandırmada, örneğin şöyle tanımlanmalıdır: - -```neon -services: - - ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') ) -``` - - -Journal -======= - -Nette, etiket ve öncelik bilgisini journal denen bir yerde saklar. Bunun için varsayılan olarak `journal.s3db` dosyası aracılığıyla SQLite kullanılır ve **`pdo` ile `pdo_sqlite` PHP uzantıları gereklidir.** - -Journal gerçekleştirimini yapılandırmada değiştirebilirsiniz: - -```neon -services: - cache.journal: MyJournal -``` - - -DI Servisleri -============= - -DI container'a şu servisler eklenir: - -| Ad | Tip | Açıklama -|---------------------------------------------------------- -| `cache.journal` | [api:Nette\Caching\Storages\Journal] | Önbellek journal deposu -| `cache.storage` | [api:Nette\Caching\Storage] | Birincil önbellek deposu - - -Önbelleği Kapatma -================= - -Uygulamanızda önbelleklemeyi kapatmanın bir yolu, depo arka ucunu [#DevNullStorage] olarak ayarlamaktır: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - -Bu ayar, Latte şablonlarının ya da DI container'ın önbelleklenmesini etkilemez; çünkü bu kütüphaneler `nette/caching` servislerini kullanmaz ve önbelleklerini bağımsız yönetir. Üstelik onların önbelleklerinin geliştirme kipinde [genellikle kapatılması gerekmez |nette:troubleshooting#Geliştirme Sırasında Önbellek Nasıl Kapatılır?]. - - -Daha yeni bir sürüme yükseltiyorsanız [yükseltme |upgrading] sayfasına bakın. diff --git a/caching/tr/@left-menu.texy b/caching/tr/@left-menu.texy deleted file mode 100644 index c1ed2c3be6..0000000000 --- a/caching/tr/@left-menu.texy +++ /dev/null @@ -1,13 +0,0 @@ -Nette Caching -************* -- [Genel bakış |@home] -- [Yükseltme|upgrading] - - -Devamını okuyun -*************** -- [Nette dokümantasyonu |nette:] -- [Nette Application |application:how-it-works] -- [Yardımcı araçlar |utils:] -- [En iyi uygulamalar |best-practices:] -- [Sorun giderme |nette:troubleshooting] diff --git a/caching/tr/@meta.texy b/caching/tr/@meta.texy deleted file mode 100644 index 8dfe82f311..0000000000 --- a/caching/tr/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokümantasyonu}} diff --git a/caching/tr/upgrading.texy b/caching/tr/upgrading.texy deleted file mode 100644 index b6ae5cb6db..0000000000 --- a/caching/tr/upgrading.texy +++ /dev/null @@ -1,20 +0,0 @@ -Yükseltme -********* - - -Sürüm 3.1'e Yükseltme -===================== - -- `Nette\Caching\Cache::start()` metodu `capture()` olarak yeniden adlandırıldı - - -Sürüm 2.4'e Yükseltme -===================== - -- `Nette\Caching\Storages\FileJournal` sınıfı artık kullanılabilir değil - - -Sürüm 2.3'e Yükseltme -===================== - -- eski ve deprecated ArrayAccess sözdizimi `$val = $cache[$key]` ya da `$cache[$key] = $val`, `E_USER_DEPRECATED` tetikler; onun yerine `$cache->load($key)` ve `$cache->save($key, $val)` kullanın diff --git a/command-line/cs/@home.texy b/command-line/cs/@home.texy deleted file mode 100644 index a9cec8256c..0000000000 --- a/command-line/cs/@home.texy +++ /dev/null @@ -1,249 +0,0 @@ -Nette Command-Line -****************** - -<div class=perex> - -Píšete v PHP skript, který se spouští z příkazové řádky? Ať jde o malého pomocníka na jedno odpoledne, nebo o nástroj s celou řadou příkazů, tahle knihovna za vás udělá všechnu tu otravnou práci okolo: - -- Přečte volby a argumenty a zkontroluje jejich hodnoty -- Sestaví nápovědu a srozumitelné chybové hlášky -- Zvládne i nástroje s více příkazy -- Vypisuje barevně, ale v rouře a v logu barvy sama vypne -- Nemá žádné závislosti - -</div> - - -Instalace -========= - -```shell -composer require nette/command-line -``` - -Vyžaduje PHP verze 8.2 a podporuje PHP až do verze 8.6. - - -První skript -============ - -Nejlíp se to ukáže na příkladu. Dejme tomu, že máte hromadu obrázků a chcete je převést do jiného formátu. Napíšeme si na to skript `convert.php`, který se bude volat třeba takhle: - -```shell -php convert.php photo.jpg logo.png --format png -php convert.php photo.jpg --dry-run -``` - -Příkazová řádka se skládá ze tří druhů položek. **Argumenty** jsou prosté hodnoty bez pomlčky, tady jména souborů. **Volba** má jméno a hodnotu: `--format png` říká, do jakého formátu převádět, a dá se zkrátit na `-f png`. **Příznak** je jen vypínač bez hodnoty: `--dry-run` (zkráceně `-n`) způsobí, že skript jen vypíše, co by udělal, ale na žádný soubor nesáhne. - -Kolik kódu je potřeba, aby tohle všechno fungovalo? Překvapivě málo. Samotný převod napíšete jako obyčejnou metodu a atributy u jejích parametrů prozradíte, jak se jednotlivé hodnoty zadávají na příkazové řádce: - -```php -use Nette\CommandLine\Attributes as Cli; -use Nette\CommandLine\Command; -use Nette\CommandLine\Parser; - -final class ImageConverter -{ - #[Cli\Command('Converts images to another format')] - public function convert( - #[Cli\Argument('Images to convert', repeatable: true)] - array $files, - #[Cli\Option('Target format', alias: '-f', enum: ['jpg', 'png', 'webp'])] - string $format = 'webp', - #[Cli\Flag('Only show what would be done', alias: '-n')] - bool $dryRun = false, - ): int - { - // převede každý soubor do $format, při $dryRun jen vypíše, co by udělal - return 0; - } -} -``` - -Všimněte si, že nikde nesaháte na `$argv` a nic neparsujete. Z parametru `$format` se stane volba `--format`, z `$dryRun` příznak `--dry-run` a výchozí hodnota parametru platí, když uživatel volbu vynechá. Texty v atributech popisují jednotlivé položky a objeví se v nápovědě. `repeatable: true` říká, že souborů může být víc, a metoda je proto dostane jako pole. `enum` vyjmenovává povolené hodnoty, takže `--format gif` skript odmítne dřív, než se metoda vůbec zavolá. - -Zbývá metodu spustit. Pod třídu do stejného souboru `convert.php` připíšete: - -```php -require __DIR__ . '/vendor/autoload.php'; - -$converter = new ImageConverter; -$program = Command::from($converter->convert(...)); - -$parser = new Parser; -$args = $parser->parse($program); -exit($args->run()); -``` - -Zápis `$converter->convert(...)` metodu nevolá, ale předává ji dál. `Command::from()` z ní vyčte, co skript přijímá. `Parser` pak přečte příkazovou řádku a zkontroluje hodnoty. Nakonec `run()` zavolá vaši metodu se správnými hodnotami. Co metoda vrátí, to se stane návratovým kódem skriptu. Podle něj shell, CI nebo jiný skript poznají, jak to dopadlo: 0 znamená úspěch, cokoli jiného problém. - -A protože lidé u příkazové řádky mají své zvyky, poradí si skript se všemi obvyklými zápisy: `--format png`, `--format=png` i `-f png` znamenají totéž, jednopísmenné volby jde slepit dohromady (`-nf png` je `-n -f png`) a volby smějí stát kdekoli mezi soubory. - - -Nápověda a chyby -================ - -Každý slušný skript má mít nápovědu a srozumitelně hlásit chyby. Obojí přidáte pár řádky, spouštěcí část `convert.php` stačí rozšířit takhle (dva nové řádky `use` patří nahoru k ostatním): - -```php -use Nette\CommandLine\HelpRenderer; -use Nette\CommandLine\UsageException; - -$converter = new ImageConverter; -$program = Command::from($converter->convert(...)); -$program->addFlag('--help', 'Print this help', alias: '-h', standalone: true); - -$parser = new Parser; -$renderer = new HelpRenderer; - -try { - $args = $parser->parse($program); - if ($args->get('--help')) { - $renderer->render($args->command); - exit(0); - } - exit($args->run()); - -} catch (UsageException $e) { - $renderer->renderError($e); - exit(2); -} -``` - -Příznak `--help` je samostatný (`standalone`), takže zafunguje, i když uživatel nezadal povinné soubory. Parser v takovém případě nic dalšího nekontroluje. `HelpRenderer` sestaví nápovědu z popisů v atributech a vypíše ji pro `$args->command`, tedy pro příkaz, který uživatel zvolil; u skriptu bez příkazů je to celý program. Zkuste teď spustit `php convert.php --help`: - -[* help.webp .(Nápověda skriptu v terminálu) *] - -Nemuseli jste ji psát, a přesto je úplná, zarovnaná a barevná. A protože vzniká ze stejné metody, která dělá práci, nikdy nebude tvrdit něco jiného, než co skript opravdu umí. - -O chyby se stará blok `catch`. Když se uživatel přepíše nebo zadá nesmysl, parser vyhodí `UsageException` a `renderError()` z ní místo stack trace udělá srozumitelnou odpověď: - -``` -$ php convert.php --fromat png photo.jpg -Error: Unknown option `--fromat`. Did you mean `--format`? - -$ php convert.php --format gif photo.jpg -Error: Option `--format`: expects `jpg`, `png` or `webp`, `gif` given. -``` - -Pod hláškou se rovnou vypíše nápověda, takže uživatel nemusí nic dohledávat, a skript skončí návratovým kódem 2, kterým se špatně zadaná příkazová řádka obvykle hlásí. Vaše metoda se v takovém případě vůbec nezavolá, takže nikdy nepoběží s nesmyslnými hodnotami. - -Tenhle spouštěcí blok je v každém skriptu stejný, takže si ho jednou napíšete a dál už jen měníte, co se do `$program` přidá. - - -Bez atributů -============ - -Atributy jsou nejpohodlnější cesta: každá volba je popsaná přímo u parametru, který ji přijímá, hodnoty dostane metoda rovnou do svých parametrů a nemáte žádnou zvláštní definici, kterou by bylo potřeba udržovat v souladu s kódem. Pod nimi ale leží obyčejné API, které můžete použít i přímo. Hodí se pro rychlý skript bez třídy nebo tehdy, když volby skládáte až za běhu: - -```php -use Nette\CommandLine\Command; -use Nette\CommandLine\Parser; - -$command = new Command('convert'); -$command->addOption('--format', 'Target format', alias: '-f', enum: ['jpg', 'png', 'webp']); -$command->addArgument('files', 'Images to convert', repeatable: true); - -$parser = new Parser; -$args = $parser->parse($command); -$args->get('--format'); // 'png' -$args->get('files'); // ['photo.jpg', 'logo.png'] -``` - -Nápovědu a chyby obsloužíte úplně stejně jako výše. Všechno, co volby a argumenty umějí, najdete na stránce [Argumenty, volby a příkazy |arguments]. - - -Více příkazů -============ - -Skripty mají ve zvyku růst. Dnes obrázky převádí, zítra by je měl umět i zmenšovat. Takové nástroje se obvykle ovládají příkazy, podobně jako `composer install` a `composer update`: - -```shell -php images.php convert photo.jpg --format png -php images.php resize photo.jpg --width 800 -php images.php --quiet convert photo.jpg -``` - -Nic nemusíte přepisovat. Každý příkaz je jedna metoda s atributem `#[Cli\Command]`, stejná jako `convert()` výše, a celou třídu takových metod přidáte jedním voláním. Volby, které platí pro všechny příkazy, jako `--quiet` v posledním řádku, přidáte k celému programu: - -```php -$tools = new ImageTools; // obsahuje metody convert() a resize() -$program = new Command('images'); -$program->addFlag('--quiet', 'Print nothing but errors', alias: '-q'); -$program->addFlag('--help', 'Print this help', alias: '-h', standalone: true); -$program->addCommandsFrom($tools); -``` - -Zbytek skriptu, tedy parser, nápověda a chyby, je stejný jako v `convert.php`. Protože `render($args->command)` vypíše nápovědu k vybranému příkazu, dostane každý příkaz vlastní nápovědu (`php images.php convert --help`). Příkazy jde i vnořovat, takže třeba `php images.php cache clear` je příkaz `clear` uvnitř příkazu `cache`. Víc v sekci [Příkazy |arguments#Příkazy]. - - -Výstup do terminálu -=================== - -Skript ale jen nečte, také píše. K tomu slouží třída `Console`, která se řídí tím, kam výstup míří. V terminálu píše barevně, a když výstup přesměrujete do souboru nebo skript běží v CI, barvy sama vynechá, takže log zůstane čistý. Podobně s průběhem práce: v terminálu se ukazuje na řádku, který se průběžně přepisuje, a do logu se vůbec nedostane. - -Konzoli vytvoříte jednou a předáte ji třídě, která ji potřebuje, třeba v konstruktoru: - -```php -final class ImageConverter -{ - public function __construct( - private Console $console, - ) { - } - - #[Cli\Command('Converts images to another format')] - public function convert( - #[Cli\Argument('Images to convert', repeatable: true)] - array $files, - ): int - { - foreach ($files as $file) { - $this->console->setStatus("Converting $file"); // řádek s průběhem - // ... - $this->console->writeLine("$file converted", 'green'); - } - return 0; - } -} - -$console = new Console; -$converter = new ImageConverter($console); -``` - -Text se navíc vypíše přesně tak, jak je. Některé knihovny v textu hledají značky pro barvy, takže XML nebo vstup od uživatele musíte nejdřív escapovat, jinak se rozbije. Tady nic takového nehrozí. Víc na stránce [Výstup do terminálu |console]. - - -Testování -========= - -Na závěr dobrá zpráva pro všechny, kdo píšou testy: skript otestujete, aniž byste ho spouštěli z příkazové řádky. Parseru předáte jednotlivá slova příkazové řádky, jako by je napsal uživatel, a ověříte, co z nich vyčetl, nebo jakou chybu nahlásil. Špatně zadanou řádku parser hlásí výjimkou `ParseException`, což je druh `UsageException`, kterou jsme zachytávali výše. Příklad používá [Nette Tester |tester:]: - -```php -$parser = new Parser; - -Assert::exception( - fn() => $parser->parse($program, ['--format', 'gif', 'photo.jpg']), - ParseException::class, - 'Option `--format`: expects `jpg`, `png` or `webp`, `gif` given.', -); - -$args = $parser->parse($program, ['photo.jpg', '--dry-run']); -Assert::same(0, $args->run()); -``` - -Víc v sekci [Testování |arguments#Testování]. - - -Přechod z verze 1.x -=================== - -Verze 2.0 je nové API bez vrstvy zpětné kompatibility: v 1.x držel jediný `Parser` definici, zpracovával řádku i vypisoval text nápovědy, který dostal, zatímco ve 2.0 je definicí `Command`, `Parser` proti ní čte řádku a `HelpRenderer` z ní vykresluje nápovědu. - -[Plugin Nette pro Claude Code a Codex |ai:agent-plugins] obsahuje návod, podle kterého váš kódovací agent kód z 1.x věrně přepíše na 2.0 a nahlásí každé místo, kde se 2.0 chová jinak. - - -{{sitename: Nette Dokumentace}} -{{maintitle: Nette Command-Line: konzolové skripty a argumenty v PHP}} -{{description: Konzolové skripty v PHP: volby, argumenty a příkazy, nápověda a srozumitelné chybové hlášky, barevný výstup a výpis průběhu. Bez závislostí.}} diff --git a/command-line/cs/@left-menu.texy b/command-line/cs/@left-menu.texy deleted file mode 100644 index 1a5b9da58e..0000000000 --- a/command-line/cs/@left-menu.texy +++ /dev/null @@ -1,5 +0,0 @@ -Nette Command-Line -****************** -- [Úvod |@home] -- [Argumenty, volby a příkazy |arguments] -- [Výstup do terminálu |console] diff --git a/command-line/cs/arguments.texy b/command-line/cs/arguments.texy deleted file mode 100644 index 93112216a6..0000000000 --- a/command-line/cs/arguments.texy +++ /dev/null @@ -1,694 +0,0 @@ -Argumenty, volby a příkazy -************************** - -.[perex] -Tahle stránka podrobně popisuje, jak říct, co váš skript na příkazové řádce přijímá, jak se hodnoty kontrolují a převádějí, jak vytvořit nástroj s více příkazy a jak vzniká nápověda a chybové hlášky. Celou dobu nás bude provázet skript na převod obrázků z [úvodní stránky |@home]. - - -Příznaky, volby a argumenty -=========================== - -Připomeňme si, jak se náš skript volá: - -```shell -php convert.php photo.jpg logo.png --format png --dry-run -``` - -Na příkazové řádce jsou tři druhy položek: - -- **argumenty** jsou hodnoty bez pomlčky, tady `photo.jpg` a `logo.png`; poznají se podle pořadí, proto se jim říká poziční, -- **volba** má jméno a hodnotu, tady `--format png`, -- **příznak** má jen jméno a funguje jako vypínač, tady `--dry-run`. - -Na [úvodní stránce |@home] jsme skript popsali atributy. Teď si ukážeme API, které pod nimi leží. Všechno z téhle části platí i pro atributy, které mají stejná nastavení, jen je píšete k parametrům metody. Popis skriptu vytvoříte objektem [api:Nette\CommandLine\Command] a jeho metodami `addArgument()`, `addOption()` a `addFlag()`: - -```php -use Nette\CommandLine\Command; -use Nette\CommandLine\Parser; - -$command = new Command('convert', 'Converts images to another format'); -$command->addArgument('files', 'Images to convert', repeatable: true); -$command->addOption('--format', 'Target format', alias: '-f'); -$command->addFlag('--dry-run', 'Only show what would be done', alias: '-n'); - -$parser = new Parser; -$args = $parser->parse($command); -``` - -Každá metoda `add*()` dostane jméno, popis, který se objeví v nápovědě, a pak pojmenované argumenty s dalším nastavením, jako je tu `alias` pro krátký zápis. [api:Nette\CommandLine\Parser] přečte příkazovou řádku podle tohoto popisu a vrátí objekt [api:Nette\CommandLine\ParseResult]. Jeho metodou `get()` si přečtete hodnotu podle jména, přesně tak, jak jste ho zadali, včetně pomlček: - -```php -$args->get('files'); // ['photo.jpg', 'logo.png'] -$args->get('--format'); // 'png', nebo null, když volba chybí -$args->get('--dry-run'); // true, nebo null, když příznak chybí -``` - -Když se ve jménu překlepnete, třeba `$args->get('--fromat')`, dostanete výjimku. Překlep se tak nemůže tvářit jako volba, kterou uživatel jen nezadal. Všechny hodnoty najednou vrátí `toArray()`. - -`parse()` čte příkazovou řádku, se kterou byl skript spuštěn. Můžete mu ale předat i vlastní pole jednotlivých slov, bez jména skriptu, což se hodí v testech: - -```php -$args = $parser->parse($command, ['photo.jpg', '--format', 'png']); -``` - -Dvě metody se hodí, když potřebujete víc než samotnou hodnotu. `isEmpty()` řekne, že uživatel za jméno skriptu nenapsal vůbec nic. `wasProvided('--format')` řekne, jestli uživatel volbu opravdu zadal, takže poznáte, že napsal `--format webp`, i když je `webp` zároveň výchozí hodnota. To oceníte třeba při slučování příkazové řádky s konfiguračním souborem. - -Pozor na jednu věc: `Parser` jen čte. Když uživatel zadá něco špatně, vyhodí výjimku, a nápovědu taky nevypíše sám. Jak obojí doplnit, ukazuje sekce [Spuštění programu |#Spuštění programu]. - - -Příznaky --------- - -Příznak je vypínač: když na řádce je, má hodnotu `true`, když chybí, `null`: - -```php -$command->addFlag('--dry-run', alias: '-n'); -// --dry-run → true -// -n → true -// (chybí) → null -``` - -Některé věci chcete mít zapnuté, dokud je uživatel nevypne. Třeba zachování metadat EXIF při převodu. S `negatable: true` vznikne k příznaku i opačný tvar `--no-…` a výchozí hodnotou řeknete, co platí, když uživatel nenapíše ani jedno: - -```php -$command->addFlag('--metadata', 'Keep EXIF metadata', negatable: true, default: true); -// --metadata → true -// --no-metadata → false -// (chybí) → true -``` - -Jednopísmenné příznaky lze slepit do jednoho slova: `-nv` znamená totéž co `-n -v`. Poslední písmeno může patřit i volbě s hodnotou, takže `-nf png` je `-n -f png`. - - -Volby ------ - -Volba nese hodnotu. Uživatel ji může oddělit mezerou nebo rovnítkem a místo dlouhého jména použít alias: - -```php -$command->addOption('--format', alias: '-f'); -// --format png → 'png' -// --format=png → 'png' -// -f png → 'png' -// --format → chyba, chybí hodnota -// (chybí) → null -``` - -Samotná volba je tedy nepovinná, ale když ji uživatel použije, musí k ní napsat i hodnotu. Když zadá stejnou volbu víckrát, platí poslední hodnota. Chcete-li všechny, použijte [opakované zadání |#Opakované zadání]. - -Někdy se hodí volba, která hodnotu mít může, ale nemusí. Třeba `--backup` zachová původní soubory, a když uživatel připíše adresář, uloží je tam. To zařídí `valueOptional: true`: - -```php -$command->addOption('--backup', valueOptional: true); -// --backup=old → 'old' -// --backup → true -// (chybí) → null -``` - -Nepovinnou hodnotu je potřeba připojit rovnítkem. Zápis `--backup old` by totiž byl dvojznačný: je `old` hodnota volby, nebo další soubor? Knihovna ho proto bere jako soubor a `--backup` jako volbu bez hodnoty. - -.[note] -Výjimkou je volba, která má zároveň [výčet povolených hodnot |#Povolené hodnoty]. Tam se hodnota vezme i po mezeře, ovšem jen když patří mezi povolené, takže nic jiného se omylem nespolkne. Pro `--strip` s hodnotami `exif` a `all` tedy `--strip all` znamená hodnotu `all`, ale `--strip photo.jpg` volbu bez hodnoty a soubor. Kvůli tomu definice odmítne příkaz pojmenovaný stejně jako některá z hodnot a argument shodný s hodnotou musí uživatel zapsat až za `--`. - - -Argumenty ---------- - -Argumenty jsou hodnoty bez pomlčky a rozlišují se podle pořadí. Jsou povinné, dokud neřeknete jinak: - -```php -$command->addArgument('input'); -// photo.jpg → 'photo.jpg' -// (chybí) → chyba, chybí povinný argument - -$command->addArgument('output', optional: true); -// (chybí) → null -``` - -Argumenty mohou stát kdekoli, před volbami i za nimi. Pro hodnoty, které začínají pomlčkou, platí pár pravidel, jak je znáte z jiných nástrojů: - -```php -// convert.php -- -weird.jpg → soubor '-weird.jpg', vše za -- je argument -// convert.php - → soubor '-', podle zvyklosti standardní vstup -// convert.php --rotate -90 → --rotate má hodnotu '-90' -``` - -Záporné číslo je tedy normální hodnota. Jedinou výjimkou je příkaz, který sám má volbu pojmenovanou jako číslo, třeba `-1` jako v `ls -1`. Tam se každé `-90` bere jako volba a zápornou hodnotu musí uživatel napsat s rovnítkem, `--rotate=-90`, nebo jako argument za `--`. - - -Výchozí hodnoty ---------------- - -`default` určuje hodnotu volby nebo argumentu pro případ, že ji uživatel nezadá: - -```php -$command->addOption('--format', default: 'webp'); -// --format png → 'png' -// (chybí) → 'webp' -``` - -Nezaměňujte to s `valueOptional`. To říká, co platí, když uživatel volbu napíše bez hodnoty, kdežto `default` platí, když ji nenapíše vůbec. Volba může mít obojí: - -```php -$command->addOption('--backup', valueOptional: true, default: 'backup'); -// --backup=old → 'old' -// --backup → true -// (chybí) → 'backup' -``` - -Výchozí hodnota se vrací přesně tak, jak jste ji zadali, takže se nekontroluje ani nepřevádí. Povinný argument s výchozí hodnotou by nedával smysl, proto ho definice odmítne. Takový argument označte jako nepovinný. - - -Povolené hodnoty ----------------- - -`enum` omezí hodnotu na výčet. Cokoli jiného skript odmítne s hláškou, která povolené hodnoty vyjmenuje: - -```php -$command->addOption('--format', enum: ['jpg', 'png', 'webp']); -// --format gif → Option `--format`: expects `jpg`, `png` or `webp`, `gif` given. -``` - -Hodnoty jsou neprázdné řetězce, i čísla tedy pište jako řetězce, třeba `['1', '2']`. Místo seznamu můžete předat backed enum a dostanete rovnou jeho případ: - -```php -enum Format: string -{ - case Jpg = 'jpg'; - case Png = 'png'; - case Webp = 'webp'; -} - -$command->addOption('--format', enum: Format::class, default: Format::Webp); -// --format png → Format::Png -// (chybí) → Format::Webp -``` - - -Opakované zadání ----------------- - -S `repeatable: true` může uživatel volbu nebo argument zadat víckrát a vy dostanete všechny hodnoty v poli: - -```php -$command->addOption('--exclude', 'Skip files matching the mask', repeatable: true); -// --exclude '*.tmp' --exclude 'thumb*' → ['*.tmp', 'thumb*'] -// (chybí) → [] - -$command->addArgument('files', repeatable: true); -// photo.jpg logo.png → ['photo.jpg', 'logo.png'] -``` - -Opakovaný argument sebere všechny zbývající hodnoty, proto musí být poslední. - - -Převod hodnot -------------- - -Z příkazové řádky přicházejí vždycky řetězce. Když potřebujete číslo nebo cestu, předejte volbě či argumentu `normalizer`, tedy funkci, která hodnotu zkontroluje a převede. Hotové najdete ve třídě [api:Nette\CommandLine\Normalizers]: `int()` a `float()` převedou číslo, volitelně v zadaném rozsahu, a `realPath()` převede cestu k existujícímu souboru na absolutní: - -```php -use Nette\CommandLine\Normalizers; - -$command->addOption('--quality', normalizer: fn($v) => Normalizers::int($v, min: 1, max: 100)); -// --quality 80 → 80 -// --quality abc → Option `--quality`: expects an integer, `abc` given. -// --quality 150 → Option `--quality`: expects a number from 1 to 100, `150` given. - -$command->addOption('--watermark', normalizer: Normalizers::realPath(...)); -// --watermark logo.png → '/home/me/images/logo.png' -// --watermark missing.png → Option `--watermark`: file path `missing.png` not found. -``` - -Bez rozsahu stačí předat samotnou metodu, `Normalizers::int(...)`. Vlastní normalizátor je libovolná funkce, která dostane řetězec a vrátí převedenou hodnotu. Když je hodnota špatně, vyhodí výjimku a knihovna z ní udělá srozumitelnou hlášku s názvem volby, jako v ukázce výše. Hotový normalizátor můžete zavolat i ze svého, třeba když chcete cestu nejdřív upravit: - -```php -$command->addOption('--watermark', normalizer: fn($v) => Normalizers::realPath("watermarks/$v")); -``` - -Normalizátor se spouští jen na hodnotách, které skutečně přišly z příkazové řádky. Na výchozí hodnotu ani na `true` u volby bez nepovinné hodnoty se nevolá. - -.[note] -Rozsah, do kterého se žádné číslo nevejde, třeba `min` větší než `max`, je chyba v kódu, ne v zadání uživatele. Proto vyhodí `ValueError`, který se na hlášku pro uživatele nepřevádí. Od PHP 8.6 můžete místo funkce `fn($v) => Normalizers::int($v, min: 1)` napsat kratší `Normalizers::int(?, min: 1)`. - - -Důkladnější validace --------------------- - -Na složitější pravidla můžete výsledek předat knihovně [Nette Schema |schema:]. Command-Line na ní nezávisí, je to prostě druhý krok. Parser vrací řetězce tak, jak je uživatel napsal, a `null` u toho, co nezadal. Hodnoty `null` proto vynechte, ať se uplatní výchozí hodnoty schématu: - -```php -use Nette\Schema\Expect; -use Nette\Schema\Processor; - -$schema = Expect::structure([ - '--quality' => Expect::string()->pattern('[1-9][0-9]?|100')->castTo('int')->default(85), - 'files' => Expect::listOf('string'), -])->otherItems(); - -$values = array_filter($args->toArray(), fn($value) => $value !== null); -$options = (new Processor)->process($schema, $values); -``` - - -Parametry v kódu ----------------- - -Metody `add*()` vracejí objekt parametru, jehož nastavení si přečtete jako vlastnosti, třeba `$option->alias`. Parametr už nejde změnit, proto se kontroluje hned při přidání a chyba v definici, třeba jméno volby bez pomlčky nebo dvakrát použité jméno, vyhodí `InvalidArgumentException` okamžitě. Později se k parametru dostanete přes `getParameter()`. Druhý argument ověří, že jde o volbu, a statická analýza pak ví, jaké vlastnosti má: - -```php -use Nette\CommandLine\Parameters\Option; - -$format = $command->getParameter('--format', Option::class); -``` - -Ne každé nastavení dává smysl u každého druhu. Příznak nemá hodnotu, proto `addFlag()` nemá `enum` ani `normalizer`. Ty mají `addOption()` a `addArgument()`, zatímco `valueOptional` a `valueName` jen `addOption()`. - - -Příkazy -======= - -Když skript umí víc věcí, ovládá se obvykle příkazy. Náš nástroj na obrázky bude umět převádět i zmenšovat a jednu volbu bude mít společnou pro všechno: - -```shell -php images.php convert photo.jpg --format png -php images.php resize photo.jpg --width 800 -php images.php --quiet convert photo.jpg -``` - -Příkaz přidáte metodou `addCommand()`. Vrátí nový `Command`, který nastavíte úplně stejně jako samotný program: - -```php -$program = new Command('images'); -$program->addFlag('--quiet', 'Print nothing but errors', alias: '-q'); - -$convert = $program->addCommand('convert', 'Converts images to another format'); -$convert->addArgument('files', 'Images to convert', repeatable: true); -$convert->addOption('--format', 'Target format', alias: '-f', enum: ['jpg', 'png', 'webp']); - -$resize = $program->addCommand('resize', 'Shrinks images to the given width'); -$resize->addArgument('files', 'Images to resize', repeatable: true); -$resize->addOption('--width', 'Width in pixels', normalizer: Normalizers::int(...)); -``` - -Příkaz se pozná podle prvního slova bez pomlčky a `$args->command` vám po zpracování řekne, který to byl. Když uživatel žádný příkaz nenapíše, je `$args->command` samotný program. Pokud má být příkaz povinný, vytvořte program s `commandRequired: true`. - -Volby programu, jako tady `--quiet`, platí ve všech příkazech a uživatel je může napsat před příkaz i za něj: `images.php -q convert photo.jpg` i `images.php convert photo.jpg -q` znamená totéž. Volbu jiného příkazu knihovna odmítne a poradí, kam patří: - -``` -$ php images.php convert photo.jpg --width 800 -Error: Option `--width` belongs to command `resize`. -``` - -Výsledek obsahuje jen hodnoty programu a vybraného příkazu. Sousední příkazy smějí používat stejná jména voleb, takže `--width` by klidně mohl mít i `convert` s jiným významem. - - -Vnořené příkazy ---------------- - -Příkaz může mít vlastní příkazy, a ty zase své. Třeba správa mezipaměti náhledů: - -```php -$cache = $program->addCommand('cache', 'Manages the thumbnail cache'); -$cache->addCommand('clear', 'Deletes all cached thumbnails'); -$cache->addCommand('stats', 'Shows how much space the cache takes'); -``` - -Uživatel pak píše `php images.php cache clear`. Příkaz, který má vlastní příkazy, nemůže mít argumenty, protože by nešlo poznat, jestli slovo za ním je argument, nebo příkaz. - - -Handlery --------- - -Zbývá říct, co se má při kterém příkazu stát. Nejpohodlnější je dát každému příkazu handler, tedy metodu nebo funkci, která jeho práci vykoná. Předáte ho jako `handler:` už při vytváření příkazu: - -```php -final class ImageTools -{ - public function convert(array $files, ?string $format, ?bool $quiet): int - { - // převede soubory, při $quiet nic nevypisuje - return 0; - } -} - -$tools = new ImageTools; -$convert = $program->addCommand('convert', 'Converts images to another format', handler: $tools->convert(...)); -``` - -Hodnoty handleru nepředáváte sami. Každý jeho parametr dostane hodnotu podle svého jména: `$files` argument `files`, `$format` volbu `--format`, a protože se počítají i volby programu, `$quiet` dostane `--quiet`. Jméno s více slovy se převádí: z `$maxWidth` je `--max-width`. Parametr typu `ParseResult` dostane celý výsledek, třeba když potřebujete `wasProvided()`, a parametr typu `Command` dostane vybraný příkaz. - -Vybraný příkaz pak spustí `run()`. Vrací to, co vrátil handler, a pokud handler nevrátí nic, znamená to 0: - -```php -$args = $parser->parse($program); -exit($args->run()); -``` - -Jak k tomu přidat nápovědu a hlášení chyb, ukazuje sekce [Spuštění programu |#Spuštění programu]. - -Typy parametrů se kontrolují proti tomu, co může volba dát. Volba, kterou uživatel nezadal a která nemá výchozí hodnotu, je `null`, proto patří do `?string`, a ne do `string`. Kdyby v handleru zůstal `string $format`, knihovna to nahlásí výjimkou hned při prvním spuštění skriptu, dřív než se cokoli provede, a to i pro příkazy, které se zrovna nespouštějí: - -``` -Command 'convert': parameter $format of handler ImageTools::convert() does not accept null, which option --format gives when absent. -``` - -.[note] -Stejně se nahlásí i další chyby v signatuře: parametr, ke kterému žádná volba ani argument neexistuje, parametr s výchozí hodnotou, kterou by vstup stejně vždy přepsal, variadický parametr a parametr předávaný referencí. Normalizátor musí deklarovat návratový typ, jinak by nebylo s čím parametr porovnat. Opakovaná volba nebo argument přijde jako pole. - - -Příkazy popsané atributy ------------------------- - -Handler může svůj příkaz popsat i sám, přímo u svého kódu. Tak jsme to dělali na [úvodní stránce |@home]. `#[Command]` dá metodě popis a `#[Flag]`, `#[Option]` a `#[Argument]` udělají z jejích parametrů příznaky, volby a argumenty: - -```php -use Nette\CommandLine\Attributes as Cli; - -final class ImageTools -{ - #[Cli\Command('Converts images to another format')] - public function convert( - ?bool $quiet, - #[Cli\Argument('Images to convert', repeatable: true)] - array $files, - #[Cli\Option('Target format', alias: '-f', enum: ['jpg', 'png', 'webp'])] - string $format = 'webp', - ): int - { - // převede soubory, při $quiet nic nevypisuje - return 0; - } - - #[Cli\Command('Shrinks images to the given width')] - public function resize(/* ... */): int - { - // ... - } -} - -$tools = new ImageTools; -$program->addCommandsFrom($tools); -``` - -`addCommandsFrom()` přidá všechny veřejné metody s atributem `#[Cli\Command]` v pořadí, v jakém jsou ve třídě. Jednotlivou metodu přidáte přes `addCommandFrom($tools->convert(...))`. Výsledkem jsou úplně obyčejné příkazy, jako byste volali `addCommand()` a `addOption()` sami. - -Jména se odvozují: příkaz z názvu metody, takže z `removeDuplicates()` je `remove-duplicates`, a volby z názvů parametrů, takže z `$maxWidth` je `--max-width`. Jiné jméno nastavíte v atributu přes `name:`. Výchozí hodnota parametru se stane výchozí hodnotou volby, argument s výchozí hodnotou je nepovinný a parametr typu backed enum přijímá jako povolené hodnoty jeho případy. - -Parametr bez atributu, jako `$quiet` výše, se napojí na volbu podle jména, tady na `--quiet` programu. Výchozí hodnotu mít nesmí, tu nese volba programu, a proto musí stát před parametry, které ji mají. - -Atributy `#[Flag]`, `#[Option]` a `#[Argument]` mají stejná nastavení jako `addFlag()`, `addOption()` a `addArgument()`, jen jméno a výchozí hodnotu berou z parametru. `#[Command]` přijímá `description`, `usage`, `commandRequired` a `name`. - -Atribut `#[Command]` se jmenuje stejně jako třída `Nette\CommandLine\Command`. Proto atributy importujeme pod aliasem `Cli` a píšeme `#[Cli\Command]`. - -Program bez příkazů popíšete stejně: `Command::from()` vytvoří z handleru celý program, jak jsme to udělali na úvodní stránce. Jeho jméno se neodvozuje z metody, protože ta pojmenovává akci, ne program, takže bez `name:` ho nápověda pojmenuje podle souboru skriptu. Takový program je obyčejný `Command`, takže k němu můžete přidávat i příkazy. - -Do atributu jde zapsat jen konstantní výraz, proto se normalizátor píše jako pole s třídou a metodou. Hotový třeba jako `[Normalizers::class, 'int']`, vlastní jako `[self::class, 'parseWidth']`. Od PHP 8.5 můžete do atributu napsat přímo `static function`. Když normalizátor potřebuje i rozsah, napište si statickou metodu, která ho předá dál: - -```php -final class ImageTools -{ - #[Cli\Command('Shrinks images to the given width')] - public function resize( - #[Cli\Argument('Images to resize', repeatable: true)] - array $files, - #[Cli\Option('Width in pixels', normalizer: [self::class, 'parseWidth'])] - int $width = 800, - ): int - { - // zmenší soubory na šířku $width - return 0; - } - - public static function parseWidth(string $value): int - { - return Normalizers::int($value, min: 1); - } -} -``` - -S atributy se z názvů vašich metod a proměnných stává rozhraní nástroje. Když přejmenujete `$maxWidth` na `$widthLimit`, změní se `--max-width` na `--width-limit` a přestanou fungovat všechny skripty, které váš nástroj volají. Takovou změnu zachytí [test nápovědy |#Testování]. - - -Volby sdílené více příkazy --------------------------- - -Příkazy často potřebují stejné volby. Třeba `convert` i `resize` ukládají výsledky do adresáře a oba umějí zkušební běh. Abyste je nemuseli opakovat, popište je jednou v konstruktoru třídy a každý handler ať ji přijme jako parametr: - -```php -final readonly class Output -{ - public function __construct( - #[Cli\Option('Directory for the results', valueName: 'dir')] - public ?string $outputDir = null, - #[Cli\Flag('Only show what would be done', alias: '-n')] - public bool $dryRun = false, - ) { - } -} - -final class ImageTools -{ - #[Cli\Command('Converts images to another format')] - public function convert( - #[Cli\Argument('Images to convert', repeatable: true)] - array $files, - Output $output, - ): int - { - // uloží výsledky do $output->outputDir - return 0; - } -} -``` - -`convert` teď přijímá `--output-dir` a `--dry-run`, a stejně tak každý další příkaz, jehož handler přijme `Output`. Při spuštění knihovna objekt vytvoří z hodnot těchto voleb a předá ho handleru. V nápovědě se volby objektu ukážou pohromadě, v místě, kde je parametr. - -Konstruktor se řídí stejnými pravidly jako handler. Handler může přijmout i víc takových objektů, třeba jeden s tím, co sdílejí všechny příkazy, a druhý s tím, co jen některé. Funguje to jen u příkazů popsaných atributy. - - -Větvení bez handlerů --------------------- - -Handlery nejsou povinné. V malém skriptu můžete zjistit vybraný příkaz a větvit sami, nejlépe přes `match`: - -```php -$args = $parser->parse($program); - -exit(match ($args->command) { - $convert => convertImages($args->get('files'), $args->get('--format')), - $resize => resizeImages($args->get('files'), $args->get('--width')), - $program => showUsage(), -}); -``` - -Protože `match` nemá větev `default`, vyhodí `UnhandledMatchError`, když na některý příkaz zapomenete. Nápovědu a chyby ale v tomhle případě obsluhujete sami, viz [Nápověda |#Nápověda] a [Chyby |#Chyby]. - - -Spuštění programu -================= - -Zpracovat řádku, odpovědět na `--help` a `--version`, nahlásit chybu a spustit handler se dělá v každém nástroji stejně. Celá spouštěcí část našeho nástroje vypadá takhle: - -```php -use Nette\CommandLine\HelpRenderer; -use Nette\CommandLine\UsageException; - -$program->addFlag('--help', 'Print this help', alias: '-h', standalone: true); -$program->addFlag('--version', 'Print the version', standalone: true); - -$parser = new Parser; -$renderer = new HelpRenderer; - -try { - $args = $parser->parse($program); - if ($args->get('--help')) { - $renderer->render($args->command); - exit(0); - } elseif ($args->get('--version')) { - echo "Images 1.0\n"; - exit(0); - } - exit($args->run()); - -} catch (UsageException $e) { - $renderer->renderError($e); - exit(2); -} -``` - -Projděme si ji. Příznaky `--help` a `--version` jsou samostatné (`standalone`), takže fungují, i když uživatel nezadal povinné argumenty, víc v [sekci o nápovědě a verzi |#help-a-version]. `render($args->command)` vypíše nápovědu k příkazu, který uživatel zvolil, takže `images.php convert --help` ukáže nápovědu k `convert`. Nakonec `run()` spustí handler vybraného příkazu a vrátí jeho návratový kód. - -Když uživatel zadá řádku špatně, parser vyhodí `ParseException`, což je druh `UsageException`. `renderError()` vypíše hlášku a pod ní nápovědu k příkazu, ve kterém se uživatel spletl, a to na chybový výstup. Skript pak skončí kódem 2. To je zvyklost pro špatně zadanou řádku a odlišuje ji od kódu 1, kterým nástroj hlásí, že něco našel nebo se mu něco nepovedlo. Stejně se ošetří `UsageException`, kterou vyhodí handler, viz [Chyby |#Chyby]. Ostatní výjimky z handleru blok `catch` propustí dál. - -Volbu `--no-color`, která vypne barvy, přidáte podobně, postup najdete na stránce [Výstup do terminálu |console#volba-no-color]. - -Spouštěcí část funguje se všemi handlery, ať jsou popsané atributy, nebo předané přes `handler:`. Program bez příkazů s handlerem vytvoříte i bez atributů, `new Command('convert', handler: ...)`. - - -Nápověda -======== - -Nápovědu sestavuje [api:Nette\CommandLine\HelpRenderer] z popisů, které jste zadali. Proto vždy odpovídá tomu, co skript skutečně přijímá. Vykreslí ji pro program i pro kterýkoli příkaz, popisy zarovná a zalomí na šířku terminálu a v terminálu ji obarví: - -```php -use Nette\CommandLine\HelpRenderer; - -$renderer = new HelpRenderer; -$renderer->render($command); -``` - -Pro skript `convert` s volbami z první části vypadá takhle: - -``` -Usage: convert [options] <files>... - -Converts images to another format - -Arguments: - <files>... Images to convert - -Options: - -f, --format <jpg|png|webp> Target format (default: webp) - -q, --quality <value> Quality from 1 to 100 - --backup[=dir] Keep the originals, optionally in the given - directory - --exclude <mask>... Skip files matching the mask - --[no-]metadata Keep EXIF metadata - -n, --dry-run Only show what would be done -``` - -Všimněte si, jak nápověda ukazuje jednotlivé druhy: nepovinnou hodnotu jako `--backup[=dir]`, opakované zadání třemi tečkami, příznak s opačným tvarem jako `--[no-]metadata` a výchozí hodnotu za popisem. - -Nahoře je řádek s použitím (usage). Generuje se sám, nebo ho nahradíte vlastním přes `usage:` v `new Command()` či `addCommand()`. Pod ním je popis příkazu. Nápověda příkazu ukazuje jeho argumenty a volby a pod nadpisem `Global options:` volby programu, které v příkazu také platí. - -Několik nastavení ovlivňuje jen nápovědu: - -- `hidden: true` parametr v nápovědě schová, ale dál funguje, -- `valueName: 'dir'` pojmenuje hodnotu, jako v `--backup[=dir]`; u volby s `enum` nahradí výčet hodnot, což se hodí, když je dlouhý, -- `defaultDescription: 'current directory'` popíše výchozí hodnotu slovy; prázdný řetězec ji vynechá, takže heslo nebo token zadaný jako výchozí hodnota se v nápovědě neobjeví. - -Bez `defaultDescription` ukáže nápověda výchozí hodnotu jako `(default: webp)`, pokud je co ukázat. Logickou hodnotu, pole a prázdný řetězec vynechá. - -Vlastní nadpisy a texty přidáte metodami `addSection()` a `addText()`, v pořadí, v jakém je voláte. Text se vypíše přesně, jak jste ho napsali, bez zalamování a barev. Když žádný nadpis nepřidáte, vloží nápověda sama `Options:`, `Arguments:` a `Commands:`. Kód v popisech pište do zpětných apostrofů jako v Markdownu. V terminálu se vykreslí vlastní barvou, v prostém textu apostrofy zůstanou. - -Místo vypsání můžete nápovědu získat jako řetězec metodou `renderToString()`. Bez konzole je to prostý text bez barev, který se hodí do souboru nebo do testu. Šířku můžete určit sami: - -```php -$renderer = new HelpRenderer(width: 100); -$text = $renderer->renderToString($command); -``` - - -`--help` a `--version` ----------------------- - -U obou příznaků stojí za pozornost jedna věc: když má skript povinný argument, skončilo by `convert.php --help` chybou, že argument chybí. Proto je označte jako samostatné (`standalone`): - -```php -$command->addFlag('--help', 'Print this help', alias: '-h', standalone: true); -$command->addFlag('--version', 'Print the version', standalone: true); -``` - -Když uživatel použije samostatný příznak, parser nic nekontroluje ani nevyžaduje. Ten příznak vrátí jako `true`, ostatní příznaky tak, jak byly zadány, a všechno ostatní s výchozí hodnotou. Nápovědu tak vypíšete dřív, než skript začne číst konfiguraci nebo kontrolovat soubory. Proto je dobré, aby nastavení, které má fungovat spolu s `--help`, bylo příznakem. Třeba `--help --no-color` vypíše nápovědu bez barev. - -U příkazů se nejdřív vybere příkaz, takže `images.php convert --help` dá v `$args->command` právě `convert`. Řádka ale pořád musí dávat smysl: neznámou volbu parser odmítne jako vždy. - - -Chyby -===== - -Když uživatel zadá řádku špatně, parser vyhodí [api:Nette\CommandLine\ParseException]. Zachytíte ji a předáte metodě `renderError()`, jak ukazuje zkrácená podoba [spouštěcí části |#Spuštění programu]: - -```php -try { - $args = $parser->parse($program); - exit($args->run()); - -} catch (UsageException $e) { - $renderer->renderError($e); - exit(2); -} -``` - -`renderError()` vypíše hlášku a pod ní nápovědu k příkazu, ve kterém se uživatel spletl, a to na chybový výstup. Uživatel tak hned vidí, co je špatně i jak to napsat správně: - -``` -Error: Unknown option `--fromat`. Did you mean `--format`? - -Usage: images convert [options] <files>... -... -``` - -`ParseException` je druh výjimky [api:Nette\CommandLine\UsageException]. Tu můžete vyhodit i vy z handleru, když narazíte na chybu, kterou popis voleb vyjádřit neumí, třeba dvě volby, které nejde použít spolu. Předejte jí příkaz, ke kterému se má vypsat nápověda, a ten handler dostane, když má parametr typu `Command`: - -```php -public function convert(Command $command, bool $stdin, array $files): int -{ - if ($stdin && $files) { - throw new UsageException('Files cannot be combined with `--stdin`.', $command); - } - // ... -} -``` - -Kód pište v hláškách do zpětných apostrofů stejně jako v popisech. V terminálu ho `renderError()` vykreslí barevně a bez apostrofů, v logu apostrofy zůstanou. Chcete-li hlášku vypsat po svém, stejně ji naformátuje metoda `formatText()`. - -Běžné hlášky, jak je ukáže terminál s barvami: - -| Option `--format` requires a value. | volba bez povinné hodnoty -| Option `--dry-run` does not accept a value. | hodnota u příznaku -| Unknown option `--fromat`. Did you mean `--format`? | neznámá volba -| Missing required argument `<files>`. | chybí povinný argument -| Unexpected arguments `b`, `c`. | přebývající argumenty -| Option `--format`: expects `jpg`, `png` or `webp`, `gif` given. | hodnota není mezi povolenými -| Unknown command `conver`. Did you mean `convert`? | neznámý příkaz -| Missing command. | příkaz je povinný, ale chybí -| Option `--width` belongs to command `resize`. | volba jiného příkazu - -.[note] -Hlášky jsou anglicky. Chcete-li je napsat jinak nebo přeložit, najdete v `ParseException` vše potřebné: `reason` říká, co bylo špatně, jako případ výčtu `ParseFailure` (třeba `ParseFailure::UnknownOption`), `parameter` je parametr, kterého se chyba týká, a `token` je slovo z příkazové řádky, o které jde, přesně jak ho uživatel napsal, nebo `null`, když něco chybí. Protože `token` pochází od uživatele, před vypsáním z něj odstraňte řídicí znaky. - -Chyby ve vaší definici, třeba jméno volby bez pomlčky, dvakrát použité jméno nebo povinný argument za nepovinným, nejsou chyby uživatele. Vyhodí `InvalidArgumentException`, kterou blok `catch` nezachytí, takže se projeví jako každá jiná chyba v kódu. - - -Testování -========= - -Skript otestujete, aniž byste ho spouštěli z příkazové řádky. Parseru předáte jednotlivá slova příkazové řádky bez jména skriptu a ověříte, co z nich vyčetl. Příklady používají [Nette Tester |tester:], ale poslouží jakýkoli testovací framework: - -```php -$args = $parser->parse($program, ['convert', 'photo.jpg', '-f', 'png']); -Assert::same('png', $args->get('--format')); -Assert::same(0, $args->run()); -``` - -Špatně zadanou řádku parser hlásí výjimkou `ParseException`, takže otestujete i to, co uživatel uvidí: - -```php -Assert::exception( - fn() => $parser->parse($program, ['convert', '--format', 'gif', 'photo.jpg']), - ParseException::class, - 'Option `--format`: expects `jpg`, `png` or `webp`, `gif` given.', -); -``` - -Jediný příkaz otestujete tak, že ho parseru předáte spolu s celou řádkou. Parser ověří, že řádka opravdu vede k tomuto příkazu: - -```php -$args = $parser->parse($convert, ['convert', 'photo.jpg']); -``` - -Vlastní test si zaslouží nápověda. Je to vlastně popis rozhraní vašeho nástroje, takže test, který ji porovná s uloženým textem, zachytí každou nechtěnou změnu, třeba přejmenovanou volbu. Pevná šířka zajistí, že výsledek nezávisí na terminálu, ve kterém test běží: - -```php -$renderer = new HelpRenderer(width: 80); -Assert::matchFile(__DIR__ . '/help.txt', $renderer->renderToString($program)); -``` - -Soubor `help.txt` poprvé vytvoříte tak, že do něj uložíte, co `renderToString()` vrátí, a zkontrolujete, že je to správně. - - -{{sitename: Nette Dokumentace}} -{{maintitle: Argumenty příkazové řádky v PHP s Nette: volby a příkazy}} -{{description: Zpracování argumentů příkazové řádky v PHP: příznaky, volby a argumenty s kontrolou hodnot, příkazy a podpříkazy, generovaná nápověda a chybové hlášky.}} diff --git a/command-line/cs/console.texy b/command-line/cs/console.texy deleted file mode 100644 index 004f9d6325..0000000000 --- a/command-line/cs/console.texy +++ /dev/null @@ -1,196 +0,0 @@ -Výstup do terminálu -******************* - -.[perex] -Barevný text, klikací odkazy, řádek s průběhem a dotazy na uživatele. Třída `Console` sama pozná, kam výstup míří, a zachová se podle toho: v terminálu je výstup barevný a živý, v souboru, v rouře nebo v CI zůstane čistý text. Nemusíte kvůli tomu psát jedinou podmínku. - - -Barevný výstup -============== - -Objekt [api:Nette\CommandLine\Console] píše do jednoho výstupu, standardně na obrazovku, a umí text obarvit: - -```php -use Nette\CommandLine\Console; - -$console = new Console; -$console->writeLine('3 images converted', 'green'); -$console->writeLine('logo.png skipped, it is already WebP', 'yellow'); -$console->writeLine($console->color('gray', 'Results in ') . $outputDir); // řádek z více barev -``` - -Konzoli obvykle vytvoříte jednou na začátku skriptu a předáte ji třídám, které vypisují, třeba v konstruktoru, jak ukazuje [úvodní stránka |@home#Výstup do terminálu]. - -Barvy přidávají jen metody, nikdy značky v textu. Některé knihovny v textu hledají značky jako `<info>`, takže XML, HTML nebo vstup od uživatele musíte nejdřív escapovat, jinak se rozbije. Tady se všechno vypíše přesně tak, jak to je. - -Jestli bude text barevný, nezáleží na vašem kódu, ale na tom, kam výstup míří. V terminálu barevný je. Když výstup přesměrujete do souboru, pošlete rourou jinému programu nebo skript běží v CI, vypíše stejný kód prostý text, protože `color()` ani `link()` pak nic nepřidají. Pozor jen na text odjinud, který si barvy nese s sebou, třeba výstup jiného programu. Ty z něj případně odstraňte pomocí `Ansi::strip()`. - - -Dva výstupy ------------ - -Každý program má dva výstupy: standardní (`STDOUT`) pro výsledky a chybový (`STDERR`) pro hlášky a chyby. Uživatel je může přesměrovat každý jinam, třeba výsledky do souboru a chyby nechat na obrazovce. Proto má každý výstup vlastní konzoli, která sama pozná, jestli její výstup vede do terminálu: - -```php -$out = new Console(STDOUT); -$err = new Console(STDERR); -``` - -Bez parametru píše konzole do `STDOUT`. Když program, který čte váš výstup, skončí dřív, jako `head` v `php images.php list | head -1`, zbytek výstupu se tiše zahodí, místo aby každý zápis vyvolal chybu. - - -Barvy ------ - -Barvu zadáte jako `'popředí'` nebo `'popředí/pozadí'`, třeba `'white/blue'`. Základní jména `black`, `gray`, `silver`, `white`, `navy`, `blue`, `green`, `lime`, `teal`, `aqua`, `maroon`, `red`, `purple`, `fuchsia`, `olive` a `yellow` vykreslí terminál podle svého barevného motivu. Barvu, která má vypadat všude stejně, zapíšete hexadecimálně, `'#87D7FF'` nebo zkráceně `'#8DF'`, případně jako číslo 0-255 rozšířené palety, třeba `'117'`. - -Neznámá barva vyhodí výjimku, i když jsou barvy vypnuté, takže překlep odhalíte hned. Barva `null` znamená žádnou barvu, což se hodí pro podmínky: `$console->color($ok ? 'green' : null, $text)`. - -Ne každý terminál umí všechny barvy. Konzole to zjistí sama a barvu vykreslí tak věrně, jak to jde: v moderním terminálu přesně, ve starším jako nejbližší barvu, kterou umí. Kolik barev terminál zvládne, vyjadřuje [api:Nette\CommandLine\ColorDepth]: `TrueColor` (jakákoli barva), `Ansi256` (paleta 256 barev), `Ansi16` (16 základních barev) a `None` (bez barev). - -Chování můžete ovlivnit proměnnými prostředí, které znají i jiné nástroje. [NO_COLOR |https://no-color.org] barvy vypne. [FORCE_COLOR |https://force-color.org] je zapne i při výstupu do souboru nebo roury, hodnota `2` nebo `3` přitom vyžádá aspoň `Ansi256`, případně `TrueColor`. `COLORTERM` s hodnotou `truecolor` nebo `24bit` řekne, že terminál umí jakoukoli barvu. - - -Volba `--no-color` ------------------- - -Uživatelé často chtějí barvy vypnout přímo na příkazové řádce. Přidejte programu příznak `--no-color` a po zpracování řádky vypněte barvy svým konzolím metodou `setColorDepth()`: - -```php -use Nette\CommandLine\ColorDepth; - -$out = new Console(STDOUT); -$err = new Console(STDERR); -$tools = new ImageTools($out); - -$program = new Command('images'); -$program->addFlag('--no-color', 'Print plain output without colors'); -$program->addCommandsFrom($tools); - -$parser = new Parser; -$args = $parser->parse($program); -if ($args->get('--no-color')) { - $out->setColorDepth(ColorDepth::None); - $err->setColorDepth(ColorDepth::None); -} -``` - -Aby se volba týkala i nápovědy a chybových hlášek, vypisujte je přes renderery s těmito konzolemi: `new HelpRenderer($out)` pro nápovědu a `new HelpRenderer($err)` pro chyby. Jen chyba v samotné příkazové řádce se nahlásí dřív, než parser příznak přečte, a zůstane proto barevná. Úplně všude barvy vypne proměnná prostředí `NO_COLOR`. - -Konzoli bez barev můžete vytvořit i rovnou, `new Console(STDOUT, colorDepth: ColorDepth::None)`. - -Konzoli se můžete i zeptat, jak na tom je: - -```php -$console->hasColors(); // jestli vypisuje barevně -$console->getColorDepth(); // kolik barev používá, třeba ColorDepth::Ansi256 -$console->isTerminal(); // jestli výstup vede do terminálu, kde se někdo dívá -$console->getWidth(); // šířka terminálu ve sloupcích, mimo terminál 80 -``` - -`hasColors()` a `isTerminal()` odpovídají na dvě různé otázky. Podle první se rozhodujte, jestli barvit, podle druhé, jestli ukazovat věci, které mají smysl jen pro člověka u obrazovky, třeba řádek s průběhem nebo dotaz. Uživatel totiž může barvy vypnout a přitom u terminálu sedět. - -`getWidth()` se terminálu zeptá jen jednou a výsledek si pamatuje, takže změnu velikosti okna pozná až nová konzole. Proměnná prostředí `COLUMNS` má přednost, což se hodí, když chcete vyzkoušet, jak výstup vypadá v úzkém terminálu. - - -Klikací odkazy -============== - -Odkaz se skládá stejně jako barva. V terminálu z textu udělá `link()` odkaz, na který jde kliknout. V souboru, v rouře nebo bez barev vypíše text a za ním adresu v závorce, takže v logu adresa nezmizí: - -```php -$console->writeLine('See ' . $console->link('https://nette.org')); -$console->writeLine($console->link('https://nette.org', 'the manual')); // mimo terminál 'the manual (https://nette.org)' -``` - -`link()` odmítne adresu s řídicími znaky, protože by mohly odkaz předčasně ukončit a poslat terminálu vlastní příkazy. Znaky s diakritikou a jiné znaky mimo ASCII nevadí. - - -Průběh a stavové řádky -====================== - -Při delší práci se hodí ukazovat, co skript právě dělá a kolik má hotovo. Na to je `setStatus()`. Vykreslí řádek na konec výstupu a další výpis ho smaže, vypíše se nad ním a další `setStatus()` řádek nakreslí znovu. Uživatel tak vidí přibývající výsledky a pod nimi jeden řádek, který se průběžně mění: - -``` -photo.jpg converted -logo.png converted -3/10 holiday.jpg -``` - -V kódu to vypadá takhle: - -```php -foreach ($files as $i => $file) { - $console->setStatus(sprintf('%d/%d %s', $i + 1, count($files), $file)); - // ... převod souboru - $console->writeLine("$file converted"); // smaže stavový řádek a vypíše se nad něj -} - -$console->clearStatus(); -``` - -Stavový řádek se v terminálu jen kreslí, do výstupu se nezapisuje. Když výstup do terminálu nevede, `setStatus()` neudělá vůbec nic, takže v souboru s logem jsou jen skutečné řádky. Stavových řádků může být i víc najednou, každý znak nového řádku začne další. Příliš dlouhý řádek se zkrátí na šířku terminálu, protože zalomený by se nedal překreslit. - -Dokud je stavový řádek vidět, kurzor je schovaný. Objeví se znovu, když stav smažete, když konzoli uvolníte a také na konci skriptu, i po `exit()`, nezachycené výjimce nebo fatální chybě. Jen Ctrl+C ukončí PHP okamžitě, takže ho musí ošetřit vaše aplikace sama. - -Stavový řádek smaže jen konzole, která ho nakreslila. Máte-li zvlášť konzoli pro výsledky a zvlášť pro chyby, kreslete stav na tu, kam jdou chyby. Jinak by se varování vypsané během práce otisklo přes stavový řádek. - - -Měření textu -============ - -Když chcete vypisovat zarovnané sloupce, `strlen()` vám nepomůže. Barvy jsou v textu uložené jako neviditelné escape sekvence, které nezabírají žádné místo, a čínský znak nebo emoji zabere na obrazovce dva sloupce. Třída [api:Nette\CommandLine\Ansi] měří a zkracuje text tak, jak ho ukáže terminál: - -```php -use Nette\CommandLine\Ansi; - -Ansi::measure($console->color('red', 'error')); // 5, escape sekvence se nepočítají -Ansi::pad($console->color('red', 'error'), 10); // doplní na 10 sloupců, ne na 10 bajtů -Ansi::truncate($path, 30, keepEnd: true); // zkrátí začátek, aby zůstalo jméno souboru -Ansi::strip($text); // text bez escape sekvencí -``` - -Šířky se řídí tabulkami Unicode, takže správně změří i emoji a vlajky. Řídicí znaky, třeba tabulátor, nezabírají žádný sloupec. `strip()` odstraní jen escape sekvence, ostatní řídicí znaky v textu nechá, takže z nedůvěryhodného textu bezpečný pro výpis neudělá. - - -Dotaz na uživatele -================== - -Někdy skript potřebuje rozhodnutí, které může udělat jen člověk. Přepsat existující soubor? Přeskočit obrázek, který nejde přečíst? [api:Nette\CommandLine\Prompt] položí otázku a počká, až uživatel odpoví jednou z možností, které povolíte: - -```php -use Nette\CommandLine\Prompt; - -$prompt = new Prompt($console); -if ($prompt->isInteractive()) { - $answer = $prompt->choose('Overwrite photo.webp?', ['y', 'n', 'a', 'q'], default: 'n'); -} -``` - -Uživatel uvidí `Overwrite photo.webp? [y/N/a/q]` a výchozí možnost poznáte podle velkého písmena. Které možnosti nabídnete, je na vás, tady by to mohlo být ano, ne, ano pro všechny a konec. Dokud uživatel neodpoví jednou z nich, prompt se ptá znovu. Na velikosti písmen nezáleží a `choose()` vrátí možnost přesně tak, jak jste ji zadali. Enter na prázdném řádku vybere výchozí možnost, a pokud žádná není, prompt se zeptá znovu. - -Když si každá možnost zaslouží pár slov, předejte je jako pole s popisy a prompt nad otázkou vypíše nabídku: - -```php -$answer = $prompt->choose('What now?', [ - 's' => 'Skip this image', - 'r' => 'Retry', - 'c' => 'Cancel the conversion', -], default: 'r'); -``` - -``` - [s] Skip this image - [r] Retry - [c] Cancel the conversion -What now? [s/R/c] -``` - -Když vstup skončí, třeba protože uživatel stiskl Ctrl+D, vrátí `choose()` hodnotu `null`, nikdy výchozí možnost. Zavřený vstup tak nemůže potichu odpovědět ano. Co to pro skript znamená, rozhodnete sami, třeba `?? 'q'`. - -Prompt se ptá jen tehdy, když vstup i konzole vedou do terminálu, takže nikdy nevezme odpověď z roury, u které nikdo nesedí. Jinak `choose()` vyhodí `LogicException` dřív, než cokoli vypíše nebo přečte. Proto se nejdřív zeptejte `isInteractive()` a rozhodněte, co skript udělá bez člověka, třeba skončí s radou použít volbu příkazové řádky, jako `--overwrite`. Předejte promptu konzoli, která kreslí stavový řádek: otázka ho pak smaže a další stav se po odpovědi nakreslí na čistý řádek. - - -{{sitename: Nette Dokumentace}} -{{maintitle: Barevný výstup do terminálu v PHP s Nette: odkazy a průběh}} -{{description: Barevný výstup do terminálu v PHP, který se v rouře a v CI sám vypne, klikací odkazy, řádek s průběhem a dotazy na uživatele.}} diff --git a/command-line/en/@home.texy b/command-line/en/@home.texy deleted file mode 100644 index c4dcaee2c7..0000000000 --- a/command-line/en/@home.texy +++ /dev/null @@ -1,249 +0,0 @@ -Nette Command-Line -****************** - -<div class=perex> - -Are you writing a PHP script that runs from the command line? Whether it's a little helper for one afternoon or a tool with a whole set of commands, this library does all the tedious work around it for you: - -- Reads options and arguments and checks their values -- Builds the help and clear error messages -- Handles tools with several commands too -- Prints in color, but switches colors off by itself in a pipe or a log -- Has no dependencies - -</div> - - -Installation -============ - -```shell -composer require nette/command-line -``` - -It requires PHP version 8.2 and supports PHP up to 8.6. - - -First Script -============ - -It's best shown with an example. Say you have a pile of images and want to convert them to another format. Let's write a script `convert.php` for it, to be called like this: - -```shell -php convert.php photo.jpg logo.png --format png -php convert.php photo.jpg --dry-run -``` - -The command line is made of three kinds of items. **Arguments** are plain values without a dash, here the file names. An **option** has a name and a value: `--format png` says which format to convert to, and can be shortened to `-f png`. A **flag** is just a switch with no value: `--dry-run` (`-n` for short) makes the script only print what it would do, without touching a single file. - -How much code does it take to make all of this work? Surprisingly little. You write the conversion itself as an ordinary method, and the attributes on its parameters tell how each value is given on the command line: - -```php -use Nette\CommandLine\Attributes as Cli; -use Nette\CommandLine\Command; -use Nette\CommandLine\Parser; - -final class ImageConverter -{ - #[Cli\Command('Converts images to another format')] - public function convert( - #[Cli\Argument('Images to convert', repeatable: true)] - array $files, - #[Cli\Option('Target format', alias: '-f', enum: ['jpg', 'png', 'webp'])] - string $format = 'webp', - #[Cli\Flag('Only show what would be done', alias: '-n')] - bool $dryRun = false, - ): int - { - // converts each file to $format, with $dryRun only prints what it would do - return 0; - } -} -``` - -Notice that you never touch `$argv` or parse anything. The parameter `$format` becomes the option `--format`, `$dryRun` the flag `--dry-run`, and the default value of a parameter applies when the user leaves the option out. The texts in the attributes describe the items and show up in the help. `repeatable: true` says there may be more files, so the method gets them as an array. `enum` lists the allowed values, so the script refuses `--format gif` before the method is even called. - -All that's left is to run the method. Below the class, in the same file `convert.php`, add: - -```php -require __DIR__ . '/vendor/autoload.php'; - -$converter = new ImageConverter; -$program = Command::from($converter->convert(...)); - -$parser = new Parser; -$args = $parser->parse($program); -exit($args->run()); -``` - -Writing `$converter->convert(...)` doesn't call the method, it passes it on. `Command::from()` reads from it what the script accepts. `Parser` then reads the command line and checks the values. Finally `run()` calls your method with the right values. Whatever the method returns becomes the exit code of the script. That's how the shell, CI or another script learns how things went: 0 means success, anything else a problem. - -And since people at the command line have their habits, the script copes with all the usual spellings: `--format png`, `--format=png` and `-f png` mean the same, single-letter options can be stuck together (`-nf png` is `-n -f png`), and options may stand anywhere between the files. - - -Help and Errors -=============== - -Every decent script should have help and report mistakes clearly. Both take just a few lines; extend the part of `convert.php` that runs the method like this (the two new `use` lines belong at the top, with the others): - -```php -use Nette\CommandLine\HelpRenderer; -use Nette\CommandLine\UsageException; - -$converter = new ImageConverter; -$program = Command::from($converter->convert(...)); -$program->addFlag('--help', 'Print this help', alias: '-h', standalone: true); - -$parser = new Parser; -$renderer = new HelpRenderer; - -try { - $args = $parser->parse($program); - if ($args->get('--help')) { - $renderer->render($args->command); - exit(0); - } - exit($args->run()); - -} catch (UsageException $e) { - $renderer->renderError($e); - exit(2); -} -``` - -The flag `--help` is standalone, so it works even when the user hasn't given the required files. The parser checks nothing else in that case. `HelpRenderer` builds the help from the descriptions in the attributes and prints it for `$args->command`, the command the user selected; in a script without commands, that's the whole program. Now try running `php convert.php --help`: - -[* help.webp .(Help of the script in a terminal) *] - -You didn't have to write it, and yet it is complete, aligned and in color. And since it comes from the same method that does the work, it will never claim anything other than what the script really can do. - -Mistakes are taken care of by the `catch` block. When the user makes a typo or enters nonsense, the parser throws `UsageException`, and instead of a stack trace `renderError()` turns it into a clear answer: - -``` -$ php convert.php --fromat png photo.jpg -Error: Unknown option `--fromat`. Did you mean `--format`? - -$ php convert.php --format gif photo.jpg -Error: Option `--format`: expects `jpg`, `png` or `webp`, `gif` given. -``` - -The help is printed right below the message, so the user doesn't have to look anything up, and the script ends with exit code 2, the usual way to report a wrong command line. Your method isn't called at all in that case, so it never runs with nonsensical values. - -This block that runs the program is the same in every script, so you write it once and from then on only change what goes into `$program`. - - -Without Attributes -================== - -Attributes are the most comfortable way: each option is described right next to the parameter that receives it, the method gets the values straight in its parameters, and there's no separate definition to keep in sync with the code. Underneath them, though, lies a plain API you can use directly. It comes in handy for a quick script without a class, or when you put the options together at runtime: - -```php -use Nette\CommandLine\Command; -use Nette\CommandLine\Parser; - -$command = new Command('convert'); -$command->addOption('--format', 'Target format', alias: '-f', enum: ['jpg', 'png', 'webp']); -$command->addArgument('files', 'Images to convert', repeatable: true); - -$parser = new Parser; -$args = $parser->parse($command); -$args->get('--format'); // 'png' -$args->get('files'); // ['photo.jpg', 'logo.png'] -``` - -The help and errors are handled exactly as above. Everything options and arguments can do is on the page [Arguments, Options and Commands |arguments]. - - -Several Commands -================ - -Scripts have a habit of growing. Today it converts images, tomorrow it should shrink them too. Such tools are usually controlled by commands, much like `composer install` and `composer update`: - -```shell -php images.php convert photo.jpg --format png -php images.php resize photo.jpg --width 800 -php images.php --quiet convert photo.jpg -``` - -You don't have to rewrite anything. Each command is one method with the `#[Cli\Command]` attribute, just like `convert()` above, and a whole class of such methods is added in one call. Options that apply to all commands, like `--quiet` on the last line, are added to the program as a whole: - -```php -$tools = new ImageTools; // contains the methods convert() and resize() -$program = new Command('images'); -$program->addFlag('--quiet', 'Print nothing but errors', alias: '-q'); -$program->addFlag('--help', 'Print this help', alias: '-h', standalone: true); -$program->addCommandsFrom($tools); -``` - -The rest of the script, the parser, the help and the errors, is the same as in `convert.php`. Since `render($args->command)` prints the help of the selected command, each command gets its own help (`php images.php convert --help`). Commands can be nested too, so `php images.php cache clear`, for example, is the command `clear` inside the command `cache`. More in [Commands |arguments#Commands]. - - -Terminal Output -=============== - -A script doesn't only read, it also writes. That's what the `Console` class is for, and it follows where the output goes. In a terminal it writes in color, and when you redirect the output to a file or the script runs in CI, it leaves the colors out by itself, so the log stays clean. The same with progress: in a terminal it shows on a line that keeps being rewritten, and it never gets into the log at all. - -You create the console once and hand it to the class that needs it, for example in the constructor: - -```php -final class ImageConverter -{ - public function __construct( - private Console $console, - ) { - } - - #[Cli\Command('Converts images to another format')] - public function convert( - #[Cli\Argument('Images to convert', repeatable: true)] - array $files, - ): int - { - foreach ($files as $file) { - $this->console->setStatus("Converting $file"); // the progress line - // ... - $this->console->writeLine("$file converted", 'green'); - } - return 0; - } -} - -$console = new Console; -$converter = new ImageConverter($console); -``` - -On top of that, text is printed exactly as it is. Some libraries look for color markup in the text, so XML or user input has to be escaped first or it breaks. Nothing like that can happen here. More on the page [Terminal Output |console]. - - -Testing -======= - -Finally, good news for everyone who writes tests: you test the script without running it from the command line. You pass the parser the individual words of the command line as if the user had typed them, and check what it read from them or which mistake it reported. The parser reports a wrong command line with `ParseException`, a kind of the `UsageException` we caught above. The example uses [Nette Tester |tester:]: - -```php -$parser = new Parser; - -Assert::exception( - fn() => $parser->parse($program, ['--format', 'gif', 'photo.jpg']), - ParseException::class, - 'Option `--format`: expects `jpg`, `png` or `webp`, `gif` given.', -); - -$args = $parser->parse($program, ['photo.jpg', '--dry-run']); -Assert::same(0, $args->run()); -``` - -More in [Testing |arguments#Testing]. - - -Upgrading from 1.x -================== - -Version 2.0 is a new API with no compatibility layer: in 1.x a single `Parser` held the definition, parsed the line and printed the help text it had been given, while in 2.0 a `Command` is the definition, `Parser` reads a line against it and `HelpRenderer` draws the help from it. - -The [Nette plugin for Claude Code and Codex |ai:agent-plugins] includes a guide by which your coding agent rewrites 1.x code to 2.0 faithfully and reports every place where 2.0 behaves differently. - - -{{sitename: Nette Documentation}} -{{maintitle: Nette Command-Line: PHP CLI Scripts and Argument Parsing}} -{{description: Command-line scripts in PHP: options, arguments and commands, help and clear error messages, colored output and progress lines. No dependencies.}} diff --git a/command-line/en/@left-menu.texy b/command-line/en/@left-menu.texy deleted file mode 100644 index cca595a286..0000000000 --- a/command-line/en/@left-menu.texy +++ /dev/null @@ -1,5 +0,0 @@ -Nette Command-Line -****************** -- [Overview |@home] -- [Arguments, Options and Commands |arguments] -- [Terminal Output |console] diff --git a/command-line/en/arguments.texy b/command-line/en/arguments.texy deleted file mode 100644 index 4b6f0dcc04..0000000000 --- a/command-line/en/arguments.texy +++ /dev/null @@ -1,694 +0,0 @@ -Arguments, Options and Commands -******************************* - -.[perex] -This page describes in detail how to say what your script accepts on the command line, how values are checked and converted, how to build a tool with several commands, and how the help and error messages come about. The image conversion script from the [overview |@home] will keep us company the whole way. - - -Flags, Options and Arguments -============================ - -Let's recall how our script is called: - -```shell -php convert.php photo.jpg logo.png --format png --dry-run -``` - -There are three kinds of items on the command line: - -- **arguments** are values without a dash, here `photo.jpg` and `logo.png`; they are told apart by their order, which is why they are called positional, -- an **option** has a name and a value, here `--format png`, -- a **flag** has just a name and works as a switch, here `--dry-run`. - -On the [overview |@home] we described the script with attributes. Now let's look at the API underneath them. Everything in this part applies to attributes as well, which take the same settings, only you write them on the parameters of a method. You describe the script with a [api:Nette\CommandLine\Command] object and its methods `addArgument()`, `addOption()` and `addFlag()`: - -```php -use Nette\CommandLine\Command; -use Nette\CommandLine\Parser; - -$command = new Command('convert', 'Converts images to another format'); -$command->addArgument('files', 'Images to convert', repeatable: true); -$command->addOption('--format', 'Target format', alias: '-f'); -$command->addFlag('--dry-run', 'Only show what would be done', alias: '-n'); - -$parser = new Parser; -$args = $parser->parse($command); -``` - -Each `add*()` method takes a name, a description that will show up in the help, and then named arguments with further settings, like `alias` for the short form here. [api:Nette\CommandLine\Parser] reads the command line according to this description and returns a [api:Nette\CommandLine\ParseResult] object. Its `get()` method reads a value by name, exactly as you wrote it, dashes included: - -```php -$args->get('files'); // ['photo.jpg', 'logo.png'] -$args->get('--format'); // 'png', or null when the option is missing -$args->get('--dry-run'); // true, or null when the flag is missing -``` - -If you make a typo in the name, such as `$args->get('--fromat')`, you get an exception. That way a typo can't pass for an option the user simply didn't give. `toArray()` returns all the values at once. - -`parse()` reads the command line the script was started with. You can also pass it your own array of individual words, without the script name, which comes in handy in tests: - -```php -$args = $parser->parse($command, ['photo.jpg', '--format', 'png']); -``` - -Two methods help when you need more than the value itself. `isEmpty()` tells you the user wrote nothing at all after the script name. `wasProvided('--format')` tells you whether the user really gave the option, so you can tell that they wrote `--format webp` even when `webp` is also the default value. That's useful, for example, when merging the command line with a configuration file. - -Watch out for one thing: `Parser` only reads. When the user gets something wrong, it throws an exception, and it doesn't print the help by itself either. How to add both is shown in the section [Running the Program |#Running the Program]. - - -Flags ------ - -A flag is a switch: when it's on the command line, its value is `true`, when it's missing, `null`: - -```php -$command->addFlag('--dry-run', alias: '-n'); -// --dry-run → true -// -n → true -// (missing) → null -``` - -Some things you want switched on until the user turns them off. Keeping EXIF metadata during conversion, for instance. With `negatable: true` the flag also gets the opposite form `--no-…`, and the default value says what applies when the user writes neither: - -```php -$command->addFlag('--metadata', 'Keep EXIF metadata', negatable: true, default: true); -// --metadata → true -// --no-metadata → false -// (missing) → true -``` - -Single-letter flags can be stuck together into one word: `-nv` means the same as `-n -v`. The last letter may even belong to an option with a value, so `-nf png` is `-n -f png`. - - -Options -------- - -An option carries a value. The user can separate it with a space or an equals sign, and use the alias instead of the long name: - -```php -$command->addOption('--format', alias: '-f'); -// --format png → 'png' -// --format=png → 'png' -// -f png → 'png' -// --format → error, the value is missing -// (missing) → null -``` - -So the option itself is optional, but once the user uses it, they must write a value too. When the user gives the same option several times, the last value wins. If you want them all, use [repeated values |#Repeated Values]. - -Sometimes an option comes in handy that may have a value but doesn't have to. Say `--backup` keeps the original files, and if the user adds a directory, stores them there. That's what `valueOptional: true` does: - -```php -$command->addOption('--backup', valueOptional: true); -// --backup=old → 'old' -// --backup → true -// (missing) → null -``` - -An optional value has to be attached with an equals sign. Writing `--backup old` would be ambiguous: is `old` the value of the option, or another file? So the library takes it as a file and `--backup` as an option without a value. - -.[note] -The exception is an option that also has a [list of allowed values |#Allowed Values]. There the value is taken even after a space, but only when it's one of the allowed ones, so nothing else gets swallowed by mistake. For `--strip` with the values `exif` and `all`, `--strip all` therefore means the value `all`, while `--strip photo.jpg` means the option without a value plus a file. That's why the definition refuses a command named like one of the values, and an argument equal to a value has to be written after `--`. - - -Arguments ---------- - -Arguments are values without a dash, told apart by their order. They are required unless you say otherwise: - -```php -$command->addArgument('input'); -// photo.jpg → 'photo.jpg' -// (missing) → error, the required argument is missing - -$command->addArgument('output', optional: true); -// (missing) → null -``` - -Arguments may stand anywhere, before the options or after them. For values that start with a dash, a few rules apply that you know from other tools: - -```php -// convert.php -- -weird.jpg → file '-weird.jpg', everything after -- is an argument -// convert.php - → file '-', by convention the standard input -// convert.php --rotate -90 → --rotate gets the value '-90' -``` - -A negative number is thus an ordinary value. The only exception is a command that itself has an option named like a number, such as `-1` in `ls -1`. There every `-90` is taken for an option, and the user has to write a negative value with an equals sign, `--rotate=-90`, or as an argument after `--`. - - -Default Values --------------- - -`default` sets the value of an option or an argument for the case the user doesn't give it: - -```php -$command->addOption('--format', default: 'webp'); -// --format png → 'png' -// (missing) → 'webp' -``` - -Don't confuse it with `valueOptional`. That one says what applies when the user writes the option without a value, whereas `default` applies when they don't write it at all. An option can have both: - -```php -$command->addOption('--backup', valueOptional: true, default: 'backup'); -// --backup=old → 'old' -// --backup → true -// (missing) → 'backup' -``` - -The default value is returned exactly as you gave it, so it is neither checked nor converted. A required argument with a default value would make no sense, so the definition refuses it. Mark such an argument as optional. - - -Allowed Values --------------- - -`enum` limits the value to a list. The script refuses anything else with a message that names the allowed values: - -```php -$command->addOption('--format', enum: ['jpg', 'png', 'webp']); -// --format gif → Option `--format`: expects `jpg`, `png` or `webp`, `gif` given. -``` - -The values are non-empty strings, so write numbers as strings too, such as `['1', '2']`. Instead of a list you can pass a backed enum and get its case straight away: - -```php -enum Format: string -{ - case Jpg = 'jpg'; - case Png = 'png'; - case Webp = 'webp'; -} - -$command->addOption('--format', enum: Format::class, default: Format::Webp); -// --format png → Format::Png -// (missing) → Format::Webp -``` - - -Repeated Values ---------------- - -With `repeatable: true` the user can give an option or an argument several times, and you get all the values in an array: - -```php -$command->addOption('--exclude', 'Skip files matching the mask', repeatable: true); -// --exclude '*.tmp' --exclude 'thumb*' → ['*.tmp', 'thumb*'] -// (missing) → [] - -$command->addArgument('files', repeatable: true); -// photo.jpg logo.png → ['photo.jpg', 'logo.png'] -``` - -A repeated argument takes all the remaining values, so it has to be the last one. - - -Converting Values ------------------ - -What comes from the command line is always a string. When you need a number or a path, give the option or the argument a `normalizer`, a function that checks the value and converts it. Ready-made ones live in the [api:Nette\CommandLine\Normalizers] class: `int()` and `float()` convert a number, optionally within a range, and `realPath()` turns the path of an existing file into an absolute one: - -```php -use Nette\CommandLine\Normalizers; - -$command->addOption('--quality', normalizer: fn($v) => Normalizers::int($v, min: 1, max: 100)); -// --quality 80 → 80 -// --quality abc → Option `--quality`: expects an integer, `abc` given. -// --quality 150 → Option `--quality`: expects a number from 1 to 100, `150` given. - -$command->addOption('--watermark', normalizer: Normalizers::realPath(...)); -// --watermark logo.png → '/home/me/images/logo.png' -// --watermark missing.png → Option `--watermark`: file path `missing.png` not found. -``` - -Without a range, passing the method itself is enough, `Normalizers::int(...)`. Your own normalizer is any function that takes a string and returns the converted value. When the value is wrong, it throws an exception, and the library turns it into a clear message naming the option, as in the example above. You can also call a ready-made normalizer from your own, for example when you want to adjust the path first: - -```php -$command->addOption('--watermark', normalizer: fn($v) => Normalizers::realPath("watermarks/$v")); -``` - -A normalizer runs only on values that really came from the command line. It is called neither on the default value nor on the `true` of an option without its optional value. - -.[note] -A range no number fits into, such as `min` greater than `max`, is a mistake in the code, not in what the user typed. That's why it throws `ValueError`, which is not turned into a message for the user. From PHP 8.6 on, instead of the function `fn($v) => Normalizers::int($v, min: 1)` you can write the shorter `Normalizers::int(?, min: 1)`. - - -Thorough Validation -------------------- - -For more complex rules you can pass the result on to [Nette Schema |schema:]. Command-Line doesn't depend on it, it's simply a second step. The parser returns strings as the user wrote them, and `null` for what they didn't give. So leave out the `null` values to let the defaults of the schema apply: - -```php -use Nette\Schema\Expect; -use Nette\Schema\Processor; - -$schema = Expect::structure([ - '--quality' => Expect::string()->pattern('[1-9][0-9]?|100')->castTo('int')->default(85), - 'files' => Expect::listOf('string'), -])->otherItems(); - -$values = array_filter($args->toArray(), fn($value) => $value !== null); -$options = (new Processor)->process($schema, $values); -``` - - -Parameters in Code ------------------- - -The `add*()` methods return the parameter object, whose settings you can read back as properties, such as `$option->alias`. A parameter can't be changed later, so it is checked the moment it's added, and a mistake in the definition, such as an option name without a dash or a name used twice, throws `InvalidArgumentException` right away. Later you can reach a parameter with `getParameter()`. The second argument checks that it is an option, and static analysis then knows which properties it has: - -```php -use Nette\CommandLine\Parameters\Option; - -$format = $command->getParameter('--format', Option::class); -``` - -Not every setting makes sense for every kind. A flag has no value, so `addFlag()` has neither `enum` nor `normalizer`. Those belong to `addOption()` and `addArgument()`, while `valueOptional` and `valueName` belong to `addOption()` alone. - - -Commands -======== - -When a script does several things, it is usually controlled by commands. Our image tool will convert and also shrink images, and it will have one option shared by everything: - -```shell -php images.php convert photo.jpg --format png -php images.php resize photo.jpg --width 800 -php images.php --quiet convert photo.jpg -``` - -You add a command with `addCommand()`. It returns a new `Command`, which you set up exactly like the program itself: - -```php -$program = new Command('images'); -$program->addFlag('--quiet', 'Print nothing but errors', alias: '-q'); - -$convert = $program->addCommand('convert', 'Converts images to another format'); -$convert->addArgument('files', 'Images to convert', repeatable: true); -$convert->addOption('--format', 'Target format', alias: '-f', enum: ['jpg', 'png', 'webp']); - -$resize = $program->addCommand('resize', 'Shrinks images to the given width'); -$resize->addArgument('files', 'Images to resize', repeatable: true); -$resize->addOption('--width', 'Width in pixels', normalizer: Normalizers::int(...)); -``` - -The command is recognized by the first word without a dash, and after parsing, `$args->command` tells you which one it was. When the user writes no command, `$args->command` is the program itself. If a command should be required, create the program with `commandRequired: true`. - -Options of the program, like `--quiet` here, apply in all commands, and the user can write them before the command or after it: `images.php -q convert photo.jpg` and `images.php convert photo.jpg -q` mean the same. An option of another command is refused, and the library tells where it belongs: - -``` -$ php images.php convert photo.jpg --width 800 -Error: Option `--width` belongs to command `resize`. -``` - -The result holds only the values of the program and of the selected command. Neighboring commands may use the same option names, so `convert` could well have its own `--width` with a different meaning. - - -Nested Commands ---------------- - -A command can have its own commands, and those their own again. Managing the thumbnail cache, for instance: - -```php -$cache = $program->addCommand('cache', 'Manages the thumbnail cache'); -$cache->addCommand('clear', 'Deletes all cached thumbnails'); -$cache->addCommand('stats', 'Shows how much space the cache takes'); -``` - -The user then writes `php images.php cache clear`. A command that has its own commands can't take arguments, because there would be no telling whether the word after it is an argument or a command. - - -Handlers --------- - -What's left is to say what should happen for which command. The most comfortable way is to give each command a handler, a method or function that does its work. You pass it as `handler:` right when creating the command: - -```php -final class ImageTools -{ - public function convert(array $files, ?string $format, ?bool $quiet): int - { - // converts the files, prints nothing with $quiet - return 0; - } -} - -$tools = new ImageTools; -$convert = $program->addCommand('convert', 'Converts images to another format', handler: $tools->convert(...)); -``` - -You don't pass the values to the handler yourself. Each of its parameters gets a value by its name: `$files` the argument `files`, `$format` the option `--format`, and since the options of the program count too, `$quiet` gets `--quiet`. A name of several words is converted: `$maxWidth` becomes `--max-width`. A parameter typed `ParseResult` gets the whole result, for example when you need `wasProvided()`, and a parameter typed `Command` gets the selected command. - -The selected command is then run by `run()`. It returns whatever the handler returned, and if the handler returns nothing, that means 0: - -```php -$args = $parser->parse($program); -exit($args->run()); -``` - -How to add the help and error reporting is shown in the section [Running the Program |#Running the Program]. - -The types of the parameters are checked against what the option can give. An option the user didn't give and that has no default value is `null`, so it belongs in `?string`, not in `string`. If the handler had `string $format`, the library reports it with an exception the very first time the script runs, before anything is done, and even for commands that aren't being run at the moment: - -``` -Command 'convert': parameter $format of handler ImageTools::convert() does not accept null, which option --format gives when absent. -``` - -.[note] -Other mistakes in the signature are reported the same way: a parameter with no option or argument to bind to, a parameter with a default value the input would always override anyway, a variadic parameter, and a parameter passed by reference. A normalizer has to declare its return type, otherwise there would be nothing to compare the parameter with. A repeated option or argument arrives as an array. - - -Commands Described by Attributes --------------------------------- - -A handler can also describe its command itself, right next to its code. That's how we did it on the [overview |@home]. `#[Command]` gives the method a description, and `#[Flag]`, `#[Option]` and `#[Argument]` turn its parameters into flags, options and arguments: - -```php -use Nette\CommandLine\Attributes as Cli; - -final class ImageTools -{ - #[Cli\Command('Converts images to another format')] - public function convert( - ?bool $quiet, - #[Cli\Argument('Images to convert', repeatable: true)] - array $files, - #[Cli\Option('Target format', alias: '-f', enum: ['jpg', 'png', 'webp'])] - string $format = 'webp', - ): int - { - // converts the files, prints nothing with $quiet - return 0; - } - - #[Cli\Command('Shrinks images to the given width')] - public function resize(/* ... */): int - { - // ... - } -} - -$tools = new ImageTools; -$program->addCommandsFrom($tools); -``` - -`addCommandsFrom()` adds all the public methods with the `#[Cli\Command]` attribute, in the order they appear in the class. A single method is added with `addCommandFrom($tools->convert(...))`. The result is perfectly ordinary commands, as if you had called `addCommand()` and `addOption()` yourself. - -The names are derived: the command from the name of the method, so `removeDuplicates()` becomes `remove-duplicates`, and the options from the names of the parameters, so `$maxWidth` becomes `--max-width`. You set a different name in the attribute with `name:`. The default value of a parameter becomes the default value of the option, an argument with a default value is optional, and a parameter typed by a backed enum accepts its cases as the allowed values. - -A parameter without an attribute, like `$quiet` above, binds to an option by its name, here to `--quiet` of the program. It must not have a default value, since the option of the program carries that, and so it has to come before the parameters that have one. - -The attributes `#[Flag]`, `#[Option]` and `#[Argument]` take the same settings as `addFlag()`, `addOption()` and `addArgument()`, only the name and the default value come from the parameter. `#[Command]` takes `description`, `usage`, `commandRequired` and `name`. - -The attribute `#[Command]` has the same name as the class `Nette\CommandLine\Command`. That's why we import the attributes under the alias `Cli` and write `#[Cli\Command]`. - -A program without commands is described the same way: `Command::from()` creates the whole program from a handler, just as we did on the overview. Its name isn't derived from the method, which names an action rather than the program, so without `name:` the help names it after the script file. Such a program is an ordinary `Command`, so you can add commands to it as well. - -An attribute can only hold a constant expression, which is why a normalizer is written as an array of a class and a method. A ready-made one as `[Normalizers::class, 'int']`, your own as `[self::class, 'parseWidth']`. From PHP 8.5 on you can write a `static function` right into the attribute. When a normalizer also needs a range, write a static method that passes it on: - -```php -final class ImageTools -{ - #[Cli\Command('Shrinks images to the given width')] - public function resize( - #[Cli\Argument('Images to resize', repeatable: true)] - array $files, - #[Cli\Option('Width in pixels', normalizer: [self::class, 'parseWidth'])] - int $width = 800, - ): int - { - // shrinks the files to $width - return 0; - } - - public static function parseWidth(string $value): int - { - return Normalizers::int($value, min: 1); - } -} -``` - -With attributes, the names of your methods and variables become the interface of your tool. When you rename `$maxWidth` to `$widthLimit`, `--max-width` turns into `--width-limit`, and every script that calls your tool stops working. A [test of the help |#Testing] catches such a change. - - -Options Shared by Several Commands ----------------------------------- - -Commands often need the same options. Both `convert` and `resize` store their results in a directory, for instance, and both can do a dry run. So that you don't have to repeat them, describe them once in the constructor of a class, and let each handler take it as a parameter: - -```php -final readonly class Output -{ - public function __construct( - #[Cli\Option('Directory for the results', valueName: 'dir')] - public ?string $outputDir = null, - #[Cli\Flag('Only show what would be done', alias: '-n')] - public bool $dryRun = false, - ) { - } -} - -final class ImageTools -{ - #[Cli\Command('Converts images to another format')] - public function convert( - #[Cli\Argument('Images to convert', repeatable: true)] - array $files, - Output $output, - ): int - { - // stores the results in $output->outputDir - return 0; - } -} -``` - -`convert` now accepts `--output-dir` and `--dry-run`, and so does every other command whose handler takes `Output`. When the command runs, the library creates the object from the values of these options and passes it to the handler. In the help, the options of the object show up together, at the place of the parameter. - -The constructor follows the same rules as a handler. A handler can also take several such objects, say one with what all commands share and another with what only some of them do. This works only for commands described by attributes. - - -Dispatching Without Handlers ----------------------------- - -Handlers aren't mandatory. In a small script you can find out the selected command and branch yourself, best with `match`: - -```php -$args = $parser->parse($program); - -exit(match ($args->command) { - $convert => convertImages($args->get('files'), $args->get('--format')), - $resize => resizeImages($args->get('files'), $args->get('--width')), - $program => showUsage(), -}); -``` - -Since `match` has no `default` arm, it throws `UnhandledMatchError` if you forget a command. The help and errors, however, are up to you in this case, see [Help |#Help] and [Errors |#Errors]. - - -Running the Program -=================== - -Parsing the line, answering `--help` and `--version`, reporting a mistake and running the handler is done the same way in every tool. The whole part of our tool that runs it looks like this: - -```php -use Nette\CommandLine\HelpRenderer; -use Nette\CommandLine\UsageException; - -$program->addFlag('--help', 'Print this help', alias: '-h', standalone: true); -$program->addFlag('--version', 'Print the version', standalone: true); - -$parser = new Parser; -$renderer = new HelpRenderer; - -try { - $args = $parser->parse($program); - if ($args->get('--help')) { - $renderer->render($args->command); - exit(0); - } elseif ($args->get('--version')) { - echo "Images 1.0\n"; - exit(0); - } - exit($args->run()); - -} catch (UsageException $e) { - $renderer->renderError($e); - exit(2); -} -``` - -Let's walk through it. The flags `--help` and `--version` are standalone, so they work even when the user hasn't given the required arguments, more in the [section on the help and version |#help-and-version]. `render($args->command)` prints the help of the command the user selected, so `images.php convert --help` shows the help of `convert`. Finally `run()` runs the handler of the selected command and returns its exit code. - -When the user gets the line wrong, the parser throws `ParseException`, a kind of `UsageException`. `renderError()` prints the message with the help of the command the user got wrong below it, to the error output. The script then ends with code 2. That's the convention for a wrong command line, and it sets it apart from code 1, with which a tool reports that it found something or that something failed. A `UsageException` thrown by a handler is handled the same way, see [Errors |#Errors]. Other exceptions from the handler pass through the `catch` block. - -The option `--no-color`, which turns colors off, is added in a similar way; you'll find how on the page [Terminal Output |console#the-no-color-option]. - -This part works with all handlers, whether described by attributes or passed as `handler:`. A program without commands but with a handler can be created without attributes too, `new Command('convert', handler: ...)`. - - -Help -==== - -The help is built by [api:Nette\CommandLine\HelpRenderer] from the descriptions you gave. That's why it always matches what the script really accepts. It draws it for the program or for any command, aligns the descriptions and wraps them to the width of the terminal, and in a terminal it colors them: - -```php -use Nette\CommandLine\HelpRenderer; - -$renderer = new HelpRenderer; -$renderer->render($command); -``` - -For the `convert` script with the options from the first part it looks like this: - -``` -Usage: convert [options] <files>... - -Converts images to another format - -Arguments: - <files>... Images to convert - -Options: - -f, --format <jpg|png|webp> Target format (default: webp) - -q, --quality <value> Quality from 1 to 100 - --backup[=dir] Keep the originals, optionally in the given - directory - --exclude <mask>... Skip files matching the mask - --[no-]metadata Keep EXIF metadata - -n, --dry-run Only show what would be done -``` - -Notice how the help shows the different kinds: an optional value as `--backup[=dir]`, repeated values with three dots, a flag with an opposite form as `--[no-]metadata`, and the default value after the description. - -At the top is the usage line. It is generated by itself, or you replace it with your own via `usage:` in `new Command()` or `addCommand()`. The description of the command follows below it. The help of a command shows its arguments and options, and under the heading `Global options:` the options of the program that apply in the command as well. - -A few settings shape only the help: - -- `hidden: true` hides a parameter from the help, while it still works, -- `valueName: 'dir'` names the value, as in `--backup[=dir]`; for an option with `enum` it replaces the list of values, which comes in handy when the list is long, -- `defaultDescription: 'current directory'` describes the default value in words; an empty string leaves it out, so a password or a token given as the default never shows up in the help. - -Without `defaultDescription`, the help shows the default value as `(default: webp)`, as long as there is something to show. It leaves out a boolean, an array and an empty string. - -Headings and texts of your own are added with `addSection()` and `addText()`, in the order you call them. A text is printed exactly as you wrote it, with no wrapping and no colors. When you add no heading, the help puts `Options:`, `Arguments:` and `Commands:` in by itself. Write code in the descriptions in backticks, as in Markdown. In a terminal it is drawn in a color of its own, in plain text the backticks stay. - -Instead of printing the help, you can get it as a string with `renderToString()`. Without a console it is plain text with no colors, which suits a file or a test. You can set the width yourself: - -```php -$renderer = new HelpRenderer(width: 100); -$text = $renderer->renderToString($command); -``` - - -`--help` and `--version` ------------------------- - -One thing about both flags deserves attention: when the script has a required argument, `convert.php --help` would end with an error that the argument is missing. That's why you mark them as standalone: - -```php -$command->addFlag('--help', 'Print this help', alias: '-h', standalone: true); -$command->addFlag('--version', 'Print the version', standalone: true); -``` - -When the user uses a standalone flag, the parser checks and requires nothing. It returns that flag as `true`, the other flags as they were given, and everything else with its default value. So you print the help before the script starts reading its configuration or checking files. That's why a setting that should work together with `--help` is best made a flag. For example, `--help --no-color` prints the help without colors. - -With commands, the command is selected first, so `images.php convert --help` gives you exactly `convert` in `$args->command`. The line still has to make sense, though: the parser refuses an unknown option as always. - - -Errors -====== - -When the user gets the line wrong, the parser throws [api:Nette\CommandLine\ParseException]. You catch it and pass it to `renderError()`, as a shortened form of the [part that runs the program |#Running the Program] shows: - -```php -try { - $args = $parser->parse($program); - exit($args->run()); - -} catch (UsageException $e) { - $renderer->renderError($e); - exit(2); -} -``` - -`renderError()` prints the message with the help of the command the user got wrong below it, to the error output. So the user sees right away both what's wrong and how to write it correctly: - -``` -Error: Unknown option `--fromat`. Did you mean `--format`? - -Usage: images convert [options] <files>... -... -``` - -`ParseException` is a kind of [api:Nette\CommandLine\UsageException]. You can throw that one from a handler yourself when you run into a mistake the description of the options can't express, such as two options that can't be used together. Give it the command whose help should be printed, which the handler gets when it has a parameter typed `Command`: - -```php -public function convert(Command $command, bool $stdin, array $files): int -{ - if ($stdin && $files) { - throw new UsageException('Files cannot be combined with `--stdin`.', $command); - } - // ... -} -``` - -Write code in the messages in backticks, just like in descriptions. In a terminal `renderError()` draws it in color and without the backticks, in a log the backticks stay. If you want to print a message your own way, the `formatText()` method formats it the same way. - -Common messages, as a terminal with colors shows them: - -| Option `--format` requires a value. | option without its required value -| Option `--dry-run` does not accept a value. | a value given to a flag -| Unknown option `--fromat`. Did you mean `--format`? | unknown option -| Missing required argument `<files>`. | a required argument is missing -| Unexpected arguments `b`, `c`. | extra arguments -| Option `--format`: expects `jpg`, `png` or `webp`, `gif` given. | value not among the allowed ones -| Unknown command `conver`. Did you mean `convert`? | unknown command -| Missing command. | a command is required but missing -| Option `--width` belongs to command `resize`. | option of another command - -.[note] -The messages are in English. If you want to word them differently or translate them, `ParseException` has everything you need: `reason` says what was wrong, as a case of the `ParseFailure` enum (such as `ParseFailure::UnknownOption`), `parameter` is the parameter the error concerns, and `token` is the word of the command line it's about, exactly as the user wrote it, or `null` when something is missing. Since `token` comes from the user, strip control characters from it before printing it. - -Mistakes in your definition, such as an option name without a dash, a name used twice or a required argument after an optional one, are not mistakes of the user. They throw `InvalidArgumentException`, which the `catch` block doesn't catch, so it shows up like any other mistake in the code. - - -Testing -======= - -You test the script without running it from the command line. You pass the parser the individual words of the command line without the script name, and check what it read from them. The examples use [Nette Tester |tester:], but any testing framework will do: - -```php -$args = $parser->parse($program, ['convert', 'photo.jpg', '-f', 'png']); -Assert::same('png', $args->get('--format')); -Assert::same(0, $args->run()); -``` - -The parser reports a wrong command line with `ParseException`, so you can test what the user will see as well: - -```php -Assert::exception( - fn() => $parser->parse($program, ['convert', '--format', 'gif', 'photo.jpg']), - ParseException::class, - 'Option `--format`: expects `jpg`, `png` or `webp`, `gif` given.', -); -``` - -A single command is tested by passing it to the parser together with the whole line. The parser checks that the line really leads to that command: - -```php -$args = $parser->parse($convert, ['convert', 'photo.jpg']); -``` - -The help deserves a test of its own. It is in fact a description of your tool's interface, so a test comparing it with a stored text catches every unwanted change, such as a renamed option. A fixed width makes sure the result doesn't depend on the terminal the test runs in: - -```php -$renderer = new HelpRenderer(width: 80); -Assert::matchFile(__DIR__ . '/help.txt', $renderer->renderToString($program)); -``` - -You create the file `help.txt` the first time by saving into it what `renderToString()` returns, and checking that it is right. - - -{{sitename: Nette Documentation}} -{{maintitle: PHP Command-Line Arguments with Nette: Options, Commands}} -{{description: Parse command-line arguments in PHP: flags, options and arguments with checked values, commands and subcommands, generated help and clear error messages.}} diff --git a/command-line/en/console.texy b/command-line/en/console.texy deleted file mode 100644 index 926c59ffe3..0000000000 --- a/command-line/en/console.texy +++ /dev/null @@ -1,196 +0,0 @@ -Terminal Output -*************** - -.[perex] -Colored text, clickable links, a progress line and questions for the user. The `Console` class finds out by itself where the output goes and behaves accordingly: in a terminal the output is colorful and alive, in a file, a pipe or CI it stays clean text. You don't have to write a single condition for it. - - -Colorful Output -=============== - -A [api:Nette\CommandLine\Console] object writes to one output, the screen by default, and can color the text: - -```php -use Nette\CommandLine\Console; - -$console = new Console; -$console->writeLine('3 images converted', 'green'); -$console->writeLine('logo.png skipped, it is already WebP', 'yellow'); -$console->writeLine($console->color('gray', 'Results in ') . $outputDir); // a line of several colors -``` - -You usually create the console once at the start of the script and pass it to the classes that print, for example in the constructor, as the [overview |@home#Terminal Output] shows. - -Colors are added only by methods, never by markup in the text. Some libraries look for markup like `<info>` in the text, so XML, HTML or user input has to be escaped first or it breaks. Here everything is printed exactly as it is. - -Whether the text comes out in color doesn't depend on your code but on where the output goes. In a terminal it's in color. When you redirect the output to a file, pipe it to another program or the script runs in CI, the same code prints plain text, because `color()` and `link()` then add nothing. Just watch out for text from elsewhere that carries its own colors, such as the output of another program. Remove them with `Ansi::strip()` if needed. - - -Two Outputs ------------ - -Every program has two outputs: the standard one (`STDOUT`) for results, and the error one (`STDERR`) for messages and errors. The user can redirect each of them somewhere else, for example the results into a file while the errors stay on the screen. That's why each output gets its own console, which finds out by itself whether its output leads to a terminal: - -```php -$out = new Console(STDOUT); -$err = new Console(STDERR); -``` - -Without a parameter, the console writes to `STDOUT`. When the program reading your output stops early, like `head` in `php images.php list | head -1`, the rest of the output is quietly dropped instead of raising an error on every write. - - -Colors ------- - -You give a color as `'foreground'` or `'foreground/background'`, such as `'white/blue'`. The basic names `black`, `gray`, `silver`, `white`, `navy`, `blue`, `green`, `lime`, `teal`, `aqua`, `maroon`, `red`, `purple`, `fuchsia`, `olive` and `yellow` are drawn by the terminal according to its color theme. A color that should look the same everywhere is written in hex, `'#87D7FF'` or the short `'#8DF'`, or as a number 0-255 of the extended palette, such as `'117'`. - -An unknown color throws an exception even when colors are off, so you catch a typo right away. The color `null` means no color, which comes in handy in conditions: `$console->color($ok ? 'green' : null, $text)`. - -Not every terminal can show every color. The console finds out by itself and draws a color as faithfully as possible: exactly in a modern terminal, as the nearest color it can in an older one. How many colors the terminal handles is expressed by [api:Nette\CommandLine\ColorDepth]: `TrueColor` (any color), `Ansi256` (a palette of 256 colors), `Ansi16` (16 basic colors) and `None` (no colors). - -You can influence the behavior with environment variables that other tools know too. [NO_COLOR |https://no-color.org] turns colors off. [FORCE_COLOR |https://force-color.org] turns them on even when the output goes to a file or a pipe, and its value `2` or `3` asks for at least `Ansi256` or `TrueColor`. `COLORTERM` set to `truecolor` or `24bit` says the terminal can show any color. - - -The `--no-color` Option ------------------------ - -Users often want to turn colors off right on the command line. Give the program a flag `--no-color`, and after parsing the line turn colors off for your consoles with the `setColorDepth()` method: - -```php -use Nette\CommandLine\ColorDepth; - -$out = new Console(STDOUT); -$err = new Console(STDERR); -$tools = new ImageTools($out); - -$program = new Command('images'); -$program->addFlag('--no-color', 'Print plain output without colors'); -$program->addCommandsFrom($tools); - -$parser = new Parser; -$args = $parser->parse($program); -if ($args->get('--no-color')) { - $out->setColorDepth(ColorDepth::None); - $err->setColorDepth(ColorDepth::None); -} -``` - -So that the option applies to the help and error messages too, print them through renderers with these consoles: `new HelpRenderer($out)` for the help and `new HelpRenderer($err)` for errors. Only a mistake in the command line itself is reported before the parser reads the flag, so it stays in color. The `NO_COLOR` environment variable turns colors off absolutely everywhere. - -You can also create a console without colors right away, `new Console(STDOUT, colorDepth: ColorDepth::None)`. - -You can also ask the console how things stand: - -```php -$console->hasColors(); // whether it prints in color -$console->getColorDepth(); // how many colors it uses, such as ColorDepth::Ansi256 -$console->isTerminal(); // whether the output leads to a terminal where someone is watching -$console->getWidth(); // width of the terminal in columns, 80 outside a terminal -``` - -`hasColors()` and `isTerminal()` answer two different questions. Use the first to decide whether to color, the second to decide whether to show things that only make sense to a person at the screen, such as a progress line or a question. A user can turn colors off and still sit at a terminal. - -`getWidth()` asks the terminal only once and remembers the result, so only a new console notices that the window was resized. The `COLUMNS` environment variable takes precedence, which comes in handy when you want to see how the output looks in a narrow terminal. - - -Clickable Links -=============== - -A link is composed just like a color. In a terminal, `link()` turns the text into a link you can click. In a file, a pipe or without colors it prints the text followed by the address in parentheses, so the address doesn't disappear from the log: - -```php -$console->writeLine('See ' . $console->link('https://nette.org')); -$console->writeLine($console->link('https://nette.org', 'the manual')); // outside a terminal 'the manual (https://nette.org)' -``` - -`link()` refuses an address with control characters, because they could end the link early and send the terminal commands of their own. Accented letters and other characters beyond ASCII are fine. - - -Progress and Status Lines -========================= - -During longer work it helps to show what the script is doing and how much is done. That's what `setStatus()` is for. It draws a line at the end of the output, the next printout erases it and is printed above it, and the next `setStatus()` draws the line again. The user thus sees the results piling up and below them one line that keeps changing: - -``` -photo.jpg converted -logo.png converted -3/10 holiday.jpg -``` - -In code it looks like this: - -```php -foreach ($files as $i => $file) { - $console->setStatus(sprintf('%d/%d %s', $i + 1, count($files), $file)); - // ... converts the file - $console->writeLine("$file converted"); // erases the status line and prints above it -} - -$console->clearStatus(); -``` - -The status line is only drawn in the terminal, it isn't written to the output. When the output doesn't lead to a terminal, `setStatus()` does nothing at all, so a log file holds only the real lines. There can be several status lines at once, each newline character starts another one. A line that is too long is cut to the width of the terminal, because a wrapped one couldn't be redrawn. - -While the status line is visible, the cursor is hidden. It shows up again when you clear the status, when the console is released, and also at the end of the script, even after `exit()`, an uncaught exception or a fatal error. Only Ctrl+C ends PHP immediately, so your application has to handle that one itself. - -A status line is erased only by the console that drew it. If you have one console for results and another for errors, draw the status on the one the errors go to. Otherwise a warning printed during the work would land on top of the status line. - - -Measuring Text -============== - -When you want to print aligned columns, `strlen()` won't help you. Colors are stored in the text as invisible escape sequences that take no space, and a Chinese character or an emoji takes two columns on the screen. The [api:Nette\CommandLine\Ansi] class measures and cuts text the way the terminal shows it: - -```php -use Nette\CommandLine\Ansi; - -Ansi::measure($console->color('red', 'error')); // 5, the escape sequences don't count -Ansi::pad($console->color('red', 'error'), 10); // pads to 10 columns, not to 10 bytes -Ansi::truncate($path, 30, keepEnd: true); // cuts the front so the file name stays -Ansi::strip($text); // the text without escape sequences -``` - -Widths follow the Unicode tables, so emoji and flags are measured correctly too. Control characters, such as a tab, take no column. `strip()` removes only escape sequences and leaves other control characters in the text, so it won't make untrusted text safe to print. - - -Asking the User -=============== - -Sometimes a script needs a decision only a person can make. Overwrite an existing file? Skip an image that can't be read? [api:Nette\CommandLine\Prompt] asks the question and waits until the user answers with one of the choices you allow: - -```php -use Nette\CommandLine\Prompt; - -$prompt = new Prompt($console); -if ($prompt->isInteractive()) { - $answer = $prompt->choose('Overwrite photo.webp?', ['y', 'n', 'a', 'q'], default: 'n'); -} -``` - -The user sees `Overwrite photo.webp? [y/N/a/q]`, and the default choice is the one in capitals. Which choices you offer is up to you, here they could be yes, no, yes to all and quit. Until the user answers with one of them, the prompt asks again. Letter case doesn't matter, and `choose()` returns the choice exactly as you declared it. Enter on an empty line picks the default, and when there is none, the prompt asks again. - -When each choice deserves a few words, pass them as an array with descriptions, and the prompt prints a menu above the question: - -```php -$answer = $prompt->choose('What now?', [ - 's' => 'Skip this image', - 'r' => 'Retry', - 'c' => 'Cancel the conversion', -], default: 'r'); -``` - -``` - [s] Skip this image - [r] Retry - [c] Cancel the conversion -What now? [s/R/c] -``` - -When the input ends, for example because the user pressed Ctrl+D, `choose()` returns `null`, never the default. A closed input thus can't quietly answer yes. What that means for the script is your decision, such as `?? 'q'`. - -The prompt asks only when both the input and the console lead to a terminal, so it never takes an answer from a pipe nobody is sitting at. Otherwise `choose()` throws `LogicException` before it prints or reads anything. So first ask `isInteractive()` and decide what the script does without a person, for example stop with a hint to use a command-line option such as `--overwrite`. Give the prompt the console that draws the status line: the question then erases it, and the next status is drawn on a clean line after the answer. - - -{{sitename: Nette Documentation}} -{{maintitle: Colored Terminal Output in PHP with Nette: Links, Progress}} -{{description: Colored terminal output in PHP that switches off in a pipe and in CI, clickable links, a progress line and questions for the user.}} diff --git a/command-line/files/help.webp b/command-line/files/help.webp deleted file mode 100644 index cbeb050bc5..0000000000 Binary files a/command-line/files/help.webp and /dev/null differ diff --git a/command-line/meta.json b/command-line/meta.json deleted file mode 100644 index bfd7aa5c05..0000000000 --- a/command-line/meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "2.x", - "repo": "nette/command-line", - "composer": "nette/command-line" -} diff --git a/component-model/cs/@home.texy b/component-model/cs/@home.texy deleted file mode 100644 index fac3687867..0000000000 --- a/component-model/cs/@home.texy +++ /dev/null @@ -1,128 +0,0 @@ -Komponentový model -****************** - -.[perex] -Důležitým pojmem v Nette je komponenta. Do stránek vkládáme [vizuální interaktivní komponenty |application:components], komponentami jsou i formuláře nebo všechny jejich prvky. Základní dvě třídy, od kterých všechny tyto komponenty dědí, jsou součástí balíčku `nette/component-model` a mají za úkol vytvářet stromovou hierarchii komponent. - - -Component -========= -[api:Nette\ComponentModel\Component] je společným předkem všech komponent. Obsahuje metody `getName()` vracející název komponenty a metodu `getParent()` vracející jejího rodiče. Ani jedno nenastavujete sami: obojí nastaví kontejner ve chvíli, kdy do něj komponentu přidáte. (Používá k tomu metodu `setParent()`, která je interní, takže ji ze svého kódu nevolejte.) - - -lookup(?string $type, bool $throw=true): ?IComponent .[method] --------------------------------------------------------------- -Vyhledá v hierarchii směrem nahoru objekt požadované třídy nebo rozhraní. Například `$component->lookup(Nette\Application\UI\Presenter::class)` vrací presenter, pokud je k němu, i přes několik úrovní, komponenta připojena. Pokud žádný odpovídající objekt neexistuje, vyhodí výjimku; při předání `false` jako druhého argumentu vrátí místo toho `null`. Když jako `$type` předáte `null`, metoda hledá nejvyšší komponentu ve stromu, tj. kořen bez rodiče. - - -lookupPath(?string $type=null, bool $throw=true): ?string .[method] -------------------------------------------------------------------- -Vrací tzv. cestu, což je řetězec vzniklý spojením jmen všech komponent na cestě mezi aktuální a hledanou komponentou. Takže např. `$component->lookupPath(Nette\Application\UI\Presenter::class)` vrací jedinečný identifikátor komponenty vůči presenteru. Když je `$type` `null` (nebo vynechán), cesta se měří až ke kořeni stromu. - - -Container -========= -[api:Nette\ComponentModel\Container] je rodičovská komponenta, tj. komponenta obsahující potomky a tvořící tak stromovou strukturu. Disponuje metodami pro snadné přidávání, získávání a odstraňování objektů. Je předkem například formuláře či tříd `Control` a `Presenter`. Potomci využívající trait `ArrayAccess` (například `Control` a `Presenter`) navíc umožňují přístup k potomkům zápisem pole, např. `$container['child']`. - - -addComponent(IComponent $component, ?string $name, ?string $insertBefore=null): static .[method] ------------------------------------------------------------------------------------------------- -Přidá komponentu do kontejneru jako potomka. Je-li `$name` roven `null`, použije se vlastní název komponenty. Volitelným `$insertBefore` - názvem existujícího potomka - vložíte novou komponentu těsně před něj; jinak se přidá na konec. Metoda vrací kontejner, takže lze volání řetězit. - -Název smí obsahovat jen písmena, číslice a podtržítka, protože pomlčka spojuje názvy do cesty (viz [getComponent() |#getComponent]); jiný název vyhodí `Nette\InvalidArgumentException`. Pokud už kontejner má potomka toho jména nebo komponenta už má rodiče, metoda vyhodí `Nette\InvalidStateException`. - - -removeComponent(IComponent $component): void .[method] ------------------------------------------------------- -Odebere potomka z kontejneru. Pokud komponenta jeho potomkem není, vyhodí `Nette\InvalidArgumentException`. - -Komponenta zůstává v kontejneru, dokud neproběhnou její callbacky `$detached` (viz [monitorování předků |#Monitorování předků]), takže ji tyto callbacky pořád vidí na jejím místě. .{data-version:4.0.2} - - -getComponent(string $name, bool $throw=true): ?IComponent .[method] -------------------------------------------------------------------- -Vrací komponentu. Při pokusu o získání nedefinovaného potomka je zavolána továrna `createComponent($name)`. Metoda `createComponent($name)` zavolá v aktuální komponentě metodu `createComponent<název komponenty>` a jako parametr jí předá název komponenty. Vytvořená komponenta je poté přidána do aktuální komponenty jako její potomek. Těmto metodám říkáme továrny na komponenty a mohou je implementovat potomci třídy `Container`. - -Název může být i cesta k hlouběji vnořenému potomkovi, jména se v ní spojují pomlčkou: `$form->getComponent('address-street')` vrátí komponentu `street` uvnitř kontejneru `address`. Pokud taková komponenta neexistuje, metoda vyhodí `Nette\InvalidArgumentException`, a když najde podobný název, rovnou ho nabídne, například `Component with name 'adress' does not exist, did you mean 'address'?`. Když jako druhý argument předáte `false`, dostanete místo toho `null`. - - -getComponents(): IComponent[] .[method] ---------------------------------------- -Vrací přímé potomky jako pole; klíče obsahují názvy těchto komponent. Pro získání celého podstromu rekurzivně použijte `getComponentTree()`, případně v kombinaci s `array_filter()` pro filtrování podle typu. (Parametry `$deep` a `$filterType` známé ze starších verzí byly ve verzi 4.0 odstraněny.) - - -getComponentTree(): list<IComponent> .[method] ----------------------------------------------- -Získá celou hierarchii komponent včetně všech vnořených podřízených komponent jako indexované pole. Prohledávání jde nejprve do hloubky. - - -Monitorování předků -=================== - -Komponentový model Nette umožňuje velmi dynamickou práci se stromem (komponenty můžeme vyjímat, přesouvat, přidávat), proto by byla chyba se spoléhat na to, že po vytvoření komponenty je hned (v konstruktoru) znám rodič, rodič rodiče atd. Většinou totiž rodič při vytvoření vůbec známý není. - -Jak může komponenta zjistit okamžik, kdy je připojena pod presenter - nebo pod jiného předka daného typu? Sledovat přímého rodiče nestačí, protože spojení může nastat výš ve stromu, například když se připojí rodič rodiče. Přesně k tomu slouží metoda [monitor($type, $attached, $detached)|api:Nette\ComponentModel\Component::monitor()]: komponenta oznámí, že chce být upozorněna, kdykoli se nad ní ve stromu objeví předek třídy nebo rozhraní `$type`, nebo z něj zmizí. Komponenta může monitorovat libovolný počet typů; callback `$attached` se spustí při připojení odpovídajícího předka a dostane ho jako argument, callback `$detached` při jeho odpojení. Monitorování lze opět ukončit metodou `unmonitor($type)`. - -Pokud předek ve stromu už je, když `monitor()` zavoláte, spustí se callback `$attached` hned. Nezáleží tedy na tom, jestli komponenta začne monitorovat před připojením, nebo až po něm. - -Oznámení kopírují strukturu stromu. Při připojování je předek uvědoměn dříve než jeho potomci (odshora dolů), takže rodič může nejprve připravit sdílený stav, případně potomka odebrat ještě předtím, než se spustí jeho vlastní callback. Při odpojování je pořadí opačné - nejdříve jsou uvědoměni potomci. Callback registrovaný pro několik typů se pro tentýž předek zavolá jen jednou a druhá registrace stejného callbacku nemá žádný účinek. Důvody tohoto chování popisuje [článek na blogu k verzi 4.0 |https://blog.nette.org/cs/component-model-4-0]. - -Pro lepší pochopení příklad: třída `UploadControl`, reprezentující formulářový prvek pro upload souborů v Nette Forms, musí formuláři nastavit atribut `enctype` na hodnotu `multipart/form-data`. V době vytvoření objektu ale k žádnému formuláři připojena být nemusí. Ve kterém okamžiku tedy formulář modifikovat? Řešení je jednoduché - v konstruktoru se požádá o monitoring: - -```php -class UploadControl extends Nette\Forms\Controls\BaseControl -{ - public function __construct($label) - { - $this->monitor(Nette\Forms\Form::class, function ($form): void { - $form->setHtmlAttribute('enctype', 'multipart/form-data'); - }); - // ... - } - - // ... -} -``` - -a jakmile je formulář k dispozici, zavolá se callback. - - -Odmítnutí připojení .{data-version:4.0.2} ------------------------------------------ -Callback může změnu i zastavit. Když `$attached` vyhodí výjimku, komponenta se vůbec nepřipojí: změna se vrátí, callbacky, které už proběhly, dostanou svůj protějšek `$detached`, a výjimka pokračuje ke kódu, který komponentu přidával. Takhle Nette Forms nedovolí vložit formulář do jiného formuláře. Tady zase průvodce odmítá žít uvnitř formuláře: - -```php -class Wizard extends Nette\ComponentModel\Container -{ - public function __construct() - { - $this->monitor(Nette\Forms\Form::class, function (): void { - throw new Nette\InvalidStateException('A wizard cannot be placed inside a form.'); - }); - } -} - -$form->addComponent(new Wizard, 'wizard'); // vyhodí výjimku, $form zůstane, jak byl -``` - -Stejně to funguje i opačně: callback `$detached`, který vyhodí výjimku, nechá komponentu na jejím místě. - - -Změny stromu v callbacku ------------------------- -Callbacky smějí strom samy měnit: odebrat vlastní komponentu, sourozence nebo předka. Oznámení s tím počítají, takže komponenta, kterou ze stromu odebral některý dřívější callback, už oznámení nedostane. - -Callback `$detached` běží ještě předtím, než se komponenta skutečně odpojí, takže `lookup()` v něm pořád najde předka, od kterého se odpojuje. .{data-version:4.0.2} - - -Klonování a serializace -======================= -Komponentu lze zkopírovat pomocí `clone`. Kopie dostane kopie všech svých potomků, ale žádného rodiče: začíná mimo strom, a proto její monitory dostanou `$detached` od předků originálu. Uvnitř kopie `lookup()` vidí vlastní strom kopie. - -Komponenty nelze serializovat; `serialize()` vyhodí `Nette\NotImplementedException`. - - -Pokud aktualizujete balíček na novější verzi, podívejte se na stránku [upgrade|upgrading]. - -{{maintitle: Komponentový model Nette: třídy Component a Container}} -{{description: Formuláře, jejich prvky i vizuální komponenty v Nette dědí od tříd Component a Container. Jak funguje pojmenování, monitorování předků a klonování.}} diff --git a/component-model/cs/@left-menu.texy b/component-model/cs/@left-menu.texy deleted file mode 100644 index a64de8aa8c..0000000000 --- a/component-model/cs/@left-menu.texy +++ /dev/null @@ -1,13 +0,0 @@ -Komponentový model -****************** -- [Úvod |@home] -- [Upgrade |upgrading] - - -Další četba -*********** -- [Dokumentace Nette |nette:] -- [Aplikace v Nette |application:how-it-works] -- [Utilities |utils:] -- [Návody a postupy |best-practices:] -- [Řešení problémů |nette:troubleshooting] diff --git a/component-model/cs/@meta.texy b/component-model/cs/@meta.texy deleted file mode 100644 index 462d9add80..0000000000 --- a/component-model/cs/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokumentace}} diff --git a/component-model/cs/upgrading.texy b/component-model/cs/upgrading.texy deleted file mode 100644 index d67026371e..0000000000 --- a/component-model/cs/upgrading.texy +++ /dev/null @@ -1,23 +0,0 @@ -Upgrade -******* - - -Upgrade na verzi 4.0 -==================== - -Většina změn se týká jen autorů vlastních komponent; běžného kódu aplikace se nedotknou. Celý příběh popisuje [článek na blogu k vydání |https://blog.nette.org/cs/component-model-4-0]. - -- Metoda `getComponents()` nyní vrací `array` místo `iterable` a její historické argumenty `$deep` a `$filterType` vyhodí `Nette\DeprecatedException`. Ze signatury zmizely už ve verzi 3.1, ale za běhu dosud fungovaly. Pro celý podstrom použijte `getComponentTree()`, případně v kombinaci s `array_filter()` pro filtrování podle typu. -- Oznámení o připojení se nově doručují odshora dolů: callback `$attached` rodiče se spustí dříve než u jeho potomků, což rodiči umožní připravit svůj stav, případně potomka odebrat ještě předtím, než se zpracuje. Pořadí při odpojování zůstává nezměněné (nejdříve potomci). -- Přepisovatelné metody `attached()` a `detached()` byly odstraněny; callbacky registrujte přes `monitor($type, $attached, $detached)`. -- Třída `Component` už nepoužívá trait `Nette\SmartObject`, takže ze základní třídy zmizel magický přístup k vlastnostem (`Control` a `BaseControl` si trait přidávají samy). -- Zastaralá konstanta `NAME_SEPARATOR` byla odstraněna; použijte `NameSeparator`, která je k dispozici od verze 3.0.3. - - -Upgrade na verzi 3.0 -==================== - -Konstruktor `Nette\ComponentModel\Component` nebyl roky používán a byl odstraněn ve verzi 3.0. Je to BC break: pokud voláte rodičovský konstruktor v komponentě nebo presenteru, musíte volání odstranit. - -{{maintitle: Upgrade Nette Component Model: co se změnilo}} -{{description: Co se změnilo v Nette Component Model 4.0 a 3.0 a jak upravit kód při přechodu na novější verzi.}} diff --git a/component-model/de/@home.texy b/component-model/de/@home.texy deleted file mode 100644 index 0e6ab23b3e..0000000000 --- a/component-model/de/@home.texy +++ /dev/null @@ -1,82 +0,0 @@ -Komponentenmodell -***************** - -.[perex] -Ein wichtiges Konzept in Nette ist die Komponente. In die Seiten fügen wir [visuelle interaktive Komponenten |application:components] ein; auch Formulare und alle ihre Elemente sind Komponenten. Die beiden grundlegenden Klassen, von denen alle diese Komponenten erben, sind Teil des Pakets `nette/component-model` und für das Erzeugen der Baumhierarchie der Komponenten zuständig. - - -Component -========= -[api:Nette\ComponentModel\Component] ist der gemeinsame Vorfahre aller Komponenten. Sie enthält die Methode `getName()`, die den Namen der Komponente zurückgibt, und die Methode `getParent()`, die ihren Elternteil zurückgibt. Beides lässt sich mit der Methode `setParent()` setzen - der erste Parameter ist der Elternteil, der zweite der Name der Komponente. - - -lookup(?string $type, bool $throw=true): ?Component .[method] -------------------------------------------------------------- -Sucht in der Hierarchie nach oben nach einem Objekt der gewünschten Klasse oder des gewünschten Interface. So gibt zum Beispiel `$component->lookup(Nette\Application\UI\Presenter::class)` den Presenter zurück, wenn die Komponente mit ihm verbunden ist, auch über mehrere Ebenen hinweg. Wird kein passendes Objekt gefunden, wirft die Methode eine Exception; übergeben Sie als zweites Argument `false`, damit sie stattdessen `null` zurückgibt. Übergeben Sie als `$type` den Wert `null`, sucht die Methode die oberste Komponente im Baum, also die Wurzel ohne Elternteil. - - -lookupPath(?string $type=null, bool $throw=true): ?string .[method] -------------------------------------------------------------------- -Gibt den sogenannten Pfad zurück, also einen String, der durch Aneinanderreihen der Namen aller Komponenten auf dem Weg zwischen der aktuellen und der gesuchten Komponente entsteht. So gibt zum Beispiel `$component->lookupPath(Nette\Application\UI\Presenter::class)` den eindeutigen Bezeichner der Komponente relativ zum Presenter zurück. Ist `$type` gleich `null` (oder weggelassen), wird der Pfad bis zur Wurzel des Baums gemessen. - - -Container -========= -[api:Nette\ComponentModel\Container] ist die übergeordnete Komponente, also die Komponente, die Kinder enthält und damit die Baumstruktur bildet. Sie hat Methoden zum bequemen Hinzufügen, Abrufen und Entfernen von Objekten. Sie ist der Vorfahre etwa des Formulars oder der Klassen `Control` und `Presenter`. Nachfahren, die den Trait `ArrayAccess` verwenden (etwa `Control` und `Presenter`), erlauben den Zugriff auf Kinder auch über die Array-Schreibweise, also `$container['child']`. - - -addComponent(Component $component, ?string $name, ?string $insertBefore=null): static .[method] ------------------------------------------------------------------------------------------------ -Fügt dem Container eine Komponente als Kind hinzu. Ist `$name` gleich `null`, wird der eigene Name der Komponente verwendet. Mit dem optionalen `$insertBefore` - dem Namen eines vorhandenen Kindes - wird die neue Komponente direkt davor eingefügt, andernfalls wird sie am Ende angehängt. Die Methode gibt den Container selbst zurück, Aufrufe lassen sich also verketten. - - -removeComponent(Component $component): void .[method] ------------------------------------------------------ -Entfernt eine Kindkomponente aus dem Container. - - -getComponent(string $name): ?Component .[method] ------------------------------------------------- -Gibt eine Komponente zurück. Beim Versuch, ein nicht definiertes Kind abzurufen, wird die Factory-Methode `createComponent($name)` aufgerufen. Die Methode `createComponent($name)` ruft in der aktuellen Komponente die Methode `createComponent<Name der Komponente>` auf und übergibt ihr den Namen der Komponente als Parameter. Die erzeugte Komponente wird anschließend der aktuellen Komponente als ihr Kind hinzugefügt. Wir nennen diese Methoden Komponenten-Factorys, und sie lassen sich in von `Container` abgeleiteten Klassen implementieren. - - -getComponents(): IComponent[] .[method] ---------------------------------------- -Gibt die direkten Nachfahren als Array zurück; die Schlüssel enthalten die Namen dieser Komponenten. Um den gesamten Teilbaum rekursiv zu erhalten, verwenden Sie `getComponentTree()`, gegebenenfalls kombiniert mit `array_filter()` zum Filtern nach Typ. (Die aus älteren Versionen bekannten Parameter `$deep` und `$filterType` wurden in Version 4.0 entfernt.) - - -getComponentTree(): list<IComponent> .[method] ----------------------------------------------- -Liefert die gesamte Hierarchie der Komponenten einschließlich aller verschachtelten Kindkomponenten als indexiertes Array. Die Suche verläuft in die Tiefe. - - -Überwachung der Vorfahren -========================= - -Das Komponentenmodell von Nette erlaubt eine sehr dynamische Arbeit mit dem Baum (wir können Komponenten entfernen, verschieben, hinzufügen), es wäre also ein Fehler, sich darauf zu verlassen, dass nach dem Erzeugen einer Komponente sofort (im Konstruktor) der Elternteil, dessen Elternteil usw. bekannt sind. Meist ist der Elternteil beim Erzeugen der Komponente überhaupt nicht bekannt. - -Wie erfährt eine Komponente den Moment, in dem sie unterhalb eines Presenters - oder unterhalb eines beliebigen anderen Vorfahren eines bestimmten Typs - angehängt wird? Den direkten Elternteil zu beobachten genügt nicht, denn die Verbindung kann weiter oben im Baum entstehen, etwa wenn der Elternteil des Elternteils angehängt wird. Genau dafür ist die Methode [monitor($type, $attached, $detached) |api:Nette\ComponentModel\Component::monitor()] da: Eine Komponente erklärt, dass sie benachrichtigt werden will, sobald über ihr im Baum ein Vorfahre der Klasse oder des Interface `$type` auftaucht oder daraus verschwindet. Eine Komponente kann beliebig viele Typen überwachen; das Callback `$attached` wird ausgelöst, wenn ein passender Vorfahre verbunden wird, und erhält diesen Vorfahren als Argument, während `$detached` ausgelöst wird, wenn er getrennt wird. Die Überwachung lässt sich mit `unmonitor($type)` wieder beenden. - -Die Benachrichtigungen folgen der Struktur des Baums. Beim Anhängen wird ein Vorfahre vor seinen Nachfahren benachrichtigt (von oben nach unten), ein Elternteil kann also zuerst gemeinsamen Zustand vorbereiten oder ein Kind sogar entfernen, bevor dessen eigenes Callback läuft. Beim Trennen ist die Reihenfolge umgekehrt - zuerst werden die Nachfahren benachrichtigt. Die Callbacks werden außerdem dedupliziert, dasselbe Callback wird also für dasselbe Objekt nie zweimal aufgerufen. Die Begründung für dieses Verhalten finden Sie im [Blogartikel über die Version 4.0 |https://blog.nette.org/en/nette-component-model-4-0]. - -Zum besseren Verständnis ein Beispiel: Die Klasse `UploadControl`, die in Nette Forms das Formularelement zum Hochladen von Dateien darstellt, muss das Attribut `enctype` des Formulars auf `multipart/form-data` setzen. Zum Zeitpunkt des Erzeugens des Objekts muss sie aber an kein Formular angehängt sein. Wann also soll das Formular geändert werden? Die Lösung ist einfach - im Konstruktor wird die Überwachung angefordert: - -```php -class UploadControl extends Nette\Forms\Controls\BaseControl -{ - public function __construct($label) - { - $this->monitor(Nette\Forms\Form::class, function ($form): void { - $form->setHtmlAttribute('enctype', 'multipart/form-data'); - }); - // ... - } - - // ... -} -``` - -und sobald das Formular verfügbar ist, wird das Callback aufgerufen. - - -Wenn Sie auf eine neuere Version aktualisieren, sehen Sie sich die Seite [Upgrade |upgrading] an. diff --git a/component-model/de/@left-menu.texy b/component-model/de/@left-menu.texy deleted file mode 100644 index d31c9dc429..0000000000 --- a/component-model/de/@left-menu.texy +++ /dev/null @@ -1,13 +0,0 @@ -Komponentenmodell -***************** -- [Übersicht |@home] -- [Upgrade|upgrading] - - -Weiterführende Lektüre -********************** -- [Nette Dokumentation |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Best Practices |best-practices:] -- [Fehlerbehebung |nette:troubleshooting] diff --git a/component-model/de/@meta.texy b/component-model/de/@meta.texy deleted file mode 100644 index b3b806b2ca..0000000000 --- a/component-model/de/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokumentation}} diff --git a/component-model/de/upgrading.texy b/component-model/de/upgrading.texy deleted file mode 100644 index 08cde92699..0000000000 --- a/component-model/de/upgrading.texy +++ /dev/null @@ -1,20 +0,0 @@ -Upgrade -******* - - -Upgrade auf Version 4.0 -======================= - -Die meisten Änderungen betreffen nur Autoren eigener Komponenten; gewöhnlicher Anwendungscode ist nicht betroffen. Die ganze Geschichte finden Sie im [Blogartikel zum Release |https://blog.nette.org/en/nette-component-model-4-0]. - -- Die Methode `getComponents()` gibt nun ein `array` statt eines `iterable` zurück, und ihre alten Argumente `$deep` und `$filterType` werfen jetzt `Nette\DeprecatedException`. Aus der Signatur waren sie bereits in Version 3.1 verschwunden, funktionierten zur Laufzeit aber bis jetzt weiter. Für den gesamten Teilbaum verwenden Sie `getComponentTree()`, gegebenenfalls kombiniert mit `array_filter()` zum Filtern nach Typ. -- Benachrichtigungen über das Anhängen werden nun von oben nach unten zugestellt: Das Callback `$attached` eines Elternteils läuft vor dem seiner Kinder, wodurch der Elternteil seinen Zustand vorbereiten oder ein Kind sogar entfernen kann, bevor es verarbeitet wird. Die Reihenfolge beim Trennen bleibt unverändert (zuerst die Kinder). -- Die überschreibbaren Methoden `attached()` und `detached()` wurden entfernt; registrieren Sie Callbacks stattdessen über `monitor($type, $attached, $detached)`. -- Die Klasse `Component` verwendet den Trait `Nette\SmartObject` nicht mehr, der magische Zugriff auf Properties ist aus der Basisklasse also verschwunden (`Control` und `BaseControl` binden den Trait selbst ein). -- Die veraltete Konstante `NAME_SEPARATOR` wurde entfernt; verwenden Sie `NameSeparator`, verfügbar seit Version 3.0.3. - - -Upgrade auf Version 3.0 -======================= - -Der Konstruktor von `Nette\ComponentModel\Component` wurde seit Jahren nicht verwendet und in Version 3.0 entfernt. Es handelt sich um einen BC Break: Wenn Sie in einer Komponente oder einem Presenter den Konstruktor des Elternteils aufrufen, müssen Sie den Aufruf entfernen. diff --git a/component-model/en/@home.texy b/component-model/en/@home.texy deleted file mode 100644 index 5ecbdd07f5..0000000000 --- a/component-model/en/@home.texy +++ /dev/null @@ -1,128 +0,0 @@ -Component Model -*************** - -.[perex] -An important concept in Nette is the component. We insert [visual interactive components |application:components] into pages; forms and all their elements are also components. The two basic classes from which all these components inherit are part of the `nette/component-model` package and are responsible for creating the component tree hierarchy. - - -Component -========= -[api:Nette\ComponentModel\Component] is the common ancestor of all components. It contains the `getName()` method returning the name of the component and the `getParent()` method returning its parent. You don't set either of them yourself: the container sets both when you add the component to it. (It uses the `setParent()` method for that, which is internal, so don't call it from your code.) - - -lookup(?string $type, bool $throw=true): ?IComponent .[method] --------------------------------------------------------------- -Searches up the hierarchy for an object of the desired class or interface. For example, `$component->lookup(Nette\Application\UI\Presenter::class)` returns the presenter if the component is connected to it, even across several levels. If no matching object is found, it throws an exception; pass `false` as the second argument to return `null` instead. If you pass `null` as the `$type`, the method searches for the topmost component in the tree, i.e., the root with no parent. - - -lookupPath(?string $type=null, bool $throw=true): ?string .[method] -------------------------------------------------------------------- -Returns the so-called path, which is a string formed by concatenating the names of all components on the path between the current component and the component being searched for. So, for example, `$component->lookupPath(Nette\Application\UI\Presenter::class)` returns the unique identifier of the component relative to the presenter. When `$type` is `null` (or omitted), the path is measured up to the root of the tree. - - -Container -========= -[api:Nette\ComponentModel\Container] is the parent component, i.e., the component containing children and thus forming the tree structure. It has methods for easily adding, retrieving, and removing objects. It is the ancestor of, for example, the form or classes `Control` and `Presenter`. Descendants that use the `ArrayAccess` trait (such as `Control` and `Presenter`) also allow accessing children via array notation, e.g. `$container['child']`. - - -addComponent(IComponent $component, ?string $name, ?string $insertBefore=null): static .[method] ------------------------------------------------------------------------------------------------- -Adds a component to the container as a child. If `$name` is `null`, the component's own name is used. Using the optional `$insertBefore` - the name of an existing child - the new component is inserted right before it; otherwise it is appended to the end. The method returns the container itself, so calls can be chained. - -The name may contain only letters, digits and underscores, because the hyphen joins names into a path (see [getComponent() |#getComponent]); any other name throws `Nette\InvalidArgumentException`. If the container already has a child of that name, or the component already has a parent, the method throws `Nette\InvalidStateException`. - - -removeComponent(IComponent $component): void .[method] ------------------------------------------------------- -Removes a child component from the container. If the component is not its child, it throws `Nette\InvalidArgumentException`. - -The component stays in the container until its `$detached` callbacks (see [monitoring ancestors |#Monitoring Ancestors]) have run, so they still see it in its place. .{data-version:4.0.2} - - -getComponent(string $name, bool $throw=true): ?IComponent .[method] -------------------------------------------------------------------- -Returns a component. Attempting to retrieve an undefined child invokes the factory method `createComponent($name)`. The `createComponent($name)` method calls the method `createComponent<component name>` in the current component, passing the component name as a parameter. The created component is then added to the current component as its child. We call these methods component factories, and they can be implemented in classes inherited from `Container`. - -The name can also be a path to a deeper descendant, with the names joined by a hyphen: `$form->getComponent('address-street')` returns the `street` component inside the `address` container. If there is no such component, the method throws `Nette\InvalidArgumentException` and suggests a similar name if it finds one, for example `Component with name 'adress' does not exist, did you mean 'address'?`. Pass `false` as the second argument to get `null` instead. - - -getComponents(): IComponent[] .[method] ---------------------------------------- -Returns direct descendants as an array; the keys contain the names of these components. To retrieve the entire subtree recursively, use `getComponentTree()`, optionally combined with `array_filter()` for type filtering. (The `$deep` and `$filterType` parameters known from older versions were removed in version 4.0.) - - -getComponentTree(): list<IComponent> .[method] ----------------------------------------------- -Gets the entire hierarchy of components, including all nested child components, as an indexed array. The search is depth-first. - - -Monitoring Ancestors -==================== - -The Nette component model allows for very dynamic work with the tree (we can remove, move, add components), so it would be a mistake to rely on the fact that after creating a component, the parent, parent's parent, etc., are known immediately (in the constructor). Usually, the parent is not known at all when the component is created. - -How can a component find out the moment it is attached below a presenter - or below any other ancestor of a given type? Watching the direct parent isn't enough, because the connection may happen higher up the tree, for example when the parent's parent gets attached. That is what the [monitor($type, $attached, $detached) |api:Nette\ComponentModel\Component::monitor()] method is for: a component declares that it wants to be notified whenever an ancestor of class or interface `$type` appears above it in the tree, or disappears from it. A component can monitor any number of types; the `$attached` callback fires when a matching ancestor connects and receives that ancestor as its argument, while `$detached` fires when it disconnects. Monitoring can be stopped again with `unmonitor($type)`. - -If the ancestor is already there when you call `monitor()`, the `$attached` callback runs right away. So it doesn't matter whether the component starts monitoring before or after it is attached. - -Notifications follow the tree structure. When attaching, an ancestor is notified before its descendants (top-down), so a parent can prepare shared state first, or even remove a child before the child's own callback runs. When detaching, the order is reversed - descendants are notified first. A callback registered for several types is called only once for the same ancestor, and registering the same callback a second time has no effect. For the reasoning behind this behavior, see the [blog post about version 4.0 |https://blog.nette.org/en/nette-component-model-4-0]. - -For better understanding, here's an example: The `UploadControl` class, representing the form control for uploading files in Nette Forms, must set the form's `enctype` attribute to `multipart/form-data`. However, at the time the object is created, it might not be attached to any form. So, at what point should the form be modified? The solution is simple - a request for monitoring is made in the constructor: - -```php -class UploadControl extends Nette\Forms\Controls\BaseControl -{ - public function __construct($label) - { - $this->monitor(Nette\Forms\Form::class, function ($form): void { - $form->setHtmlAttribute('enctype', 'multipart/form-data'); - }); - // ... - } - - // ... -} -``` - -and as soon as the form becomes available, the callback is invoked. - - -Refusing to Be Attached .{data-version:4.0.2} ---------------------------------------------- -A callback can also stop the change. When `$attached` throws an exception, the component is not attached at all: the change is rolled back, the callbacks that have already run get their `$detached` counterpart, and the exception goes on to the code that was adding the component. This is how Nette Forms refuse to place a form inside another form. Here a wizard refuses to live inside a form: - -```php -class Wizard extends Nette\ComponentModel\Container -{ - public function __construct() - { - $this->monitor(Nette\Forms\Form::class, function (): void { - throw new Nette\InvalidStateException('A wizard cannot be placed inside a form.'); - }); - } -} - -$form->addComponent(new Wizard, 'wizard'); // throws the exception, $form stays as it was -``` - -The same works the other way round: a `$detached` callback that throws keeps the component where it is. - - -Changing the Tree in a Callback -------------------------------- -Callbacks may change the tree themselves: remove their own component, a sibling, or an ancestor. The notifications take it into account, so a component that an earlier callback has removed from the tree is no longer notified. - -A `$detached` callback runs before the component is actually detached, so `lookup()` inside it still finds the ancestor being left. .{data-version:4.0.2} - - -Cloning and Serialization -========================= -A component can be copied with `clone`. The copy gets copies of all its descendants, but no parent: it starts outside the tree, so its monitors receive `$detached` from the ancestors of the original. Inside the copy, `lookup()` sees the copy's own tree. - -Components cannot be serialized; `serialize()` throws `Nette\NotImplementedException`. - - -If you are upgrading to a newer version, see the [upgrading] page. - -{{maintitle: Nette Component Model: Component and Container Classes}} -{{description: Forms, their controls and visual components in Nette all inherit from the Component and Container classes. How naming, monitoring ancestors and cloning work.}} diff --git a/component-model/en/@left-menu.texy b/component-model/en/@left-menu.texy deleted file mode 100644 index 37693b5d78..0000000000 --- a/component-model/en/@left-menu.texy +++ /dev/null @@ -1,13 +0,0 @@ -Component Model -*************** -- [Overview |@home] -- [Upgrading] - - -Further Reading -*************** -- [Nette Documentation |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Best practices |best-practices:] -- [Troubleshooting |nette:troubleshooting] diff --git a/component-model/en/@meta.texy b/component-model/en/@meta.texy deleted file mode 100644 index 42471908b0..0000000000 --- a/component-model/en/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Documentation}} diff --git a/component-model/en/upgrading.texy b/component-model/en/upgrading.texy deleted file mode 100644 index b9993873e1..0000000000 --- a/component-model/en/upgrading.texy +++ /dev/null @@ -1,23 +0,0 @@ -Upgrading -********* - - -Upgrading to Version 4.0 -======================== - -Most changes affect only authors of custom components; ordinary application code is unaffected. See the [release blog post |https://blog.nette.org/en/nette-component-model-4-0] for the full story. - -- The `getComponents()` method now returns an `array` instead of an `iterable`, and its legacy `$deep` and `$filterType` arguments now throw `Nette\DeprecatedException`. They disappeared from the signature back in version 3.1, but kept working at runtime until now. For the whole subtree use `getComponentTree()`, optionally combined with `array_filter()` for type filtering. -- Attach notifications are now delivered top-down: a parent's `$attached` callback runs before its children's, which lets the parent prepare its state or even remove a child before it is processed. The detachment order is unchanged (children first). -- The overridable `attached()` and `detached()` methods were removed; register callbacks via `monitor($type, $attached, $detached)` instead. -- The `Component` class no longer uses the `Nette\SmartObject` trait, so magic property access is gone from the base class (`Control` and `BaseControl` add the trait themselves). -- The deprecated `NAME_SEPARATOR` constant was removed; use `NameSeparator`, available since version 3.0.3. - - -Upgrading to Version 3.0 -======================== - -The constructor of `Nette\ComponentModel\Component` has not been used for years and was removed in version 3.0. It is a BC break: if you call the parent constructor in a component or presenter, you must remove the call. - -{{maintitle: Upgrading Nette Component Model: What Changed}} -{{description: What changed in Nette Component Model 4.0 and 3.0, and how to update your code when moving to a newer version.}} diff --git a/component-model/es/@home.texy b/component-model/es/@home.texy deleted file mode 100644 index 3320fd550d..0000000000 --- a/component-model/es/@home.texy +++ /dev/null @@ -1,82 +0,0 @@ -Modelo de componentes -********************* - -.[perex] -Un concepto importante en Nette es el componente. En las páginas insertamos [componentes visuales interactivos |application:components]; los formularios y todos sus elementos también son componentes. Las dos clases básicas de las que heredan todos estos componentes forman parte del paquete `nette/component-model` y se encargan de crear la jerarquía del árbol de componentes. - - -Component -========= -[api:Nette\ComponentModel\Component] es el antecesor común de todos los componentes. Contiene el método `getName()`, que devuelve el nombre del componente, y el método `getParent()`, que devuelve su padre. Ambos se pueden establecer con el método `setParent()`: el primer parámetro es el padre y el segundo el nombre del componente. - - -lookup(?string $type, bool $throw=true): ?Component .[method] -------------------------------------------------------------- -Busca hacia arriba en la jerarquía un objeto de la clase o interfaz deseada. Por ejemplo, `$component->lookup(Nette\Application\UI\Presenter::class)` devuelve el presenter si el componente está conectado a él, aunque sea a través de varios niveles. Si no encuentra ningún objeto correspondiente, lanza una excepción; pase `false` como segundo argumento para que devuelva `null` en su lugar. Si pasa `null` como `$type`, el método busca el componente más alto del árbol, es decir, la raíz sin padre. - - -lookupPath(?string $type=null, bool $throw=true): ?string .[method] -------------------------------------------------------------------- -Devuelve la llamada ruta, que es una cadena formada al concatenar los nombres de todos los componentes del camino entre el componente actual y el componente buscado. Así, por ejemplo, `$component->lookupPath(Nette\Application\UI\Presenter::class)` devuelve el identificador único del componente relativo al presenter. Cuando `$type` es `null` (o se omite), la ruta se mide hasta la raíz del árbol. - - -Container -========= -[api:Nette\ComponentModel\Container] es el componente padre, es decir, el componente que contiene hijos y que forma así la estructura de árbol. Tiene métodos para añadir, obtener y eliminar objetos con facilidad. Es el antecesor, por ejemplo, del formulario o de las clases `Control` y `Presenter`. Los descendientes que usan el trait `ArrayAccess` (como `Control` y `Presenter`) permiten además acceder a los hijos con la notación de array, p. ej. `$container['child']`. - - -addComponent(Component $component, ?string $name, ?string $insertBefore=null): static .[method] ------------------------------------------------------------------------------------------------ -Añade un componente al contenedor como hijo. Si `$name` es `null`, se usa el nombre propio del componente. Con el `$insertBefore` opcional, el nombre de un hijo existente, el componente nuevo se inserta justo antes de él; en caso contrario se añade al final. El método devuelve el propio contenedor, así que las llamadas se pueden encadenar. - - -removeComponent(Component $component): void .[method] ------------------------------------------------------ -Elimina un componente hijo del contenedor. - - -getComponent(string $name): ?Component .[method] ------------------------------------------------- -Devuelve un componente. Intentar obtener un hijo no definido invoca el método factory `createComponent($name)`. El método `createComponent($name)` llama al método `createComponent<nombre del componente>` del componente actual y le pasa como parámetro el nombre del componente. El componente creado se añade después al componente actual como hijo suyo. A estos métodos los llamamos factories de componentes y se pueden implementar en las clases que heredan de `Container`. - - -getComponents(): IComponent[] .[method] ---------------------------------------- -Devuelve los descendientes directos como array; las claves contienen los nombres de esos componentes. Para obtener todo el subárbol de forma recursiva, use `getComponentTree()`, combinado opcionalmente con `array_filter()` para filtrar por tipo. (Los parámetros `$deep` y `$filterType` conocidos de las versiones anteriores se eliminaron en la versión 4.0.) - - -getComponentTree(): list<IComponent> .[method] ----------------------------------------------- -Obtiene toda la jerarquía de componentes, incluidos todos los componentes hijos anidados, como array indexado. La búsqueda es en profundidad. - - -Monitorizar los antecesores -=========================== - -El modelo de componentes de Nette permite trabajar con el árbol de forma muy dinámica (podemos eliminar, mover y añadir componentes), así que sería un error confiar en que, tras crear un componente, el padre, el padre del padre, etc. se conocen de inmediato (en el constructor). Normalmente, cuando se crea el componente el padre no se conoce en absoluto. - -¿Cómo puede averiguar un componente el momento en que se adjunta bajo un presenter, o bajo cualquier otro antecesor de un tipo dado? Vigilar al padre directo no basta, porque la conexión puede ocurrir más arriba en el árbol, por ejemplo cuando se adjunta el padre del padre. Para eso sirve el método [monitor($type, $attached, $detached) |api:Nette\ComponentModel\Component::monitor()]: un componente declara que quiere que se le avise siempre que por encima de él aparezca en el árbol un antecesor de la clase o interfaz `$type`, o que desaparezca de él. Un componente puede monitorizar cualquier número de tipos; el callback `$attached` se dispara cuando se conecta un antecesor correspondiente y lo recibe como argumento, mientras que `$detached` se dispara cuando se desconecta. La monitorización se puede detener de nuevo con `unmonitor($type)`. - -Las notificaciones siguen la estructura del árbol. Al adjuntar, se avisa antes al antecesor que a sus descendientes (de arriba abajo), así que un padre puede preparar primero el estado compartido, o incluso eliminar un hijo antes de que se ejecute el callback de ese hijo. Al desconectar, el orden se invierte: se avisa primero a los descendientes. Los callbacks también se deduplican, así que el mismo callback nunca se llama dos veces para el mismo objeto. El razonamiento tras este comportamiento está en la [entrada del blog sobre la versión 4.0 |https://blog.nette.org/en/nette-component-model-4-0]. - -Para entenderlo mejor, aquí tiene un ejemplo: la clase `UploadControl`, que representa el elemento de formulario para subir archivos en Nette Forms, tiene que establecer el atributo `enctype` del formulario a `multipart/form-data`. Pero, en el momento en que se crea el objeto, puede que no esté adjunto a ningún formulario. Entonces, ¿en qué momento hay que modificar el formulario? La solución es sencilla: en el constructor se hace una petición de monitorización: - -```php -class UploadControl extends Nette\Forms\Controls\BaseControl -{ - public function __construct($label) - { - $this->monitor(Nette\Forms\Form::class, function ($form): void { - $form->setHtmlAttribute('enctype', 'multipart/form-data'); - }); - // ... - } - - // ... -} -``` - -y, en cuanto el formulario está disponible, se invoca el callback. - - -Si está actualizando a una versión más reciente, vea la página de [actualización |upgrading]. diff --git a/component-model/es/@left-menu.texy b/component-model/es/@left-menu.texy deleted file mode 100644 index 574ccef237..0000000000 --- a/component-model/es/@left-menu.texy +++ /dev/null @@ -1,13 +0,0 @@ -Modelo de componentes -********************* -- [Introducción |@home] -- [Actualización|upgrading] - - -Lecturas adicionales -******************** -- [Documentación de Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Buenas prácticas |best-practices:] -- [Solución de problemas |nette:troubleshooting] diff --git a/component-model/es/@meta.texy b/component-model/es/@meta.texy deleted file mode 100644 index 3798d9cda4..0000000000 --- a/component-model/es/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentación de Nette}} diff --git a/component-model/es/upgrading.texy b/component-model/es/upgrading.texy deleted file mode 100644 index 8857811e0c..0000000000 --- a/component-model/es/upgrading.texy +++ /dev/null @@ -1,20 +0,0 @@ -Actualización -************* - - -Actualización a la versión 4.0 -============================== - -La mayoría de los cambios afectan solo a los autores de componentes propios; el código corriente de las aplicaciones no se ve afectado. La historia completa está en la [entrada del blog sobre la versión |https://blog.nette.org/en/nette-component-model-4-0]. - -- El método `getComponents()` devuelve ahora un `array` en lugar de un `iterable`, y sus argumentos heredados `$deep` y `$filterType` lanzan ahora `Nette\DeprecatedException`. Desaparecieron de la firma ya en la versión 3.1, pero hasta ahora seguían funcionando en tiempo de ejecución. Para todo el subárbol use `getComponentTree()`, combinado opcionalmente con `array_filter()` para filtrar por tipo. -- Las notificaciones de conexión se entregan ahora de arriba abajo: el callback `$attached` del padre se ejecuta antes que el de sus hijos, lo que le permite al padre preparar su estado o incluso eliminar un hijo antes de que se procese. El orden de desconexión no cambia (primero los hijos). -- Se eliminaron los métodos sobrescribibles `attached()` y `detached()`; registre los callbacks con `monitor($type, $attached, $detached)` en su lugar. -- La clase `Component` ya no usa el trait `Nette\SmartObject`, así que en la clase base desaparece el acceso mágico a las propiedades (`Control` y `BaseControl` añaden el trait por su cuenta). -- Se eliminó la constante obsoleta `NAME_SEPARATOR`; use `NameSeparator`, disponible desde la versión 3.0.3. - - -Actualización a la versión 3.0 -============================== - -El constructor de `Nette\ComponentModel\Component` llevaba años sin usarse y se eliminó en la versión 3.0. Es un BC break: si llama al constructor padre en un componente o presenter, tiene que eliminar la llamada. diff --git a/component-model/fr/@home.texy b/component-model/fr/@home.texy deleted file mode 100644 index 8205b27905..0000000000 --- a/component-model/fr/@home.texy +++ /dev/null @@ -1,82 +0,0 @@ -Modèle de composants -******************** - -.[perex] -Le composant est une notion importante dans Nette. Nous insérons dans les pages des [composants visuels interactifs |application:components] ; les formulaires et tous leurs éléments sont eux aussi des composants. Les deux classes de base dont héritent tous ces composants font partie du paquet `nette/component-model` et sont responsables de la construction de l'arbre hiérarchique des composants. - - -Component -========= -[api:Nette\ComponentModel\Component] est l'ancêtre commun de tous les composants. Il contient la méthode `getName()`, qui renvoie le nom du composant, et la méthode `getParent()`, qui renvoie son parent. On peut définir les deux avec la méthode `setParent()` : le premier paramètre est le parent, le second le nom du composant. - - -lookup(?string $type, bool $throw=true): ?Component .[method] -------------------------------------------------------------- -Recherche vers le haut de la hiérarchie un objet de la classe ou de l'interface voulue. Par exemple, `$component->lookup(Nette\Application\UI\Presenter::class)` renvoie le presenter si le composant y est rattaché, même à plusieurs niveaux de distance. Si aucun objet correspondant n'est trouvé, la méthode lève une exception ; passez `false` en second argument pour obtenir `null` à la place. Si vous passez `null` comme `$type`, la méthode cherche le composant le plus haut de l'arbre, c'est-à-dire la racine sans parent. - - -lookupPath(?string $type=null, bool $throw=true): ?string .[method] -------------------------------------------------------------------- -Renvoie ce qu'on appelle le chemin, une chaîne formée en concaténant les noms de tous les composants situés entre le composant courant et le composant recherché. Ainsi, `$component->lookupPath(Nette\Application\UI\Presenter::class)` renvoie l'identifiant unique du composant par rapport au presenter. Quand `$type` vaut `null` (ou est omis), le chemin est mesuré jusqu'à la racine de l'arbre. - - -Container -========= -[api:Nette\ComponentModel\Container] est le composant parent, c'est-à-dire celui qui contient des enfants et forme ainsi la structure arborescente. Il dispose de méthodes pour ajouter, récupérer et retirer facilement des objets. C'est l'ancêtre, par exemple, du formulaire et des classes `Control` et `Presenter`. Les descendants qui utilisent le trait `ArrayAccess` (comme `Control` et `Presenter`) permettent aussi d'accéder aux enfants par la notation de tableau, par ex. `$container['child']`. - - -addComponent(Component $component, ?string $name, ?string $insertBefore=null): static .[method] ------------------------------------------------------------------------------------------------ -Ajoute un composant au conteneur comme enfant. Si `$name` vaut `null`, le nom propre du composant est utilisé. Grâce au paramètre facultatif `$insertBefore`, le nom d'un enfant existant, le nouveau composant est inséré juste avant celui-ci ; sinon, il est ajouté à la fin. La méthode renvoie le conteneur lui-même, les appels peuvent donc s'enchaîner. - - -removeComponent(Component $component): void .[method] ------------------------------------------------------ -Retire un composant enfant du conteneur. - - -getComponent(string $name): ?Component .[method] ------------------------------------------------- -Renvoie un composant. La tentative de récupérer un enfant non défini appelle la méthode fabrique `createComponent($name)`. La méthode `createComponent($name)` appelle dans le composant courant la méthode `createComponent<nom du composant>` en lui passant le nom du composant en paramètre. Le composant créé est ensuite ajouté au composant courant comme son enfant. Nous appelons ces méthodes des fabriques de composants ; elles peuvent être implémentées dans les classes héritant de `Container`. - - -getComponents(): IComponent[] .[method] ---------------------------------------- -Renvoie les descendants directs sous forme de tableau ; les clés contiennent les noms de ces composants. Pour récupérer récursivement tout le sous-arbre, utilisez `getComponentTree()`, éventuellement combiné à `array_filter()` pour filtrer par type. (Les paramètres `$deep` et `$filterType` connus des versions antérieures ont été supprimés dans la version 4.0.) - - -getComponentTree(): list<IComponent> .[method] ----------------------------------------------- -Récupère toute la hiérarchie des composants, y compris tous les composants enfants imbriqués, sous forme de tableau indexé. Le parcours se fait en profondeur d'abord. - - -Surveillance des ancêtres -========================= - -Le modèle de composants de Nette permet un travail très dynamique avec l'arbre (nous pouvons retirer, déplacer, ajouter des composants) ; il serait donc erroné de compter sur le fait qu'après la création d'un composant, le parent, le parent du parent, etc., soient immédiatement connus (dans le constructeur). D'ordinaire, le parent n'est pas connu du tout au moment de la création du composant. - -Comment un composant peut-il apprendre l'instant où il est rattaché sous un presenter, ou sous n'importe quel autre ancêtre d'un type donné ? Surveiller le parent direct ne suffit pas, car le rattachement peut se produire plus haut dans l'arbre, par exemple quand c'est le parent du parent qui est attaché. C'est à cela que sert la méthode [monitor($type, $attached, $detached) |api:Nette\ComponentModel\Component::monitor()] : un composant déclare qu'il veut être averti dès qu'un ancêtre de la classe ou de l'interface `$type` apparaît au-dessus de lui dans l'arbre, ou en disparaît. Un composant peut surveiller autant de types qu'il veut ; le callback `$attached` se déclenche quand un ancêtre correspondant se rattache et le reçoit en argument, tandis que `$detached` se déclenche quand il se détache. La surveillance peut être arrêtée avec `unmonitor($type)`. - -Les notifications suivent la structure de l'arbre. Au rattachement, un ancêtre est averti avant ses descendants (de haut en bas) : un parent peut donc préparer d'abord un état partagé, voire retirer un enfant avant que le callback de celui-ci ne s'exécute. Au détachement, l'ordre est inversé, les descendants sont avertis en premier. Les callbacks sont par ailleurs dédupliqués : le même callback n'est jamais appelé deux fois pour le même objet. Pour le raisonnement derrière ce comportement, voyez l'[article de blog sur la version 4.0 |https://blog.nette.org/en/nette-component-model-4-0]. - -Un exemple pour mieux comprendre : la classe `UploadControl`, qui représente dans Nette Forms le contrôle de formulaire servant à envoyer des fichiers, doit fixer l'attribut `enctype` du formulaire à `multipart/form-data`. Or, au moment où l'objet est créé, il peut n'être rattaché à aucun formulaire. À quel moment modifier alors le formulaire ? La solution est simple : la demande de surveillance se fait dans le constructeur : - -```php -class UploadControl extends Nette\Forms\Controls\BaseControl -{ - public function __construct($label) - { - $this->monitor(Nette\Forms\Form::class, function ($form): void { - $form->setHtmlAttribute('enctype', 'multipart/form-data'); - }); - // ... - } - - // ... -} -``` - -et dès que le formulaire est disponible, le callback est appelé. - - -Si vous passez à une version plus récente, consultez la page [mise à niveau |upgrading]. diff --git a/component-model/fr/@left-menu.texy b/component-model/fr/@left-menu.texy deleted file mode 100644 index a325edaeb4..0000000000 --- a/component-model/fr/@left-menu.texy +++ /dev/null @@ -1,13 +0,0 @@ -Modèle de composants -******************** -- [Introduction |@home] -- [Mise à niveau|upgrading] - - -Pour aller plus loin -******************** -- [Documentation Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Bonnes pratiques |best-practices:] -- [Résolution de problèmes |nette:troubleshooting] diff --git a/component-model/fr/@meta.texy b/component-model/fr/@meta.texy deleted file mode 100644 index 72ae4b8db8..0000000000 --- a/component-model/fr/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentation Nette}} diff --git a/component-model/fr/upgrading.texy b/component-model/fr/upgrading.texy deleted file mode 100644 index c51378a805..0000000000 --- a/component-model/fr/upgrading.texy +++ /dev/null @@ -1,20 +0,0 @@ -Mise à niveau -************* - - -Passage à la version 4.0 -======================== - -La plupart des changements ne touchent que les auteurs de composants personnalisés ; le code ordinaire d'une application n'est pas concerné. L'histoire complète est dans l'[article de blog de la sortie |https://blog.nette.org/en/nette-component-model-4-0]. - -- La méthode `getComponents()` renvoie désormais un `array` au lieu d'un `iterable`, et ses anciens arguments `$deep` et `$filterType` lèvent maintenant `Nette\DeprecatedException`. Ils avaient disparu de la signature dès la version 3.1, mais continuaient de fonctionner à l'exécution jusqu'à présent. Pour tout le sous-arbre, utilisez `getComponentTree()`, éventuellement combiné à `array_filter()` pour filtrer par type. -- Les notifications d'attachement sont désormais délivrées de haut en bas : le callback `$attached` du parent s'exécute avant celui de ses enfants, ce qui permet au parent de préparer son état, voire de retirer un enfant avant qu'il ne soit traité. L'ordre du détachement est inchangé (les enfants d'abord). -- Les méthodes surchargeables `attached()` et `detached()` ont été supprimées ; enregistrez plutôt vos callbacks par `monitor($type, $attached, $detached)`. -- La classe `Component` n'utilise plus le trait `Nette\SmartObject` : l'accès magique aux propriétés a donc disparu de la classe de base (`Control` et `BaseControl` ajoutent le trait eux-mêmes). -- La constante dépréciée `NAME_SEPARATOR` a été supprimée ; utilisez `NameSeparator`, disponible depuis la version 3.0.3. - - -Passage à la version 3.0 -======================== - -Le constructeur de `Nette\ComponentModel\Component` n'était plus utilisé depuis des années et a été supprimé dans la version 3.0. C'est une rupture de compatibilité : si vous appelez le constructeur parent dans un composant ou un presenter, vous devez supprimer cet appel. diff --git a/component-model/it/@home.texy b/component-model/it/@home.texy deleted file mode 100644 index fe9dc4a7f6..0000000000 --- a/component-model/it/@home.texy +++ /dev/null @@ -1,82 +0,0 @@ -Component Model -*************** - -.[perex] -Un concetto importante in Nette è il componente. Nelle pagine inseriamo [componenti visivi interattivi |application:components]; anche i form e tutti i loro elementi sono componenti. Le due classi di base da cui tutti questi componenti ereditano fanno parte del pacchetto `nette/component-model` e si occupano di creare la gerarchia dell'albero dei componenti. - - -Component -========= -[api:Nette\ComponentModel\Component] è l'antenato comune di tutti i componenti. Contiene il metodo `getName()`, che restituisce il nome del componente, e il metodo `getParent()`, che ne restituisce il genitore. Entrambi si possono impostare con il metodo `setParent()`: il primo parametro è il genitore, il secondo il nome del componente. - - -lookup(?string $type, bool $throw=true): ?Component .[method] -------------------------------------------------------------- -Cerca verso l'alto nella gerarchia un oggetto della classe o dell'interfaccia desiderata. Per esempio `$component->lookup(Nette\Application\UI\Presenter::class)` restituisce il presenter se il componente vi è collegato, anche attraverso più livelli. Se non trova alcun oggetto corrispondente, lancia un'eccezione; passate `false` come secondo argomento per far restituire invece `null`. Se passate `null` come `$type`, il metodo cerca il componente più in alto nell'albero, cioè la radice senza genitore. - - -lookupPath(?string $type=null, bool $throw=true): ?string .[method] -------------------------------------------------------------------- -Restituisce il cosiddetto percorso, cioè una stringa formata concatenando i nomi di tutti i componenti sul cammino tra il componente corrente e quello cercato. Per esempio `$component->lookupPath(Nette\Application\UI\Presenter::class)` restituisce l'identificatore univoco del componente rispetto al presenter. Quando `$type` è `null` (oppure viene omesso), il percorso si misura fino alla radice dell'albero. - - -Container -========= -[api:Nette\ComponentModel\Container] è il componente genitore, cioè il componente che contiene dei figli e forma così la struttura ad albero. Ha metodi per aggiungere, ottenere e rimuovere facilmente gli oggetti. È l'antenato per esempio del form o delle classi `Control` e `Presenter`. I discendenti che usano il trait `ArrayAccess` (come `Control` e `Presenter`) permettono anche di accedere ai figli con la notazione ad array, per esempio `$container['child']`. - - -addComponent(Component $component, ?string $name, ?string $insertBefore=null): static .[method] ------------------------------------------------------------------------------------------------ -Aggiunge un componente al container come figlio. Se `$name` è `null`, viene usato il nome proprio del componente. Con il parametro facoltativo `$insertBefore`, cioè il nome di un figlio esistente, il nuovo componente viene inserito subito prima di esso; altrimenti viene accodato alla fine. Il metodo restituisce il container stesso, così le chiamate si possono concatenare. - - -removeComponent(Component $component): void .[method] ------------------------------------------------------ -Rimuove un componente figlio dal container. - - -getComponent(string $name): ?Component .[method] ------------------------------------------------- -Restituisce un componente. Il tentativo di ottenere un figlio non definito richiama il metodo factory `createComponent($name)`. Il metodo `createComponent($name)` chiama nel componente corrente il metodo `createComponent<nome del componente>`, passandogli come parametro il nome del componente. Il componente creato viene poi aggiunto al componente corrente come suo figlio. Questi metodi li chiamiamo factory di componenti e si possono realizzare nelle classi che ereditano da `Container`. - - -getComponents(): IComponent[] .[method] ---------------------------------------- -Restituisce i discendenti diretti come array; le chiavi contengono i nomi di questi componenti. Per ottenere ricorsivamente l'intero sottoalbero usate `getComponentTree()`, eventualmente combinato con `array_filter()` per filtrare per tipo. (I parametri `$deep` e `$filterType` noti dalle versioni più vecchie sono stati rimossi nella versione 4.0.) - - -getComponentTree(): list<IComponent> .[method] ----------------------------------------------- -Ottiene l'intera gerarchia dei componenti, compresi tutti i componenti figli annidati, come array indicizzato. La ricerca è in profondità. - - -Sorvegliare gli antenati -======================== - -Il modello a componenti di Nette permette di lavorare con l'albero in modo molto dinamico (possiamo rimuovere, spostare, aggiungere componenti), quindi sarebbe un errore contare sul fatto che dopo la creazione di un componente si conoscano subito (nel costruttore) il genitore, il genitore del genitore ecc. Di solito, quando il componente viene creato, il genitore non è affatto noto. - -Come può un componente scoprire il momento in cui viene collegato sotto un presenter, o sotto un altro antenato di un dato tipo? Sorvegliare il genitore diretto non basta, perché il collegamento può avvenire più in alto nell'albero, per esempio quando viene collegato il genitore del genitore. È a questo che serve il metodo [monitor($type, $attached, $detached) |api:Nette\ComponentModel\Component::monitor()]: un componente dichiara di voler essere avvisato ogni volta che sopra di lui nell'albero compare, o da esso sparisce, un antenato della classe o dell'interfaccia `$type`. Un componente può sorvegliare un numero qualsiasi di tipi; il callback `$attached` scatta quando un antenato corrispondente si collega e riceve quell'antenato come argomento, mentre `$detached` scatta quando si scollega. La sorveglianza si può interrompere con `unmonitor($type)`. - -Le notifiche seguono la struttura dell'albero. Al collegamento un antenato viene avvisato prima dei suoi discendenti (dall'alto verso il basso), così un genitore può preparare per primo lo stato condiviso, o perfino rimuovere un figlio prima che il callback del figlio venga eseguito. Al distacco l'ordine è invertito: vengono avvisati per primi i discendenti. I callback vengono inoltre deduplicati, così lo stesso callback non viene mai chiamato due volte per lo stesso oggetto. Il ragionamento dietro questo comportamento lo trovate nell'[articolo del blog sulla versione 4.0 |https://blog.nette.org/en/nette-component-model-4-0]. - -Per capire meglio, ecco un esempio: la classe `UploadControl`, che rappresenta in Nette Forms il controllo per caricare i file, deve impostare l'attributo `enctype` del form a `multipart/form-data`. Nel momento in cui l'oggetto viene creato, però, può non essere collegato ad alcun form. In quale momento va quindi modificato il form? La soluzione è semplice: la richiesta di sorveglianza si fa nel costruttore: - -```php -class UploadControl extends Nette\Forms\Controls\BaseControl -{ - public function __construct($label) - { - $this->monitor(Nette\Forms\Form::class, function ($form): void { - $form->setHtmlAttribute('enctype', 'multipart/form-data'); - }); - // ... - } - - // ... -} -``` - -e appena il form diventa disponibile, il callback viene richiamato. - - -Se state aggiornando a una versione più recente, guardate la pagina [aggiornamento |upgrading]. diff --git a/component-model/it/@left-menu.texy b/component-model/it/@left-menu.texy deleted file mode 100644 index d14767c2db..0000000000 --- a/component-model/it/@left-menu.texy +++ /dev/null @@ -1,13 +0,0 @@ -Modello a componenti -******************** -- [Panoramica |@home] -- [Aggiornamento|upgrading] - - -Letture consigliate -******************* -- [Documentazione di Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Best practice |best-practices:] -- [Risoluzione dei problemi |nette:troubleshooting] diff --git a/component-model/it/@meta.texy b/component-model/it/@meta.texy deleted file mode 100644 index 4647d0c8a2..0000000000 --- a/component-model/it/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentazione Nette}} diff --git a/component-model/it/upgrading.texy b/component-model/it/upgrading.texy deleted file mode 100644 index 189a793eaf..0000000000 --- a/component-model/it/upgrading.texy +++ /dev/null @@ -1,20 +0,0 @@ -Aggiornamento -************* - - -Aggiornamento alla versione 4.0 -=============================== - -La maggior parte delle modifiche riguarda solo gli autori di componenti personalizzati; il normale codice applicativo non ne risente. Tutta la storia la trovate nell'[articolo del blog sul rilascio |https://blog.nette.org/en/nette-component-model-4-0]. - -- Il metodo `getComponents()` restituisce ora un `array` invece di un `iterable` e i suoi vecchi argomenti `$deep` e `$filterType` lanciano ora `Nette\DeprecatedException`. Erano spariti dalla firma già nella versione 3.1, ma fino a ora continuavano a funzionare a runtime. Per l'intero sottoalbero usate `getComponentTree()`, eventualmente combinato con `array_filter()` per filtrare per tipo. -- Le notifiche di attach vengono ora consegnate dall'alto verso il basso: il callback `$attached` del genitore viene eseguito prima di quelli dei figli, il che permette al genitore di preparare il proprio stato o perfino di rimuovere un figlio prima che venga elaborato. L'ordine di distacco è invariato (prima i figli). -- I metodi ridefinibili `attached()` e `detached()` sono stati rimossi; registrate invece i callback con `monitor($type, $attached, $detached)`. -- La classe `Component` non usa più il trait `Nette\SmartObject`, quindi l'accesso magico alle proprietà è sparito dalla classe base (`Control` e `BaseControl` aggiungono il trait da sé). -- La costante deprecata `NAME_SEPARATOR` è stata rimossa; usate `NameSeparator`, disponibile dalla versione 3.0.3. - - -Aggiornamento alla versione 3.0 -=============================== - -Il costruttore di `Nette\ComponentModel\Component` non veniva usato da anni ed è stato rimosso nella versione 3.0. È un BC break: se in un componente o in un presenter chiamate il costruttore genitore, dovete rimuovere la chiamata. diff --git a/component-model/ja/@home.texy b/component-model/ja/@home.texy deleted file mode 100644 index 6e004510e1..0000000000 --- a/component-model/ja/@home.texy +++ /dev/null @@ -1,82 +0,0 @@ -Component Model -*************** - -.[perex] -Nette の大事な考え方のひとつがコンポーネントです。ページには[目に見える対話的なコンポーネント |application:components]を差し込みます。フォームとそのすべての要素もコンポーネントです。これらすべてのコンポーネントが継承する 2 つの基本のクラスは `nette/component-model` のパッケージの一部で、コンポーネントの木の階層を作ることを受け持ちます。 - - -Component -========= -[api:Nette\ComponentModel\Component]はすべてのコンポーネントの共通の祖先です。コンポーネントの名前を返す `getName()` メソッドと、その親を返す `getParent()` メソッドを持ちます。どちらも `setParent()` メソッドで設定できます。第 1 パラメータが親、第 2 パラメータがコンポーネントの名前です。 - - -lookup(?string $type, bool $throw=true): ?Component .[method] -------------------------------------------------------------- -階層を上へたどって、目当てのクラスやインターフェースのオブジェクトを探します。たとえば `$component->lookup(Nette\Application\UI\Presenter::class)` は、そのコンポーネントがプレゼンターにつながっていれば、何段か上でもそのプレゼンターを返します。合うオブジェクトが見つからなければ例外を投げます。代わりに `null` を返させたいなら、第 2 引数に `false` を渡します。`$type` に `null` を渡すと、このメソッドは木のいちばん上のコンポーネント、つまり親のない根を探します。 - - -lookupPath(?string $type=null, bool $throw=true): ?string .[method] -------------------------------------------------------------------- -いわゆるパスを返します。これは今のコンポーネントと探しているコンポーネントのあいだの道すじにあるすべてのコンポーネントの名前をつないだ文字列です。ですからたとえば `$component->lookupPath(Nette\Application\UI\Presenter::class)` は、プレゼンターから見たそのコンポーネントの一意の識別子を返します。`$type` が `null` の場合(または省いた場合)、パスは木の根まで測られます。 - - -Container -========= -[api:Nette\ComponentModel\Container]は親のコンポーネント、つまり子を含んで木の構造を作るコンポーネントです。オブジェクトを簡単に足し、取り出し、取り除くメソッドを持ちます。たとえばフォームや `Control`、`Presenter` のクラスの祖先です。`ArrayAccess` のトレイトを使う子孫(`Control` や `Presenter` など)では、`$container['child']` のように配列の書き方で子に触れられます。 - - -addComponent(Component $component, ?string $name, ?string $insertBefore=null): static .[method] ------------------------------------------------------------------------------------------------ -コンテナに子としてコンポーネントを足します。`$name` が `null` なら、そのコンポーネント自身の名前が使われます。省略できる `$insertBefore`(既存の子の名前)を使うと、新しいコンポーネントはそのすぐ前に差し込まれます。そうでなければ最後に足されます。このメソッドはコンテナ自身を返すので、呼び出しをつなげられます。 - - -removeComponent(Component $component): void .[method] ------------------------------------------------------ -コンテナから子のコンポーネントを取り除きます。 - - -getComponent(string $name): ?Component .[method] ------------------------------------------------- -コンポーネントを返します。定義されていない子を取り出そうとすると、ファクトリメソッド `createComponent($name)` が呼ばれます。`createComponent($name)` メソッドは、今のコンポーネントの `createComponent<コンポーネントの名前>` というメソッドを、コンポーネントの名前をパラメータとして渡して呼びます。作られたコンポーネントは、そのあと今のコンポーネントの子として足されます。これらのメソッドをコンポーネントのファクトリと呼び、`Container` を継承したクラスで実装できます。 - - -getComponents(): IComponent[] .[method] ---------------------------------------- -直接の子孫を配列として返します。キーにはそれらのコンポーネントの名前が入ります。部分木全体を再帰的に取り出すには `getComponentTree()` を、型で絞るなら `array_filter()` と組み合わせて使ってください。(古い版で知られていた `$deep` と `$filterType` のパラメータは、バージョン 4.0 で取り除かれました。) - - -getComponentTree(): list<IComponent> .[method] ----------------------------------------------- -入れ子のすべての子のコンポーネントも含めた、コンポーネントの階層全体を添字の配列として取り出します。探索は深さ優先です。 - - -祖先を見張る -====== - -Nette のコンポーネントのしくみは木をとても動的に扱えるので(コンポーネントを取り除いたり、移したり、足したりできます)、コンポーネントを作った直後(コンストラクタの中)に親や親の親などが分かっていると当てにするのは誤りです。ふつう、コンポーネントが作られる時点で親はまったく分かっていません。 - -では、コンポーネントは自分がプレゼンターの下(あるいは、ある型のほかの祖先の下)に取り付けられた瞬間をどう知るのでしょうか。直接の親を見張るだけでは足りません。つながりは木のもっと上で、たとえば親の親が取り付けられたときに起こるかもしれないからです。そのために [monitor($type, $attached, $detached) |api:Nette\ComponentModel\Component::monitor()]メソッドがあります。コンポーネントは、クラスやインターフェース `$type` の祖先が木の中で自分の上に現れたとき、あるいはそこから消えたときに知らせてほしいと申し出ます。コンポーネントはいくつでも型を見張れます。`$attached` のコールバックは合う祖先がつながったときに発火し、その祖先を引数として受け取ります。`$detached` は切り離されたときに発火します。見張るのは `unmonitor($type)` でやめられます。 - -知らせは木の構造に沿って届きます。取り付けのときは祖先が子孫より先に知らされるので(上から下へ)、親が共有の状態を先に整えたり、子自身のコールバックが走る前にその子を取り除いたりできます。取り外しのときは順序が逆で、子孫が先に知らされます。コールバックの重複も取り除かれるので、同じオブジェクトについて同じコールバックが二度呼ばれることはありません。この振る舞いの理由は、[バージョン 4.0 のブログ記事 |https://blog.nette.org/en/nette-component-model-4-0]をご覧ください。 - -分かりやすくするために例を挙げます。Nette Forms でファイルをアップロードするフォームの要素を表す `UploadControl` クラスは、フォームの `enctype` の属性を `multipart/form-data` にしなければなりません。しかしそのオブジェクトが作られる時点では、どのフォームにも取り付けられていないかもしれません。ではどの時点でフォームに手を入れればよいのでしょうか。解は簡単で、コンストラクタで見張りを申し出るのです。 - -```php -class UploadControl extends Nette\Forms\Controls\BaseControl -{ - public function __construct($label) - { - $this->monitor(Nette\Forms\Form::class, function ($form): void { - $form->setHtmlAttribute('enctype', 'multipart/form-data'); - }); - // ... - } - - // ... -} -``` - -そしてフォームが使えるようになったとたん、そのコールバックが呼ばれます。 - - -新しい版へ上げるなら、[アップグレード |upgrading]のページをご覧ください。 diff --git a/component-model/ja/@left-menu.texy b/component-model/ja/@left-menu.texy deleted file mode 100644 index eb3baffa8e..0000000000 --- a/component-model/ja/@left-menu.texy +++ /dev/null @@ -1,13 +0,0 @@ -コンポーネントモデル -********** -- [概要 |@home] -- [アップグレード|upgrading] - - -関連情報 -**** -- [Nette ドキュメント |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [ベストプラクティス |best-practices:] -- [トラブルシューティング |nette:troubleshooting] diff --git a/component-model/ja/@meta.texy b/component-model/ja/@meta.texy deleted file mode 100644 index 43b85f3cac..0000000000 --- a/component-model/ja/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette ドキュメント}} diff --git a/component-model/ja/upgrading.texy b/component-model/ja/upgrading.texy deleted file mode 100644 index 601fd0da99..0000000000 --- a/component-model/ja/upgrading.texy +++ /dev/null @@ -1,20 +0,0 @@ -アップグレード -******* - - -バージョン 4.0 へのアップグレード -=================== - -変更のほとんどは独自のコンポーネントを書く人にだけ関わり、ふつうのアプリケーションのコードには影響しません。詳しい経緯は[リリースのブログ記事 |https://blog.nette.org/en/nette-component-model-4-0]をご覧ください。 - -- `getComponents()` メソッドは `iterable` ではなく `array` を返すようになり、古い `$deep` と `$filterType` の引数は `Nette\DeprecatedException` を投げるようになりました。これらはバージョン 3.1 で見出しから消えていましたが、実行時には今まで動いていました。部分木全体には `getComponentTree()` を、型で絞るなら `array_filter()` と組み合わせて使ってください。 -- 取り付けの知らせは上から下へ届くようになりました。親の `$attached` のコールバックが子のものより先に走るので、親は自分の状態を整えたり、子が処理される前にそれを取り除いたりできます。取り外しの順序は変わりません(子が先です)。 -- 上書きできた `attached()` と `detached()` のメソッドは取り除かれました。代わりに `monitor($type, $attached, $detached)` でコールバックを登録してください。 -- `Component` クラスはもう `Nette\SmartObject` のトレイトを使わないので、基底のクラスからマジックによるプロパティのアクセスはなくなりました(`Control` と `BaseControl` は自分でそのトレイトを足します)。 -- 非推奨だった `NAME_SEPARATOR` の定数は取り除かれました。バージョン 3.0.3 からある `NameSeparator` を使ってください。 - - -バージョン 3.0 へのアップグレード -=================== - -`Nette\ComponentModel\Component` のコンストラクタは何年も使われておらず、バージョン 3.0 で取り除かれました。これは後方互換を壊す変更です。コンポーネントやプレゼンターで親のコンストラクタを呼んでいるなら、その呼び出しを取り除かなければなりません。 diff --git a/component-model/meta.json b/component-model/meta.json deleted file mode 100644 index eed25f2df7..0000000000 --- a/component-model/meta.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": "4.x", - "repo": "nette/component-model", - "composer": "nette/component-model", - "api": "https://api.nette.org/component-model/" -} diff --git a/component-model/pl/@home.texy b/component-model/pl/@home.texy deleted file mode 100644 index 7391e0af14..0000000000 --- a/component-model/pl/@home.texy +++ /dev/null @@ -1,82 +0,0 @@ -Model komponentów -***************** - -.[perex] -Ważnym pojęciem w Nette jest komponent. Do stron wstawiamy [wizualne interaktywne komponenty |application:components]; formularze i wszystkie ich elementy również są komponentami. Dwie podstawowe klasy, po których dziedziczą wszystkie te komponenty, są częścią pakietu `nette/component-model` i odpowiadają za tworzenie hierarchii drzewa komponentów. - - -Component -========= -[api:Nette\ComponentModel\Component] to wspólny przodek wszystkich komponentów. Zawiera metodę `getName()` zwracającą nazwę komponentu i metodę `getParent()` zwracającą jego rodzica. Oba można ustawić metodą `setParent()`: pierwszym parametrem jest rodzic, a drugim nazwa komponentu. - - -lookup(?string $type, bool $throw=true): ?Component .[method] -------------------------------------------------------------- -Szuka w górę hierarchii obiektu pożądanej klasy albo interfejsu. Na przykład `$component->lookup(Nette\Application\UI\Presenter::class)` zwraca presenter, jeśli komponent jest do niego podłączony, nawet przez kilka poziomów. Jeśli pasujący obiekt nie zostanie znaleziony, rzuca wyjątek; przekaż jako drugi argument `false`, żeby zamiast tego zwrócić `null`. Jeśli przekażesz jako `$type` wartość `null`, metoda szuka najwyższego komponentu w drzewie, czyli korzenia bez rodzica. - - -lookupPath(?string $type=null, bool $throw=true): ?string .[method] -------------------------------------------------------------------- -Zwraca tak zwaną ścieżkę, czyli ciąg powstały z połączenia nazw wszystkich komponentów na drodze między bieżącym komponentem a komponentem szukanym. Na przykład `$component->lookupPath(Nette\Application\UI\Presenter::class)` zwraca unikalny identyfikator komponentu względem presentera. Gdy `$type` to `null` (albo jest pominięty), ścieżka mierzona jest do korzenia drzewa. - - -Container -========= -[api:Nette\ComponentModel\Container] to komponent rodzicielski, czyli komponent zawierający dzieci i tworzący tym samym strukturę drzewiastą. Ma metody do łatwego dodawania, pobierania i usuwania obiektów. Jest przodkiem na przykład formularza albo klas `Control` i `Presenter`. Potomkowie używający traitu `ArrayAccess` (jak `Control` i `Presenter`) pozwalają też sięgać po dzieci zapisem tablicowym, np. `$container['child']`. - - -addComponent(Component $component, ?string $name, ?string $insertBefore=null): static .[method] ------------------------------------------------------------------------------------------------ -Dodaje komponent do kontenera jako dziecko. Jeśli `$name` to `null`, używana jest własna nazwa komponentu. Za pomocą opcjonalnego `$insertBefore`, czyli nazwy istniejącego dziecka, nowy komponent wstawiany jest tuż przed nim; w przeciwnym razie dołączany jest na koniec. Metoda zwraca sam kontener, więc wywołania można łączyć w łańcuch. - - -removeComponent(Component $component): void .[method] ------------------------------------------------------ -Usuwa komponent potomny z kontenera. - - -getComponent(string $name): ?Component .[method] ------------------------------------------------- -Zwraca komponent. Próba pobrania niezdefiniowanego dziecka wywołuje metodę fabrykującą `createComponent($name)`. Metoda `createComponent($name)` wywołuje w bieżącym komponencie metodę `createComponent<nazwa komponentu>`, przekazując nazwę komponentu jako parametr. Utworzony komponent dodawany jest następnie do bieżącego komponentu jako jego dziecko. Metody te nazywamy fabrykami komponentów i można je implementować w klasach dziedziczących po `Container`. - - -getComponents(): IComponent[] .[method] ---------------------------------------- -Zwraca bezpośrednich potomków jako tablicę; klucze zawierają nazwy tych komponentów. Żeby pobrać rekurencyjnie całe poddrzewo, użyj `getComponentTree()`, opcjonalnie w połączeniu z `array_filter()` do filtrowania po typie. (Parametry `$deep` i `$filterType` znane ze starszych wersji zostały usunięte w wersji 4.0.) - - -getComponentTree(): list<IComponent> .[method] ----------------------------------------------- -Pobiera całą hierarchię komponentów, wraz ze wszystkimi zagnieżdżonymi komponentami potomnymi, jako tablicę indeksowaną. Przeszukiwanie odbywa się w głąb. - - -Monitorowanie przodków -====================== - -Model komponentów Nette pozwala bardzo dynamicznie pracować z drzewem (możemy usuwać, przenosić, dodawać komponenty), więc błędem byłoby poleganie na tym, że po utworzeniu komponentu rodzic, rodzic rodzica itd. są znani natychmiast (w konstruktorze). Zwykle przy tworzeniu komponentu rodzic w ogóle nie jest znany. - -Jak komponent może dowiedzieć się o momencie, w którym zostaje podłączony pod presenter albo pod dowolnego innego przodka danego typu? Obserwowanie bezpośredniego rodzica nie wystarczy, bo połączenie może nastąpić wyżej w drzewie, na przykład gdy podłączany jest rodzic rodzica. Do tego służy metoda [monitor($type, $attached, $detached) |api:Nette\ComponentModel\Component::monitor()]: komponent deklaruje, że chce być powiadamiany zawsze, gdy nad nim w drzewie pojawi się przodek klasy albo interfejsu `$type` albo gdy z niego zniknie. Komponent może monitorować dowolną liczbę typów; callback `$attached` uruchamia się, gdy pasujący przodek się podłączy, i otrzymuje tego przodka jako argument, a `$detached` uruchamia się, gdy się odłączy. Monitorowanie można znowu zatrzymać przez `unmonitor($type)`. - -Powiadomienia podążają za strukturą drzewa. Przy dołączaniu przodek powiadamiany jest przed swoimi potomkami (z góry na dół), więc rodzic może najpierw przygotować wspólny stan albo nawet usunąć dziecko, zanim uruchomi się jego własny callback. Przy odłączaniu kolejność jest odwrotna: najpierw powiadamiani są potomkowie. Callbacki są też deduplikowane, więc ten sam callback nigdy nie jest wywoływany dwa razy dla tego samego obiektu. Uzasadnienie tego zachowania znajdziesz we [wpisie na blogu o wersji 4.0 |https://blog.nette.org/en/nette-component-model-4-0]. - -Dla lepszego zrozumienia oto przykład: klasa `UploadControl`, reprezentująca element formularza do wysyłania plików w Nette Forms, musi ustawić formularzowi atrybut `enctype` na `multipart/form-data`. W momencie utworzenia obiektu może jednak nie być podłączona do żadnego formularza. W którym więc momencie formularz zmodyfikować? Rozwiązanie jest proste: żądanie monitorowania składa się w konstruktorze: - -```php -class UploadControl extends Nette\Forms\Controls\BaseControl -{ - public function __construct($label) - { - $this->monitor(Nette\Forms\Form::class, function ($form): void { - $form->setHtmlAttribute('enctype', 'multipart/form-data'); - }); - // ... - } - - // ... -} -``` - -a gdy tylko formularz stanie się dostępny, callback zostanie wywołany. - - -Jeśli aktualizujesz do nowszej wersji, zajrzyj na stronę [aktualizacji |upgrading]. diff --git a/component-model/pl/@left-menu.texy b/component-model/pl/@left-menu.texy deleted file mode 100644 index 7d68b3a733..0000000000 --- a/component-model/pl/@left-menu.texy +++ /dev/null @@ -1,13 +0,0 @@ -Model komponentów -***************** -- [Przegląd |@home] -- [Aktualizacja|upgrading] - - -Dalsza lektura -************** -- [Dokumentacja Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Dobre praktyki |best-practices:] -- [Rozwiązywanie problemów |nette:troubleshooting] diff --git a/component-model/pl/@meta.texy b/component-model/pl/@meta.texy deleted file mode 100644 index 61ac92d1af..0000000000 --- a/component-model/pl/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Dokumentacja Nette}} diff --git a/component-model/pl/upgrading.texy b/component-model/pl/upgrading.texy deleted file mode 100644 index a89b507636..0000000000 --- a/component-model/pl/upgrading.texy +++ /dev/null @@ -1,20 +0,0 @@ -Aktualizacja -************ - - -Aktualizacja do wersji 4.0 -========================== - -Większość zmian dotyczy tylko autorów własnych komponentów; zwykły kod aplikacji pozostaje nietknięty. Pełną historię znajdziesz we [wpisie na blogu o wydaniu |https://blog.nette.org/en/nette-component-model-4-0]. - -- Metoda `getComponents()` zwraca teraz `array` zamiast `iterable`, a jej dawne argumenty `$deep` i `$filterType` rzucają teraz `Nette\DeprecatedException`. Zniknęły z sygnatury już w wersji 3.1, ale do tej pory działały w czasie wykonania. Dla całego poddrzewa użyj `getComponentTree()`, opcjonalnie w połączeniu z `array_filter()` do filtrowania po typie. -- Powiadomienia o dołączeniu dostarczane są teraz z góry na dół: callback `$attached` rodzica uruchamia się przed callbackami dzieci, co pozwala rodzicowi przygotować swój stan albo nawet usunąć dziecko, zanim zostanie przetworzone. Kolejność odłączania pozostaje bez zmian (najpierw dzieci). -- Nadpisywalne metody `attached()` i `detached()` zostały usunięte; zamiast tego rejestruj callbacki przez `monitor($type, $attached, $detached)`. -- Klasa `Component` nie używa już traitu `Nette\SmartObject`, więc magiczny dostęp do właściwości zniknął z klasy bazowej (`Control` i `BaseControl` dodają ten trait same). -- Usunięto przestarzałą stałą `NAME_SEPARATOR`; używaj `NameSeparator`, dostępnej od wersji 3.0.3. - - -Aktualizacja do wersji 3.0 -========================== - -Konstruktor `Nette\ComponentModel\Component` nie był używany od lat i został usunięty w wersji 3.0. To BC break: jeśli wywołujesz konstruktor rodzica w komponencie albo presenterze, musisz to wywołanie usunąć. diff --git a/component-model/ru/@home.texy b/component-model/ru/@home.texy deleted file mode 100644 index c4a42dd817..0000000000 --- a/component-model/ru/@home.texy +++ /dev/null @@ -1,82 +0,0 @@ -Компонентная модель -******************* - -.[perex] -Важное понятие в Nette - компонент. В страницы мы вставляем [визуальные интерактивные компоненты |application:components]; компонентами являются и формы, и все их элементы. Два базовых класса, от которых наследуются все эти компоненты, входят в пакет `nette/component-model` и отвечают за создание иерархии дерева компонентов. - - -Component -========= -[api:Nette\ComponentModel\Component] - общий предок всех компонентов. У него есть метод `getName()`, возвращающий имя компонента, и метод `getParent()`, возвращающий его родителя. Оба можно задать методом `setParent()`: первый параметр - родитель, второй - имя компонента. - - -lookup(?string $type, bool $throw=true): ?Component .[method] -------------------------------------------------------------- -Ищет вверх по иерархии объект нужного класса или интерфейса. Например, `$component->lookup(Nette\Application\UI\Presenter::class)` возвращает презентер, если компонент к нему подключён, пусть даже через несколько уровней. Если подходящий объект не найден, метод выбрасывает исключение; передайте вторым аргументом `false`, чтобы вместо этого вернулся `null`. Если в качестве `$type` передать `null`, метод ищет самый верхний компонент в дереве, то есть корень без родителя. - - -lookupPath(?string $type=null, bool $throw=true): ?string .[method] -------------------------------------------------------------------- -Возвращает так называемый путь - строку, образованную соединением имён всех компонентов на пути между текущим компонентом и искомым. Так что, например, `$component->lookupPath(Nette\Application\UI\Presenter::class)` возвращает уникальный идентификатор компонента относительно презентера. Когда `$type` равен `null` (или опущен), путь отмеряется до корня дерева. - - -Container -========= -[api:Nette\ComponentModel\Container] - родительский компонент, то есть компонент, содержащий потомков и тем самым образующий древовидную структуру. У него есть методы для удобного добавления, получения и удаления объектов. Он является предком, например, формы или классов `Control` и `Presenter`. Потомки, использующие трейт `ArrayAccess` (такие как `Control` и `Presenter`), позволяют обращаться к потомкам и записью через массив, например `$container['child']`. - - -addComponent(Component $component, ?string $name, ?string $insertBefore=null): static .[method] ------------------------------------------------------------------------------------------------ -Добавляет компонент в контейнер как потомка. Если `$name` равно `null`, используется собственное имя компонента. С помощью необязательного `$insertBefore` - имени существующего потомка - новый компонент вставляется прямо перед ним; иначе он добавляется в конец. Метод возвращает сам контейнер, так что вызовы можно объединять в цепочку. - - -removeComponent(Component $component): void .[method] ------------------------------------------------------ -Удаляет компонент-потомок из контейнера. - - -getComponent(string $name): ?Component .[method] ------------------------------------------------- -Возвращает компонент. Попытка получить неопределённого потомка вызывает фабричный метод `createComponent($name)`. Метод `createComponent($name)` вызывает в текущем компоненте метод `createComponent<имя компонента>`, передавая имя компонента параметром. Созданный компонент затем добавляется в текущий компонент как его потомок. Эти методы мы называем фабриками компонентов, и их можно реализовать в классах, унаследованных от `Container`. - - -getComponents(): IComponent[] .[method] ---------------------------------------- -Возвращает непосредственных потомков массивом; в ключах находятся имена этих компонентов. Чтобы получить всё поддерево рекурсивно, используйте `getComponentTree()`, при необходимости в сочетании с `array_filter()` для фильтрации по типу. (Параметры `$deep` и `$filterType`, известные по прежним версиям, в версии 4.0 удалены.) - - -getComponentTree(): list<IComponent> .[method] ----------------------------------------------- -Получает всю иерархию компонентов, включая все вложенные компоненты-потомки, в виде индексированного массива. Обход идёт в глубину. - - -Наблюдение за предками -====================== - -Компонентная модель Nette позволяет очень динамично работать с деревом (мы можем удалять, перемещать, добавлять компоненты), поэтому было бы ошибкой полагаться на то, что сразу после создания компонента (в конструкторе) известны родитель, родитель родителя и т. д. Обычно при создании компонента родитель вообще неизвестен. - -Как компоненту узнать момент, когда он окажется под презентером или под любым другим предком заданного типа? Следить за непосредственным родителем недостаточно, потому что соединение может произойти выше по дереву, например когда присоединяется родитель родителя. Для этого и служит метод [monitor($type, $attached, $detached) |api:Nette\ComponentModel\Component::monitor()]: компонент объявляет, что хочет получать уведомление всякий раз, когда над ним в дереве появляется предок класса или интерфейса `$type` либо исчезает из него. Компонент может наблюдать за сколькими угодно типами; callback `$attached` срабатывает, когда подходящий предок подключается, и получает этого предка аргументом, а `$detached` срабатывает при отключении. Наблюдение можно снова прекратить через `unmonitor($type)`. - -Уведомления следуют структуре дерева. При присоединении предок уведомляется раньше своих потомков (сверху вниз), так что родитель может сначала подготовить общее состояние или даже удалить потомка до того, как выполнится собственный callback потомка. При отсоединении порядок обратный: сначала уведомляются потомки. Callback'и к тому же дедуплицируются, так что один и тот же callback никогда не вызывается дважды для одного объекта. Обоснование такого поведения смотрите в [записи блога о версии 4.0 |https://blog.nette.org/en/nette-component-model-4-0]. - -Для лучшего понимания вот пример: класс `UploadControl`, представляющий элемент формы для загрузки файлов в Nette Forms, должен задать форме атрибут `enctype` со значением `multipart/form-data`. Однако в момент создания объекта он может быть ещё не присоединён ни к какой форме. Так в какой же момент изменить форму? Решение простое: запрос на наблюдение делается в конструкторе: - -```php -class UploadControl extends Nette\Forms\Controls\BaseControl -{ - public function __construct($label) - { - $this->monitor(Nette\Forms\Form::class, function ($form): void { - $form->setHtmlAttribute('enctype', 'multipart/form-data'); - }); - // ... - } - - // ... -} -``` - -и как только форма станет доступна, callback будет вызван. - - -Если вы переходите на более новую версию, посмотрите страницу [обновления |upgrading]. diff --git a/component-model/ru/@left-menu.texy b/component-model/ru/@left-menu.texy deleted file mode 100644 index b7d69c476a..0000000000 --- a/component-model/ru/@left-menu.texy +++ /dev/null @@ -1,13 +0,0 @@ -Модель компонентов -****************** -- [Обзор |@home] -- [Обновление|upgrading] - - -Дополнительные материалы -************************ -- [Документация Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Лучшие практики |best-practices:] -- [Устранение неполадок |nette:troubleshooting] diff --git a/component-model/ru/@meta.texy b/component-model/ru/@meta.texy deleted file mode 100644 index 7f329adfce..0000000000 --- a/component-model/ru/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документация Nette}} diff --git a/component-model/ru/upgrading.texy b/component-model/ru/upgrading.texy deleted file mode 100644 index 62eec079e9..0000000000 --- a/component-model/ru/upgrading.texy +++ /dev/null @@ -1,20 +0,0 @@ -Обновление -********** - - -Обновление до версии 4.0 -======================== - -Большинство изменений затрагивает только авторов собственных компонентов; обычного кода приложения они не касаются. Всю историю целиком смотрите в [записи блога о выпуске |https://blog.nette.org/en/nette-component-model-4-0]. - -- Метод `getComponents()` теперь возвращает `array` вместо `iterable`, а его устаревшие аргументы `$deep` и `$filterType` теперь выбрасывают `Nette\DeprecatedException`. Из сигнатуры они исчезли ещё в версии 3.1, но до сих пор во время выполнения работали. Для всего поддерева используйте `getComponentTree()`, при необходимости в сочетании с `array_filter()` для фильтрации по типу. -- Уведомления о присоединении теперь доставляются сверху вниз: callback `$attached` родителя выполняется раньше, чем у его потомков, что позволяет родителю подготовить своё состояние или даже удалить потомка до того, как тот будет обработан. Порядок отсоединения не изменился (сначала потомки). -- Переопределяемые методы `attached()` и `detached()` удалены; регистрируйте callback'и через `monitor($type, $attached, $detached)`. -- Класс `Component` больше не использует трейт `Nette\SmartObject`, так что магическое обращение к свойствам из базового класса ушло (`Control` и `BaseControl` добавляют трейт сами). -- Устаревшая константа `NAME_SEPARATOR` удалена; используйте `NameSeparator`, доступную начиная с версии 3.0.3. - - -Обновление до версии 3.0 -======================== - -Конструктор `Nette\ComponentModel\Component` годами не использовался и в версии 3.0 был удалён. Это нарушение обратной совместимости: если вы вызываете родительский конструктор в компоненте или презентере, вызов нужно убрать. diff --git a/component-model/tr/@home.texy b/component-model/tr/@home.texy deleted file mode 100644 index 35ee741b3c..0000000000 --- a/component-model/tr/@home.texy +++ /dev/null @@ -1,82 +0,0 @@ -Bileşen Modeli -************** - -.[perex] -Nette'de önemli bir kavram bileşendir. Sayfalara [görsel etkileşimli bileşenler |application:components] ekleriz; formlar ve onların tüm elemanları da bileşendir. Tüm bu bileşenlerin kalıttığı iki temel sınıf `nette/component-model` paketinin parçasıdır ve bileşen ağacı hiyerarşisini oluşturmaktan sorumludur. - - -Component -========= -[api:Nette\ComponentModel\Component], tüm bileşenlerin ortak atasıdır. Bileşenin adını döndüren `getName()` metodunu ve anasını döndüren `getParent()` metodunu içerir. İkisi de `setParent()` metoduyla ayarlanabilir; ilk parametre ana, ikincisi ise bileşen adıdır. - - -lookup(?string $type, bool $throw=true): ?Component .[method] -------------------------------------------------------------- -Hiyerarşide yukarı doğru, istenen sınıf ya da arayüzden bir nesne arar. Örneğin `$component->lookup(Nette\Application\UI\Presenter::class)`, bileşen bir presenter'a bağlıysa, birkaç düzey ötede olsa bile onu döndürür. Eşleşen bir nesne bulunamazsa istisna fırlatır; onun yerine `null` döndürmesi için ikinci argüman olarak `false` aktarın. `$type` olarak `null` aktarırsanız, metot ağaçtaki en üstteki bileşeni, yani anası olmayan kökü arar. - - -lookupPath(?string $type=null, bool $throw=true): ?string .[method] -------------------------------------------------------------------- -Yol denen şeyi döndürür; bu, geçerli bileşen ile aranan bileşen arasındaki yoldaki tüm bileşenlerin adlarının birleştirilmesiyle oluşan bir dizedir. Yani örneğin `$component->lookupPath(Nette\Application\UI\Presenter::class)`, bileşenin presenter'a göre benzersiz tanımlayıcısını döndürür. `$type` değeri `null` olduğunda (ya da atlandığında) yol, ağacın köküne dek ölçülür. - - -Container -========= -[api:Nette\ComponentModel\Container], ana bileşendir; yani çocuk içeren ve böylece ağaç yapısını oluşturan bileşen. Nesneleri kolayca eklemek, almak ve kaldırmak için metotları vardır. Örneğin formun ya da `Control` ile `Presenter` sınıflarının atasıdır. `ArrayAccess` trait'ini kullanan alt sınıflar (`Control` ve `Presenter` gibi) çocuklara dizi yazımıyla erişmeye de izin verir, örneğin `$container['child']`. - - -addComponent(Component $component, ?string $name, ?string $insertBefore=null): static .[method] ------------------------------------------------------------------------------------------------ -Container'a çocuk olarak bir bileşen ekler. `$name` değeri `null` ise bileşenin kendi adı kullanılır. İsteğe bağlı `$insertBefore` parametresi (var olan bir çocuğun adı) kullanılırsa, yeni bileşen tam ondan önce eklenir; aksi hâlde sona iliştirilir. Metot container'ın kendisini döndürür, dolayısıyla çağrılar zincirlenebilir. - - -removeComponent(Component $component): void .[method] ------------------------------------------------------ -Bir çocuk bileşeni container'dan kaldırır. - - -getComponent(string $name): ?Component .[method] ------------------------------------------------- -Bir bileşen döndürür. Tanımlanmamış bir çocuğu almaya çalışmak `createComponent($name)` factory metodunu çağırır. `createComponent($name)` metodu, geçerli bileşende `createComponent<bileşen adı>` metodunu, bileşen adını parametre olarak aktararak çağırır. Oluşturulan bileşen sonra geçerli bileşene çocuk olarak eklenir. Bu metotlara bileşen factory'leri deriz ve onlar `Container` sınıfından kalıtan sınıflarda gerçekleştirilebilir. - - -getComponents(): IComponent[] .[method] ---------------------------------------- -Doğrudan alt bileşenleri bir dizi olarak döndürür; anahtarlar bu bileşenlerin adlarını içerir. Alt ağacın tamamını özyinelemeli almak için, tip süzmesi gerekiyorsa `array_filter()` ile birleştirerek `getComponentTree()` kullanın. (Eski sürümlerden bilinen `$deep` ve `$filterType` parametreleri 4.0 sürümünde kaldırıldı.) - - -getComponentTree(): list<IComponent> .[method] ----------------------------------------------- -Tüm iç içe çocuk bileşenler dahil bileşen hiyerarşisinin tamamını indeksli bir dizi olarak alır. Arama derinlik öncelikli yapılır. - - -Ataları İzleme -============== - -Nette bileşen modeli ağaçla çok dinamik çalışmaya izin verir (bileşenleri kaldırabilir, taşıyabilir, ekleyebiliriz), dolayısıyla bir bileşen oluşturulduktan hemen sonra (yapıcıda) anasının, ananın anasının vb. bilindiğine güvenmek yanlış olurdu. Genellikle bileşen oluşturulurken ana hiç bilinmez. - -Bir bileşen, bir presenter'ın ya da belirli bir tipteki başka bir atanın altına iliştirildiği anı nasıl öğrenebilir? Doğrudan anayı izlemek yetmez, çünkü bağlantı ağacın daha yukarısında, örneğin ananın anası iliştirildiğinde gerçekleşebilir. [monitor($type, $attached, $detached) |api:Nette\ComponentModel\Component::monitor()] metodu tam da bunun içindir: bir bileşen, ağaçta üstünde `$type` sınıfından ya da arayüzünden bir ata belirdiğinde ya da oradan yittiğinde bildirim almak istediğini bildirir. Bir bileşen istediği kadar tipi izleyebilir; eşleşen bir ata bağlandığında `$attached` callback'i tetiklenir ve argüman olarak o atayı alır, `$detached` ise ata ayrıldığında tetiklenir. İzleme `unmonitor($type)` ile yeniden durdurulabilir. - -Bildirimler ağaç yapısını izler. İliştirmede bir ata, alt bileşenlerinden önce bildirim alır (yukarıdan aşağıya), böylece bir ana önce paylaşılan durumu hazırlayabilir, hatta bir çocuğu kendi callback'i çalışmadan önce kaldırabilir. Ayırmada sıra terstir; önce alt bileşenler bildirim alır. Callback'ler ayrıca yinelenmeden arındırılır, dolayısıyla aynı callback aynı nesne için asla iki kez çağrılmaz. Bu davranışın ardındaki gerekçe için [4.0 sürümü hakkındaki blog yazısına |https://blog.nette.org/en/nette-component-model-4-0] bakın. - -Daha iyi anlamak için bir örnek: Nette Forms'ta dosya yükleme form denetimini temsil eden `UploadControl` sınıfı, formun `enctype` niteliğini `multipart/form-data` yapmalıdır. Ancak nesnenin oluşturulduğu anda hiçbir forma iliştirilmemiş olabilir. Öyleyse form hangi noktada değiştirilmelidir? Çözüm basittir: yapıcıda izleme isteği yapılır: - -```php -class UploadControl extends Nette\Forms\Controls\BaseControl -{ - public function __construct($label) - { - $this->monitor(Nette\Forms\Form::class, function ($form): void { - $form->setHtmlAttribute('enctype', 'multipart/form-data'); - }); - // ... - } - - // ... -} -``` - -ve form kullanılabilir olur olmaz callback çağrılır. - - -Daha yeni bir sürüme yükseltiyorsanız [yükseltme |upgrading] sayfasına bakın. diff --git a/component-model/tr/@left-menu.texy b/component-model/tr/@left-menu.texy deleted file mode 100644 index 836f507021..0000000000 --- a/component-model/tr/@left-menu.texy +++ /dev/null @@ -1,13 +0,0 @@ -Bileşen modeli -************** -- [Genel bakış |@home] -- [Yükseltme|upgrading] - - -Devamını okuyun -*************** -- [Nette dokümantasyonu |nette:] -- [Nette Application |application:how-it-works] -- [Yardımcı araçlar |utils:] -- [En iyi uygulamalar |best-practices:] -- [Sorun giderme |nette:troubleshooting] diff --git a/component-model/tr/@meta.texy b/component-model/tr/@meta.texy deleted file mode 100644 index 8dfe82f311..0000000000 --- a/component-model/tr/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokümantasyonu}} diff --git a/component-model/tr/upgrading.texy b/component-model/tr/upgrading.texy deleted file mode 100644 index f5b61e3e6d..0000000000 --- a/component-model/tr/upgrading.texy +++ /dev/null @@ -1,20 +0,0 @@ -Yükseltme -********* - - -Sürüm 4.0'a Yükseltme -===================== - -Değişikliklerin çoğu yalnızca özel bileşen yazarlarını ilgilendirir; sıradan uygulama kodu etkilenmez. Öykünün tamamı için [sürüm blog yazısına |https://blog.nette.org/en/nette-component-model-4-0] bakın. - -- `getComponents()` metodu artık `iterable` yerine bir `array` döndürüyor ve eski `$deep` ile `$filterType` argümanları artık `Nette\DeprecatedException` fırlatıyor. Onlar imzadan 3.1 sürümünde çıkmıştı, ama şimdiye dek çalışma zamanında çalışmayı sürdürüyordu. Alt ağacın tamamı için `getComponentTree()` kullanın, tip süzmesi için isteğe bağlı olarak `array_filter()` ile birleştirin. -- Attach bildirimleri artık yukarıdan aşağıya iletiliyor: bir ananın `$attached` callback'i çocuklarınınkinden önce çalışıyor; bu, ananın durumunu hazırlamasına, hatta bir çocuğu işlenmeden önce kaldırmasına olanak tanıyor. Ayrılma sırası değişmedi (önce çocuklar). -- Geçersiz kılınabilir `attached()` ve `detached()` metotları kaldırıldı; onun yerine callback'leri `monitor($type, $attached, $detached)` ile kaydedin. -- `Component` sınıfı artık `Nette\SmartObject` trait'ini kullanmıyor, dolayısıyla sihirli özellik erişimi temel sınıftan kalktı (`Control` ve `BaseControl` trait'i kendileri ekliyor). -- Deprecated `NAME_SEPARATOR` sabiti kaldırıldı; 3.0.3 sürümünden beri bulunan `NameSeparator` sabitini kullanın. - - -Sürüm 3.0'a Yükseltme -===================== - -`Nette\ComponentModel\Component` sınıfının yapıcısı yıllardır kullanılmıyordu ve 3.0 sürümünde kaldırıldı. Bu bir BC break'tir: bir bileşende ya da presenter'da ana yapıcıyı çağırıyorsanız, çağrıyı kaldırmalısınız. diff --git a/contributing/cs/@home.texy b/contributing/cs/@home.texy deleted file mode 100644 index 7c1b46225e..0000000000 --- a/contributing/cs/@home.texy +++ /dev/null @@ -1,20 +0,0 @@ -Staňte se přispěvatelem do Nette -******************************** - -.[perex] -Zjistěte, jak se můžete zapojit do našeho open source projektu. Osvojte si postupy pro přispívání do zdrojového kódu a dokumentace a staňte se součástí komunity vývojářů, kteří se aktivně podílejí na zdokonalování Nette. - - -**Kód** - -- [Jak přispět do kódu? |code] -- [Kódovací standard |coding-standard] - -**Dokumentace** - -- [Jak přispět do dokumentace? |documentation] -- [Dokumentační syntax |syntax] -- "Náhledový editor":https://editor.nette.org - -{{maintitle: Jak přispět do Nette: staňte se přispěvatelem}} -{{description: Zjistěte, jak se zapojit do open source projektu Nette: přispívejte do zdrojového kódu a dokumentace a staňte se součástí komunity vývojářů.}} diff --git a/contributing/cs/@left-menu.texy b/contributing/cs/@left-menu.texy deleted file mode 100644 index cdd44bf231..0000000000 --- a/contributing/cs/@left-menu.texy +++ /dev/null @@ -1,18 +0,0 @@ -Kód -*** -- [Jak přispět do kódu? |code] -- [Kódovací standard |coding-standard] - -Dokumentace -*********** -- [Jak přispět do dokumentace? |documentation] -- [Dokumentační syntax |syntax] -- "Náhledový editor":https://editor.nette.org - - -Další četba -*********** -- [Dokumentace Nette |nette:] -- [Nástroje |tools:] -- [Kdo tvoří Nette |https://nette.org/contributors] -- [Nette na GitHubu |https://github.com/nette] diff --git a/contributing/cs/code.texy b/contributing/cs/code.texy deleted file mode 100644 index 6a74181751..0000000000 --- a/contributing/cs/code.texy +++ /dev/null @@ -1,108 +0,0 @@ -Jak přispět do kódu -******************* - -.[perex] -Chystáte se přispět do Nette Frameworku a potřebujete se zorientovat v pravidlech a postupech? Tento průvodce pro začátečníky vám krok za krokem ukáže, jak efektivně přispívat do kódu, pracovat s repozitáři a implementovat změny. - - -Postup -====== - -Pro přispívání do kódu je nezbytné mít účet na [GitHub|https://github.com] a být obeznámen se základy práce s verzovacím systémem Git. Pokud neovládáte práci s Gitem, můžete se podívat na průvodce [git - the simple guide |https://rogerdudler.github.io/git-guide/] a případně využít některého z mnoha [grafických klientů |https://git-scm.com/downloads/guis]. - - -Příprava prostředí a repozitáře -------------------------------- - -1) na GitHubu si vytvořte [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] repozitáře [balíčku |www:packages], který se chystáte upravit -2) tento repozitář [naklonujete |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] na svůj počítač -3) nainstalujte závislosti, včetně [Nette Testeru |tester:], pomocí příkazu `composer install` -4) zkontrolujte, že testy fungují, spuštěním `composer tester` -5) vytvořte si [novou větev |#Nová větev] založenou na poslední vydané verzi - - -Implementace vlastních změn ---------------------------- - -Nyní můžete provést své vlastní úpravy kódu: - -1) naprogramujte požadované změny a nezapomeňte na testy -2) ujistěte se, že testy proběhnou úspěšně, pomocí `composer tester` -3) pomocí `dresscode check` zkontrolujte, zda kód splňuje [kódovací standard |#Coding Standards] -4) změny uložte (commitněte) s popisem v [tomto formátu |#Popis komitu] - -Můžete vytvořit několik commitů, jeden pro každý logický krok. Každý commit by měl být smysluplný samostatně. - - -Odeslání změn -------------- - -Jakmile budete se změnami spokojeni, můžete je odeslat: - -1) odešlete (pushněte) změny na GitHub do vašeho forku -2) odtud je odešlete do Nette repozitáře vytvořením [pull request|https://help.github.com/articles/creating-a-pull-request] (PR) -3) uveďte v popisu [dostatek informací |#Popis pull requestu] - - -Zapracování připomínek ----------------------- - -Vaše commity nyní uvidí i ostatní. Je běžné, že dostanete komentáře s připomínkami: - -1) sledujte navrhované úpravy -2) zapracujte je jako nové commity nebo je [slučte s předchozími |https://help.github.com/en/github/using-git/about-git-rebase] -3) znovu odešlete commity na GitHub a automaticky se objeví v pull requestu - -Nikdy nevytvářejte nový pull request kvůli úpravě stávajícího. - - -Dokumentace ------------ - -Pokud jste změnili funkčnost nebo přidali novou, nezapomeňte ji také [přidat do dokumentace |documentation]. - - -Nová větev -========== - -Pokud je to možné, provádějte změny vůči poslední vydané verzi, tj. poslednímu tagu v dané větvi. Pro tag `v3.2.1` vytvoříte větev tímto příkazem: - -```shell -git checkout -b new_branch_name v3.2.1 -``` - - -Coding Standards -================ - -Váš kód musí splňovat [coding standard] používaný v Nette Framework. Pro kontrolu a automatickou opravu použijte nástroj [DressCode |tools:coding-standard], kde najdete i návod na instalaci a použití. - - -Popis komitu -============ - -V Nette mají předměty komitů formát: `Presenter: fixed AJAX detection [Closes #69]` - -- oblast následovaná dvojtečkou -- účel commitu v minulém čase, je-li to možné, začněte slovem: "added .(přidaná nová vlastnost)", "fixed .(oprava)", "refactored .(změna v kódu beze změny chování)", changed, removed -- pokud commit přeruší zpětnou kompatibilitu, doplňte "BC break" -- případná vazba na issue tracker jako `(#123)` nebo `[Closes #69]` -- za subjektem může následovat jeden volný řádek a poté podrobnější popis včetně třeba odkazů na fórum - - -Popis pull requestu -=================== - -Při vytváření pull requestu vám rozhraní GitHubu umožní zadat název a popis. Uveďte výstižný název a v popisu poskytněte co nejvíce informací o důvodech pro vaši změnu. - -Zobrazí se také záhlaví, kde uveďte, zda se jedná o novou funkci nebo opravu chyby a zda může dojít k narušení zpětné kompatibility (BC break). Pokud je k dispozici související problém (issue), odkazujte na něj, aby byl uzavřen po schválení pull requestu. - -``` -- bug fix / new feature? <!-- #issue numbers, if any --> -- BC break? yes/no -``` - - -{{priority: -1}} -{{maintitle: Jak přispět do kódu Nette: pull request krok za krokem}} -{{description: Průvodce přispíváním do kódu Nette pro začátečníky: repozitáře, nová větev, coding standards a jak popsat komity a pull requesty.}} diff --git a/contributing/cs/coding-standard.texy b/contributing/cs/coding-standard.texy deleted file mode 100644 index ad96d06c60..0000000000 --- a/contributing/cs/coding-standard.texy +++ /dev/null @@ -1,150 +0,0 @@ -Kódovací standard -***************** - -.[perex] -Tento dokument popisuje pravidla a doporučení pro vývoj Nette. Při přispívání kódu do Nette je musíte dodržovat. Nejjednodušší způsob, jak to udělat, je napodobit existující kód. Jde o to, aby veškerý kód vypadal, jako by ho napsal jeden člověk. - -Nette Coding Standard odpovídá [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/] se dvěma hlavními výjimkami: pro odsazení používá [#tabulátory místo mezer] a pro [konstanty tříd používá PascalCase|https://blog.nette.org/cs/za-mene-kriku-v-kodu]. - -.[tip] -Mnoho z těchto pravidel umí automaticky kontrolovat a opravovat nástroj [DressCode |tools:coding-standard], takže je nemusíte hlídat ručně. Tento standard zná pod jménem `nette`: stačí do `dresscode.neon` napsat `use: [nette]` a `dresscode check` vám ukáže, kde se od něj váš kód odchyluje. - - -Obecná pravidla -=============== - -- Každý soubor PHP musí obsahovat `declare(strict_types=1)` -- Dva prázdné řádky se používají k oddělení metod pro lepší čitelnost. -- Důvod použití shut-up operátoru (`@`) musí být zdokumentován: `@mkdir($dir); // @ - adresář může existovat`. -- Pokud je použit slabě typizovaný operátor porovnání (tj. `==`, `!=`, ...), musí být zdokumentován záměr: `// == přijmout null` -- Do jednoho souboru `exceptions.php` můžete zapsat více výjimek, do souboru `enums.php` více enumů. -- U rozhraní se viditelnost metod neuvádí, protože jsou vždy veřejné. -- Každá property, návratová hodnota a parametr musí mít uvedený typ. Naopak u finálních konstant typ nikdy neuvádíme, protože je zjevný. -- K ohraničení řetězce by se měly používat jednoduché uvozovky, s výjimkou případů, kdy samotný literál obsahuje apostrofy. - - -Pojmenovací konvence -==================== - -- Nepoužívejte zkratky, pokud není celý název příliš dlouhý. -- U dvoupísmenných zkratek používejte velká písmena, u delších zkratek pascal/camel. -- Pro název třídy používejte podstatné jméno nebo slovní spojení. -- Názvy tříd musí obsahovat nejen specifičnost (`Array`), ale také obecnost (`ArrayIterator`). Výjimkou jsou atributy jazyka PHP. -- "Konstanty tříd a enumy by měly používat PascalCaps":https://blog.nette.org/cs/za-mene-kriku-v-kodu. -- "Rozhraní a abstraktní třídy by neměly obsahovat předpony nebo přípony":https://blog.nette.org/cs/predpony-a-pripony-do-nazvu-rozhrani-nepatri jako `Abstract`, `Interface` nebo `I`. - - -Zalamování a závorky -==================== - -Nette Coding Standard odpovídá PSR-12 (resp. PER Coding Style), v některých bodech jej doplňuje nebo upravuje: - -- arrow funkce se píší bez mezery před závorkou, tj. `fn($a) => $b` -- nevyžaduje se prázdný řádek mezi různými typy `use` import statements -- návratový typ funkce/metody a úvodní složená závorka jsou vždy na samostatných řádcích: - -```php - public function find( - string $dir, - array $options, - ): array - { - // tělo metody - } -``` - -Úvodní složená závorka na samostatném řádku je důležitá pro vizuální oddělení signatury funkce/metody od těla. Pokud je signatura na jednom řádku, je oddělení zřetelné (obrázek vlevo), pokud je na více řádcích, v PSR signatury a těla splývají (uprostřed), zatímco v Nette standardu jsou nadále oddělené (vpravo): - -[* new-line-after.webp *] - - -Bloky dokumentace (phpDoc) -========================== - -Hlavní pravidlo: Nikdy neduplikujte žádné informace v signatuře, jako je typ parametru nebo návratový typ, bez přidané hodnoty. - -Dokumentační blok pro definici třídy: - -- Začíná popisem třídy. -- Následuje prázdný řádek. -- Následují anotace `@property` (nebo `@property-read`, `@property-write`), jedna po druhé. Syntaxe je: anotace, mezera, typ, mezera, $jméno. -- Následují anotace `@method`, jedna po druhé. Syntaxe je: anotace, mezera, návratový typ, mezera, jméno(typ $param, ...). -- Anotace `@author` se vynechává. Autorství se uchovává v historii zdrojového kódu. -- Lze použít anotace `@internal` nebo `@deprecated`. - -```php -/** - * MIME message part. - * - * @property string $encoding - * @property-read array $headers - * @method string getSomething(string $name) - * @method static bool isEnabled() - */ -``` - -Dokumentační blok pro vlastnost, který obsahuje pouze anotaci `@var`, by měl být jednořádkový: - -```php -/** @var string[] */ -private array $name; -``` - -Dokumentační blok pro definici metody: - -- Začíná krátkým popisem metody. -- Žádný prázdný řádek. -- Anotace `@param` po jednotlivých řádcích. -- Anotace `@return`. -- Anotace `@throws`, jedna po druhé. -- Lze použít anotace `@internal` nebo `@deprecated`. - -Za každou anotací následuje jedna mezera, s výjimkou `@param`, za kterou pro lepší čitelnost následují dvě mezery. - -```php -/** - * Finds a file in directory. - * @param string[] $options - * @return string[] - * @throws DirectoryNotFoundException - */ -public function find(string $dir, array $options): array -``` - - -Globální funkce a konstanty -=========================== - -Globální funkce a konstanty se píší bez úvodního zpětného lomítka, tedy `count($arr)` nikoliv `\count($arr)`. Pro funkce, které umí PHP optimalizovat, uvedeme na začátku souboru `use function`, aby je kompilátor mohl přeložit efektivněji. Jedná se zejména o funkce jako `count`, `strlen`, `is_array`, `is_string`, `is_scalar`, `sprintf` aj. Funkce se uvádějí na jednom řádku, aby úvodní blok importů nebyl zbytečně velký: - -```php -use Nette; -use function count, is_array, is_scalar, sprintf; -``` - -Výjimečně takto uvádíme i konstanty, u kterých může znalost hodnoty posloužit kompilátoru: - -```php -use const PHP_OS_FAMILY; -``` - - -Tabulátory místo mezer -====================== - -Tabulátory mají oproti mezerám několik výhod: - -- velikost odstupu lze v editorech a na "webu":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size přizpůsobit -- nevnucují kódu uživatelovu preferenci velikosti odsazení, takže kód je lépe přenositelný -- lze je napsat jedním stiskem klávesy (kdekoli, nejen v editorech, které mění tabulátory na mezery) -- odsazování je jejich smyslem -- respektují potřeby zrakově postižených a nevidomých kolegů - -Používáním tabulátorů v našich projektech umožňujeme přizpůsobení šířky, které se může většině lidí zdát jako zbytečnost, ale pro lidi se zrakovým postižením je nezbytné. - -Pro nevidomé programátory, kteří používají braillské displeje, představuje každá mezera jednu braillskou buňku. Pokud je tedy výchozí odsazení 4 mezery, odsazení 3. úrovně plýtvá 12 cennými braillskými buňkami ještě před začátkem kódu. Na 40buňkovém displeji, který se u notebooků používá nejčastěji, je to více než čtvrtina dostupných buněk, které jsou promrhány bez jakékoliv informace. - - -{{priority: -1}} -{{maintitle: Kódovací standard Nette: pravidla pro PHP kód}} -{{description: Pravidla pro psaní kódu v Nette: pojmenovací konvence, zalamování a závorky, phpDoc, globální funkce a konstanty a tabulátory místo mezer.}} diff --git a/contributing/cs/documentation.texy b/contributing/cs/documentation.texy deleted file mode 100644 index 5e3754072b..0000000000 --- a/contributing/cs/documentation.texy +++ /dev/null @@ -1,71 +0,0 @@ -Jak přispět do dokumentace -************************** - -.[perex] -Přispívání do dokumentace je jednou z nejpřínosnějších činností, neboť pomáháte druhým porozumět frameworku. - - -Jak psát? ---------- - -Dokumentace je určena především lidem, kteří se s tématem seznamují. Proto by měla splňovat několik důležitých bodů: - -- Začněte od jednoduchého a obecného. K pokročilejším tématům přejděte až na konci -- Snažte se věc co nejlépe vysvětlit. Zkuste například téma nejprve vysvětlit kolegovi -- Uvádějte jen ty informace, které uživatel skutečně k danému tématu potřebuje vědět -- Ověřte si, že vaše informace jsou skutečně pravdivé. Každý kód otestujte -- Buďte struční - co napíšete, zkraťte na polovinu. A pak klidně ještě jednou -- Šetřete zvýrazňovači všeho druhu, od tučného písma po rámečky jako `.[note]` -- V kódech dodržujte [Coding Standard] - -Osvojte si také [syntax]. Pro náhled článku během jeho psaní můžete použít [editor s náhledem |https://editor.nette.org/]. - - -Jazykové mutace ---------------- - -Primárním jazykem je angličtina, vaše změny by tedy měly být v češtině i angličtině. Pokud angličtina není vaší silnou stránkou, použijte [DeepL Translator |https://www.deepl.com/translator] a ostatní vám text zkontrolují. - -Překlad do ostatních jazyků bude proveden automaticky po schválení a doladění vaší úpravy. - - -Triviální úpravy ----------------- - -Pro přispívání do dokumentace je nezbytné mít účet na [GitHub|https://github.com]. - -Nejjednodušší způsob, jak provést drobnou změnu v dokumentaci, je využít odkazy na konci každé stránky: - -- *Ukaž na GitHubu* otevře zdrojovou podobu dané stránky na GitHubu. Poté stačí stisknout tlačítko `E` a můžete začít editovat (je nutné být na GitHubu přihlášený) -- *Otevři náhled* otevře editor, kde rovnou vidíte i výslednou vizuální podobu - -Protože [editor s náhledem |https://editor.nette.org/] nemá možnost ukládat změny přímo na GitHub, je nutné po dokončení úprav zdrojový text zkopírovat do schránky (tlačítkem *Copy to clipboard*) a poté jej vložit do editoru na GitHubu. Pod editačním polem je formulář pro odeslání. Zde nezapomeňte stručně shrnout a vysvětlit důvod vaší úpravy. Po odeslání vznikne tzv. pull request (PR), který je možné dále editovat. - - -Větší úpravy ------------- - -Vhodnější, než využít rozhraní GitHubu, je být obeznámen se základy práce s verzovacím systémem Git. Pokud neovládáte práci s Gitem, můžete se podívat na průvodce [git - the simple guide |https://rogerdudler.github.io/git-guide/] a případně využít některého z mnoha [grafických klientů |https://git-scm.com/downloads/guis]. - -Dokumentaci upravujte tímto způsobem: - -1) na GitHubu si vytvořte [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] repozitáře [nette/docs |https://github.com/nette/docs] -2) tento repozitář [naklonujete |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] na svůj počítač -3) poté v [příslušné větvi |#Struktura dokumentace] proveďte změny -4) zkontrolujte přebytečné mezery v textu pomocí nástroje [Code-Checker |tools:code-checker] -5) změny uložte (commitněte) -6) pokud jste se změnami spokojeni, odešlete (pushněte) je na GitHub do vašeho forku -7) odtud je odešlete do repozitáře `nette/docs` vytvořením [pull request|https://help.github.com/articles/creating-a-pull-request] (PR) - -Je běžné, že budete dostávat komentáře s připomínkami. Sledujte navrhované změny a zapracujte je. Navrhované změny přidejte jako nové commity a znovu odešlete na GitHub. Nikdy nevytvářejte kvůli úpravě pull requestu nový pull request. - - -Struktura dokumentace ---------------------- - -Celá dokumentace je umístěna na GitHubu v repozitáři [nette/docs |https://github.com/nette/docs]. Aktuální verze je v masteru, starší verze jsou umístěny ve větvích jako `doc-3.x`, `doc-2.x`. - -Obsah každé větve se dělí do hlavních složek představujících jednotlivé oblasti dokumentace. Například `application/` odpovídá https://doc.nette.org/cs/application, `latte/` odpovídá https://latte.nette.org atd. Každá tato složka obsahuje podsložky představující jazykové mutace (`cs`, `en`, ...) a případně podsložku `files` s obrázky, které je možné do stránek v dokumentaci vkládat. - -{{maintitle: Jak přispět do dokumentace Nette}} -{{description: Přispívání do dokumentace pomáhá druhým porozumět frameworku. Jak psát pro nováčky, které jazykové verze upravovat a jak si text předem prohlédnout.}} diff --git a/contributing/cs/syntax.texy b/contributing/cs/syntax.texy deleted file mode 100644 index 6308019d94..0000000000 --- a/contributing/cs/syntax.texy +++ /dev/null @@ -1,145 +0,0 @@ -Dokumentační syntax -******************* - -Dokumentace používá Markdown & [Texy syntaxi |https://texy.deegee.dev/syntax] s některými rozšířeními. - - -Odkazy -====== - -Pro interní odkazy se používá zápis v hranatých závorkách `[odkaz]`. A to buď ve tvaru se svislítkem `[text odkazu |cíl odkazu]`, nebo zkráceně `[text odkazu]`, pokud je cíl shodný s textem (po transformaci na malá písmena a pomlčky): - -- `[Page name]` -> `<a href="/en/page-name">Page name</a>` -- `[link text |Page name]` -> `<a href="/en/page-name">link text</a>` - -Odkazovat můžeme do jiné jazykové mutace nebo do jiné sekce. Sekcí se rozumí Nette knihovna (např. `forms`, `latte` apod.) nebo speciální sekce jako `best-practices`, `quickstart` atd.: - -- `[cs:Page name]` -> `<a href="/cs/page-name">Page name</a>` (stejná sekce, jiný jazyk) -- `[tracy:Page name]` -> `<a href="//tracy.nette.org/en/page-name">Page name</a>` (jiná sekce, stejný jazyk) -- `[tracy:cs:Page name]` -> `<a href="//tracy.nette.org/cs/page-name">Page name</a>` (jiná sekce i jazyk) - -Pomocí `#` je také možné zacílit na konkrétní nadpis na stránce. - -- `[#Heading]` -> `<a href="#toc-heading">Heading</a>` (nadpis na aktuální stránce) -- `[Page name#Heading]` -> `<a href="/en/page-name#toc-heading">Page name</a>` - -Odkaz na úvodní stránku sekce: (`@home` je speciální výraz pro domovskou stránku sekce) - -- `[link text |@home]` -> `<a href="/en/">link text</a>` -- `[link text |tracy:]` -> `<a href="//tracy.nette.org/en/">link text</a>` - - -Odkazy do API dokumentace -------------------------- - -Vždy uvádějte pouze pomocí tohoto zápisu: - -- `[api:Nette\SmartObject]` -> [api:Nette\SmartObject] -- `[api:Nette\Forms\Form::setTranslator()]` -> [api:Nette\Forms\Form::setTranslator()] -- `[api:Nette\Forms\Form::$onSubmit]` -> [api:Nette\Forms\Form::$onSubmit] -- `[api:Nette\Forms\Form::Required]` -> [api:Nette\Forms\Form::Required] - -Plně kvalifikované názvy používejte pouze v první zmínce. Pro další odkazy použijte zjednodušený název: - -- `[Form::setTranslator() |api:Nette\Forms\Form::setTranslator()]` -> [Form::setTranslator() |api:Nette\Forms\Form::setTranslator()] - - -Odkazy do PHP dokumentace -------------------------- - -- `[php:substr]` -> [php:substr] - - -Zdrojový kód -============ - -Blok kódu začíná <code>```lang</code> a končí <code>```</code>. Podporované jazyky jsou `php`, `latte`, `neon`, `html`, `css`, `js` a `sql`. Pro odsazení vždy používejte tabulátory. - -``` - ```php - public function renderPage($id) - { - } - ``` -``` - -Můžete také uvést jméno souboru jako <code>```php .{file: ArrayTest.php}</code> a blok kódu se vykreslí tímto způsobem: - -```php .{file: ArrayTest.php} -public function renderPage($id) -{ -} -``` - - -Nadpisy -======= - -Nejvyšší nadpis (tedy název stránky) podtrhněte hvězdičkami. Pro oddělení sekcí používejte rovnítka. Nadpisy podtrhujte rovnítky a poté pomlčkami: - -``` -MVC Aplikace & presentery -************************* -... - - -Tvorba odkazů -============= -... - - -Odkazy v šablonách ------------------- -... -``` - - -Rámečky a styly -=============== - -Perex označíme třídou `.[perex]` .[perex] - -Poznámku označíme třídou `.[note]` .[note] - -Tip označíme třídou `.[tip]` .[tip] - -Varování označíme třídou `.[caution]` .[caution] - -Důraznější varování označíme třídou `.[warning]` .[warning] - -Číslo verze `.{data-version:2.4.10}` .{data-version:2.4.10} - -Třídy zapisujte před řádkem: - -``` -.[perex] -Tohle je perex. -``` - -Uvědomte si prosím, že rámečky jako `.[tip]` "tahají" oči, tudíž se používají pro zdůraznění, nikoliv pro méně podstatné informace. Proto jejich používáním maximálně šetřte. - - -Obsah -===== - -Obsah (odkazy v pravém menu) je automaticky generovaný pro všechny stránky, jejichž velikost přesáhne 4 000 bajtů, přičemž toto výchozí chování je možné upravit pomocí [#meta značky] `{{toc}}`. Text tvořící obsah se bere standardně přímo z textu nadpisů, ale pomocí modifikátoru `.{toc}` je možné zobrazit v obsahu jiný text, což se hodí hlavně pro delší nadpisy. - -``` - - -Dlouhý a inteligentní nadpis .{toc: Libovolný jiný text zobrazený v obsahu} -=========================================================================== -``` - - -Meta značky -=========== - -- nastavení vlastního názvu stránky (v `<title>` a drobečkové navigaci) `{{title: Jiný název}}` -- přesměrování `{{redirect: pla:cs}}` - viz [#odkazy] -- vynucení `{{toc}}` či zakázání `{{toc: no}}` automatického obsahu (boxík s odkazy na jednotlivé nadpisy) -- určení levého menu `{{leftbar: utils:@left-menu}}` či jeho vypnutí `{{leftbar: no}}` - -{{priority: -1}} -{{maintitle: Syntax dokumentace Nette: značkování stránek v Texy}} -{{description: Značkování dokumentace Nette: odkazy, zdrojový kód, nadpisy, rámečky a styly, obsah stránky a meta značky.}} diff --git a/contributing/de/@home.texy b/contributing/de/@home.texy deleted file mode 100644 index c4fcfeb89a..0000000000 --- a/contributing/de/@home.texy +++ /dev/null @@ -1,17 +0,0 @@ -Machen Sie bei Nette mit -************************ - -.[perex] -Erfahren Sie, wie Sie sich an unserem Open-Source-Projekt beteiligen können. Lernen Sie die Abläufe kennen, um zum Quellcode und zur Dokumentation beizutragen, und werden Sie Teil der Community von Entwicklern, die sich aktiv an der Verbesserung von Nette beteiligen. - - -**Code** - -- [Zum Code beitragen |code] -- [Coding Standard |coding-standard] - -**Dokumentation** - -- [Zur Dokumentation beitragen |documentation] -- [Dokumentationssyntax |syntax] -- "Vorschau-Editor":https://editor.nette.org diff --git a/contributing/de/@left-menu.texy b/contributing/de/@left-menu.texy deleted file mode 100644 index 53e21f1303..0000000000 --- a/contributing/de/@left-menu.texy +++ /dev/null @@ -1,18 +0,0 @@ -Code -**** -- [Zum Code beitragen |code] -- [Coding Standard |coding-standard] - -Dokumentation -************* -- [Zur Dokumentation beitragen |documentation] -- [Dokumentationssyntax |syntax] -- "Vorschau-Editor":https://editor.nette.org - - -Weiterführende Lektüre -********************** -- [Nette Dokumentation |nette:] -- [Werkzeuge |tools:] -- [Wer entwickelt Nette |https://nette.org/contributors] -- [Nette auf GitHub |https://github.com/nette] diff --git a/contributing/de/code.texy b/contributing/de/code.texy deleted file mode 100644 index f718bf8024..0000000000 --- a/contributing/de/code.texy +++ /dev/null @@ -1,107 +0,0 @@ -Zum Code beitragen -****************** - -.[perex] -Planen Sie, zum Nette Framework beizutragen, und müssen sich noch mit den Regeln und Abläufen vertraut machen? Dieser Leitfaden für Einsteiger führt Sie durch die Schritte, mit denen Sie wirksam Code beisteuern, mit Repositories arbeiten und Änderungen umsetzen. - - -Vorgehensweise -============== - -Um Code beizusteuern, brauchen Sie unbedingt ein Konto auf [GitHub|https://github.com] und Grundkenntnisse im Umgang mit dem Versionsverwaltungssystem Git. Wenn Sie sich mit Git nicht auskennen, können Sie sich [git - the simple guide|https://rogerdudler.github.io/git-guide/] ansehen und einen der vielen [grafischen Clients|https://git-scm.com/downloads/guis] in Betracht ziehen. - - -Vorbereitung der Umgebung und des Repositorys ---------------------------------------------- - -1) Erstellen Sie auf GitHub einen [Fork|https://help.github.com/en/github/getting-started-with-github/fork-a-repo] des [Repositorys des Pakets|www:packages], das Sie ändern möchten -2) [Klonen Sie|https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] dieses Repository auf Ihren Computer -3) Installieren Sie die Abhängigkeiten einschließlich [Nette Tester|tester:] mit dem Befehl `composer install` -4) Überprüfen Sie mit `composer tester`, dass die Tests laufen -5) Erstellen Sie einen [neuen Branch |#Neuer Branch] auf Basis der zuletzt veröffentlichten Version - - -Implementierung eigener Änderungen ----------------------------------- - -Jetzt können Sie Ihre eigenen Anpassungen am Code vornehmen: - -1) Implementieren Sie die gewünschten Änderungen und vergessen Sie die Tests nicht -2) Vergewissern Sie sich mit `composer tester`, dass die Tests erfolgreich durchlaufen -3) Prüfen Sie, ob der Code dem [Coding Standard |#Coding Standard] entspricht -4) Speichern (committen) Sie die Änderungen mit einer Beschreibung in [diesem Format |#Commit-Beschreibung] - -Sie können mehrere Commits anlegen, einen für jeden logischen Schritt. Jeder Commit sollte für sich genommen sinnvoll sein. - - -Senden der Änderungen ---------------------- - -Sobald Sie mit den Änderungen zufrieden sind, können Sie sie einsenden: - -1) Pushen Sie die Änderungen zu GitHub in Ihren Fork -2) Senden Sie sie von dort in das Nette-Repository, indem Sie einen [Pull Request|https://help.github.com/articles/creating-a-pull-request] (PR) erstellen -3) Geben Sie in der Beschreibung [ausreichend Informationen |#Beschreibung des Pull Requests] an - - -Einarbeitung von Anmerkungen ----------------------------- - -Ihre Commits sind nun für andere sichtbar. Üblicherweise erhalten Sie Kommentare mit Vorschlägen: - -1) Behalten Sie die vorgeschlagenen Änderungen im Blick -2) Arbeiten Sie sie als neue Commits ein oder [führen Sie sie mit den vorherigen zusammen|https://help.github.com/en/github/using-git/about-git-rebase] -3) Senden Sie die Commits erneut zu GitHub, sie erscheinen dann automatisch im Pull Request - -Erstellen Sie niemals einen neuen Pull Request, um einen bestehenden zu ändern. - - -Dokumentation -------------- - -Wenn Sie eine Funktionalität geändert oder eine neue hinzugefügt haben, vergessen Sie nicht, sie auch [in die Dokumentation aufzunehmen|documentation]. - - -Neuer Branch -============ - -Nehmen Sie Änderungen nach Möglichkeit gegen die zuletzt veröffentlichte Version vor, also gegen den letzten Tag im Branch. Für den Tag `v3.2.1` erstellen Sie den Branch mit diesem Befehl: - -```shell -git checkout -b new_branch_name v3.2.1 -``` - - -Coding Standard -=============== - -Ihr Code muss dem [Coding Standard|coding-standard] entsprechen, der im Nette Framework verwendet wird. Zum Prüfen und automatischen Korrigieren Ihres Codes dient das Werkzeug [Nette Coding Standard |tools:coding-standard], wo Sie auch eine Anleitung zu Installation und Verwendung finden. - - -Commit-Beschreibung -=================== - -In Nette haben die Betreffzeilen von Commits das folgende Format: `Presenter: fixed AJAX detection [Closes #69]` - -- Bereich gefolgt von einem Doppelpunkt -- Zweck des Commits in der Vergangenheitsform; beginnen Sie nach Möglichkeit mit Wörtern wie: "added (neue Funktion)", "fixed (Korrektur)", "refactored (Codeänderung ohne Verhaltensänderung)", "changed", "removed" -- Bricht der Commit die Rückwärtskompatibilität, ergänzen Sie "BC break" -- Ein etwaiger Bezug zum Issue-Tracker wie `(#123)` oder `[Closes #69]` -- Nach der Betreffzeile kann eine Leerzeile und danach eine ausführlichere Beschreibung folgen, einschließlich etwa Links ins Forum - - -Beschreibung des Pull Requests -============================== - -Beim Erstellen eines Pull Requests können Sie in der Oberfläche von GitHub einen Titel und eine Beschreibung eingeben. Geben Sie einen treffenden Titel an und schreiben Sie in die Beschreibung möglichst viele Informationen über die Gründe für Ihre Änderung. - -Geben Sie im Kopf außerdem an, ob es sich um eine neue Funktion oder um eine Fehlerkorrektur handelt und ob die Rückwärtskompatibilität gestört werden kann (BC break). Gibt es ein zugehöriges Issue, verlinken Sie es, damit es mit der Annahme des Pull Requests geschlossen wird. - -``` -- bug fix / new feature? <!-- #issue numbers, if any --> -- BC break? yes/no -- doc PR: nette/docs#? <!-- highly welcome, see https://nette.org/en/writing --> -``` - - -{{priority: -1}} diff --git a/contributing/de/coding-standard.texy b/contributing/de/coding-standard.texy deleted file mode 100644 index db83c6d9ef..0000000000 --- a/contributing/de/coding-standard.texy +++ /dev/null @@ -1,148 +0,0 @@ -Coding Standard -*************** - -.[perex] -Dieses Dokument beschreibt die Regeln und Empfehlungen für die Entwicklung von Nette. Wenn Sie Code zu Nette beisteuern, müssen Sie sich an sie halten. Am einfachsten gelingt das, indem Sie den bestehenden Code nachahmen. Das Ziel ist, dass der gesamte Code so aussieht, als hätte ihn eine einzige Person geschrieben. - -Der Nette Coding Standard entspricht dem [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/] mit zwei wesentlichen Ausnahmen: Zum Einrücken verwendet er [Tabulatoren statt Leerzeichen |#Tabulatoren statt Leerzeichen] und für Klassenkonstanten [PascalCase|https://blog.nette.org/de/for-less-screaming-in-the-code]. - -.[tip] -Viele dieser Regeln kann das Werkzeug [Nette Coding Standard |tools:coding-standard] automatisch prüfen und korrigieren, Sie müssen sie also nicht von Hand kontrollieren. - - -Allgemeine Regeln -================= - -- Jede PHP-Datei muss `declare(strict_types=1)` enthalten -- Zur besseren Lesbarkeit werden Methoden durch zwei Leerzeilen getrennt -- Der Grund für die Verwendung des Fehlerunterdrückungsoperators (`@`) muss dokumentiert werden: `@mkdir($dir); // @ - directory may exist` -- Wird ein schwach typisierter Vergleichsoperator verwendet (also `==`, `!=`, ...), muss die Absicht dokumentiert werden: `// == to accept null` -- Mehrere Exception-Klassen dürfen Sie in eine einzige Datei namens `exceptions.php` schreiben, mehrere Enums in `enums.php` -- Bei Interfaces wird die Sichtbarkeit von Methoden nicht angegeben, denn sie sind immer public -- Jede Property, jeder Rückgabewert und jeder Parameter muss einen Typ angegeben haben. Umgekehrt geben wir bei finalen Konstanten den Typ nie an, weil er offensichtlich ist -- Zum Begrenzen von Strings sollen einfache Anführungszeichen verwendet werden, außer wenn das Literal selbst Apostrophe enthält - - -Benennungskonventionen -====================== - -- Vermeiden Sie Abkürzungen, sofern der volle Name nicht zu ausufernd wird -- Verwenden Sie bei zweibuchstabigen Abkürzungen Großbuchstaben, bei längeren Abkürzungen PascalCase/camelCase -- Verwenden Sie für den Klassennamen ein Substantiv oder eine Substantivgruppe -- Klassennamen müssen nicht nur das Besondere (`Array`), sondern auch das Allgemeine (`ArrayIterator`) enthalten. PHP-Attribute sind eine Ausnahme -- "Klassenkonstanten und Enums sollten PascalCaps verwenden":https://blog.nette.org/de/for-less-screaming-in-the-code -- "Interfaces und abstrakte Klassen sollten keine Präfixe oder Suffixe enthalten":https://blog.nette.org/de/prefixes-and-suffixes-do-not-belong-in-interface-names wie `Abstract`, `Interface` oder `I` - - -Umbrüche und Klammern -===================== - -Der Nette Coding Standard entspricht PSR-12 (bzw. dem PER Coding Style), präzisiert oder verändert ihn aber in einigen Punkten: - -- Arrow-Funktionen werden ohne Leerzeichen vor der Klammer geschrieben, also `fn($a) => $b` -- Zwischen verschiedenen Arten von `use`-Import-Anweisungen ist keine Leerzeile nötig -- Der Rückgabetyp einer Funktion/Methode und die öffnende geschweifte Klammer stehen immer in getrennten Zeilen: - -```php - public function find( - string $dir, - array $options, - ): array - { - // Rumpf der Methode - } -``` - -Die öffnende geschweifte Klammer in einer eigenen Zeile ist wichtig, um die Signatur der Funktion/Methode optisch vom Rumpf zu trennen. Steht die Signatur in einer Zeile, ist die Trennung deutlich (Bild links), erstreckt sie sich über mehrere Zeilen, verschmelzen Signatur und Rumpf bei PSR miteinander (Mitte), während sie im Nette-Standard weiterhin getrennt bleiben (rechts): - -[* new-line-after.webp *] - - -Dokumentationsblöcke (phpDoc) -============================= - -Die Hauptregel: **Wiederholen Sie niemals** Informationen aus der Signatur wie Parametertyp oder Rückgabetyp, ohne einen Mehrwert zu schaffen. - -Dokumentationsblock für eine Klassendefinition: - -- Beginnt mit einer Beschreibung der Klasse -- Danach folgt eine Leerzeile -- Danach folgen Annotationen `@property` (oder `@property-read`, `@property-write`), eine pro Zeile. Syntax: Annotation, Leerzeichen, Typ, Leerzeichen, `$name` -- Danach folgen Annotationen `@method`, eine pro Zeile. Syntax: Annotation, Leerzeichen, Rückgabetyp, Leerzeichen, `name(type $param, ...)` -- Die Annotation `@author` entfällt. Die Autorschaft wird in der Historie des Quellcodes geführt -- Die Annotationen `@internal` oder `@deprecated` dürfen verwendet werden - -```php -/** - * MIME message part. - * - * @property string $encoding - * @property-read array $headers - * @method string getSomething(string $name) - * @method static bool isEnabled() - */ -``` - -Ein Dokumentationsblock für eine Property, der nur die Annotation `@var` enthält, soll einzeilig sein: - -```php -/** @var string[] */ -private array $name; -``` - -Dokumentationsblock für eine Methodendefinition: - -- Beginnt mit einer kurzen Beschreibung der Methode -- Keine Leerzeile -- Annotationen `@param`, eine pro Zeile -- Annotation `@return` -- Annotationen `@throws`, eine pro Zeile -- Die Annotationen `@internal` oder `@deprecated` dürfen verwendet werden - -Nach jeder Annotation folgt ein Leerzeichen, außer bei `@param`, nach der zur besseren Lesbarkeit zwei Leerzeichen folgen. - -```php -/** - * Finds a file in directory. - * @param string[] $options - * @return string[] - * @throws DirectoryNotFoundException - */ -public function find(string $dir, array $options): array -``` - - -Globale Funktionen und Konstanten -================================= - -Globale Funktionen und Konstanten werden ohne führenden Backslash geschrieben, also `count($arr)` und nicht `\count($arr)`. Bei Funktionen, die PHP optimieren kann, ergänzen Sie am Anfang der Datei `use function`, damit der Compiler sie effizienter übersetzen kann. Dazu gehören Funktionen wie `count`, `strlen`, `is_array`, `is_string`, `is_scalar`, `sprintf` usw. Die Funktionen werden in einer einzigen Zeile aufgeführt, damit der Import-Block kompakt bleibt: - -```php -use Nette; -use function count, is_array, is_scalar, sprintf; -``` - -Gelegentlich importieren wir auch Konstanten, bei denen die Kenntnis ihres Wertes dem Compiler helfen kann: - -```php -use const PHP_OS_FAMILY; -``` - - -Tabulatoren statt Leerzeichen -============================= - -Tabulatoren haben gegenüber Leerzeichen mehrere Vorteile: - -- Die Größe der Einrückung lässt sich in Editoren und im "Web":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size einstellen -- Sie zwingen dem Code nicht die bevorzugte Einrückungsgröße des Benutzers auf, wodurch der Code portabler wird -- Sie lassen sich mit einem einzigen Tastendruck eingeben (überall, nicht nur in Editoren, die Tabulatoren in Leerzeichen umwandeln) -- Einrücken ist ihr Zweck -- Sie berücksichtigen die Bedürfnisse sehbehinderter und blinder Kollegen - -Indem wir in unseren Projekten Tabulatoren verwenden, ermöglichen wir eine Anpassung der Breite, die den meisten Menschen unnötig erscheinen mag, für Menschen mit Sehbehinderung aber wesentlich ist. - -Für blinde Programmierer, die Braillezeilen verwenden, steht jedes Leerzeichen für eine Braillezelle. Beträgt die Standardeinrückung also 4 Leerzeichen, verschwendet eine Einrückung der 3. Ebene 12 wertvolle Braillezellen, bevor der Code überhaupt beginnt. Auf einer Zeile mit 40 Zellen, wie sie bei Laptops am häufigsten ist, ist das mehr als ein Viertel der verfügbaren Zellen, das ohne jede Information verschwendet wird. - - -{{priority: -1}} diff --git a/contributing/de/documentation.texy b/contributing/de/documentation.texy deleted file mode 100644 index 62fdf843b6..0000000000 --- a/contributing/de/documentation.texy +++ /dev/null @@ -1,68 +0,0 @@ -Zur Dokumentation beitragen -*************************** - -.[perex] -Ein Beitrag zur Dokumentation gehört zu den wertvollsten Tätigkeiten überhaupt, denn er hilft anderen dabei, das Framework zu verstehen. - - -Wie schreibt man? ------------------ - -Die Dokumentation ist in erster Linie für Menschen gedacht, die das Thema neu kennenlernen. Deshalb sollte sie einige wichtige Punkte erfüllen: - -- Beginnen Sie mit einfachen und allgemeinen Begriffen. Zu fortgeschritteneren Themen gehen Sie erst zum Schluss über. -- Versuchen Sie, das Thema so verständlich wie möglich zu erklären. Erklären Sie es zum Beispiel zuerst einem Kollegen. -- Geben Sie nur die Informationen an, die der Benutzer für das jeweilige Thema wirklich braucht. -- Überprüfen Sie, dass Ihre Informationen zutreffen. Testen Sie jedes Stück Code. -- Fassen Sie sich kurz - halbieren Sie, was Sie geschrieben haben. Und dann ruhig noch einmal. -- Gehen Sie sparsam mit Hervorhebungen um, von fettem Text bis zu Rahmen wie `.[note]`. -- Halten Sie sich in den Codebeispielen an den [Coding Standard|coding-standard]. - -Machen Sie sich außerdem mit der [Syntax |syntax] vertraut. Für eine Vorschau des Artikels beim Schreiben können Sie den [Vorschau-Editor |https://editor.nette.org/] verwenden. - - -Sprachversionen ---------------- - -Englisch ist die Hauptsprache, Ihre Änderungen sollten also idealerweise auf Englisch sein. Wenn Englisch nicht Ihre Stärke ist, nutzen Sie den [DeepL Translator |https://www.deepl.com/translator], und andere werden Ihren Text durchsehen. - -Die Übersetzung in die anderen Sprachen erfolgt automatisch, nachdem Ihre Änderung angenommen und abgeschlossen ist. - - -Triviale Änderungen -------------------- - -Um zur Dokumentation beizutragen, brauchen Sie ein Konto auf [GitHub |https://github.com]. - -Am einfachsten nehmen Sie eine kleine Änderung in der Dokumentation über die Links am Ende jeder Seite vor: - -- *Auf GitHub anzeigen* öffnet die Quellfassung der Seite auf GitHub. Dann genügt die Taste `E`, um mit dem Bearbeiten zu beginnen (Sie müssen bei GitHub angemeldet sein). -- *Vorschau öffnen* öffnet einen Editor, in dem Sie sofort das endgültige Aussehen sehen. - -Da der [Vorschau-Editor |https://editor.nette.org/] Änderungen nicht direkt auf GitHub speichern kann, müssen Sie den Quelltext nach dem Bearbeiten in die Zwischenablage kopieren (über die Schaltfläche *In die Zwischenablage kopieren*) und ihn dann in den Editor auf GitHub einfügen. Unter dem Bearbeitungsfeld befindet sich ein Formular zum Absenden. Vergessen Sie dort nicht, kurz zusammenzufassen und zu begründen, warum Sie die Änderung vorgenommen haben. Nach dem Absenden entsteht ein Pull Request (PR), der sich weiter bearbeiten lässt. - - -Größere Änderungen ------------------- - -Statt sich allein auf die Oberfläche von GitHub zu verlassen, ist es besser, die Grundlagen im Umgang mit dem Versionsverwaltungssystem Git zu beherrschen. Wenn Sie sich mit Git nicht auskennen, können Sie sich [git - the simple guide |https://rogerdudler.github.io/git-guide/] ansehen und einen der vielen verfügbaren [grafischen Clients |https://git-scm.com/downloads/guis] in Betracht ziehen. - -Die Dokumentation bearbeiten Sie so: - -1) Erstellen Sie auf GitHub einen [Fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] des Repositorys [nette/docs |https://github.com/nette/docs]. -2) [Klonen Sie |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] dieses Repository auf Ihren Computer. -3) Nehmen Sie die Änderungen dann im [passenden Branch |#Struktur der Dokumentation] vor. -4) Prüfen Sie den Text mit dem Werkzeug [Code-Checker |tools:code-checker] auf überflüssige Leerzeichen. -5) Speichern (committen) Sie die Änderungen. -6) Wenn Sie mit den Änderungen zufrieden sind, pushen Sie sie zu GitHub in Ihren Fork. -7) Senden Sie sie von dort in das Repository `nette/docs`, indem Sie einen [Pull Request|https://help.github.com/articles/creating-a-pull-request] (PR) erstellen. - -Üblicherweise erhalten Sie Kommentare mit Vorschlägen. Behalten Sie die vorgeschlagenen Änderungen im Blick und arbeiten Sie sie ein. Fügen Sie die vorgeschlagenen Änderungen als neue Commits hinzu und pushen Sie sie erneut zu GitHub. Erstellen Sie niemals einen neuen Pull Request, nur um einen bestehenden zu ändern. - - -Struktur der Dokumentation --------------------------- - -Die gesamte Dokumentation liegt auf GitHub im Repository [nette/docs |https://github.com/nette/docs]. Die aktuelle Fassung befindet sich im Branch `master`, ältere Versionen in Branches wie `doc-3.x` oder `doc-2.x`. - -Der Inhalt jedes Branches ist in Hauptordner unterteilt, die den einzelnen Bereichen der Dokumentation entsprechen. So entspricht etwa `application/` der Adresse `https://doc.nette.org/en/application`, `latte/` der Adresse `https://latte.nette.org` usw. Jeder dieser Ordner enthält Unterordner für die Sprachversionen (`cs`, `en`, ...) und optional einen Unterordner `files` mit Bildern, die sich in die Seiten der Dokumentation einbinden lassen. diff --git a/contributing/de/syntax.texy b/contributing/de/syntax.texy deleted file mode 100644 index b3e446102c..0000000000 --- a/contributing/de/syntax.texy +++ /dev/null @@ -1,143 +0,0 @@ -Dokumentationssyntax -******************** - -Die Dokumentation verwendet Markdown & die [Texy-Syntax |https://texy.deegee.dev/syntax] mit einigen Erweiterungen. - - -Links -===== - -Für interne Links wird die Notation in eckigen Klammern `[Link]` verwendet, und zwar entweder in der Form mit senkrechtem Strich `[Linktext |Linkziel]` oder verkürzt `[Linktext]`, wenn das Ziel mit dem Text übereinstimmt (nach der Umwandlung in Kleinbuchstaben und Bindestriche): - -- `[Page name]` -> `<a href="/en/page-name">Page name</a>` -- `[link text |Page name]` -> `<a href="/en/page-name">link text</a>` - -Wir können auf eine andere Sprachversion oder einen anderen Abschnitt verlinken. Ein Abschnitt ist eine Nette-Bibliothek (z. B. `forms`, `latte` usw.) oder ein besonderer Abschnitt wie `best-practices`, `quickstart` usw.: - -- `[cs:Page name]` -> `<a href="/cs/page-name">Page name</a>` (gleicher Abschnitt, andere Sprache) -- `[tracy:Page name]` -> `<a href="//tracy.nette.org/en/page-name">Page name</a>` (anderer Abschnitt, gleiche Sprache) -- `[tracy:cs:Page name]` -> `<a href="//tracy.nette.org/cs/page-name">Page name</a>` (anderer Abschnitt und andere Sprache) - -Mit `#` lässt sich außerdem eine bestimmte Überschrift auf der Seite ansteuern. - -- `[#Heading]` -> `<a href="#toc-heading">Heading</a>` (Überschrift auf der aktuellen Seite) -- `[Page name#Heading]` -> `<a href="/en/page-name#toc-heading">Page name</a>` - -Link auf die Startseite des Abschnitts: (`@home` ist ein besonderer Ausdruck für die Startseite des Abschnitts) - -- `[link text |@home]` -> `<a href="/en/">link text</a>` -- `[link text |tracy:]` -> `<a href="//tracy.nette.org/en/">link text</a>` - - -Links zur API-Dokumentation ---------------------------- - -Verwenden Sie stets die folgende Notation: - -- `[api:Nette\SmartObject]` -> [api:Nette\SmartObject] -- `[api:Nette\Forms\Form::setTranslator()]` -> [api:Nette\Forms\Form::setTranslator()] -- `[api:Nette\Forms\Form::$onSubmit]` -> [api:Nette\Forms\Form::$onSubmit] -- `[api:Nette\Forms\Form::Required]` -> [api:Nette\Forms\Form::Required] - -Verwenden Sie den vollständigen Namen nur bei der ersten Erwähnung. Bei weiteren Links verwenden Sie eine vereinfachte Form: - -- `[Form::setTranslator() |api:Nette\Forms\Form::setTranslator()]` -> [Form::setTranslator() |api:Nette\Forms\Form::setTranslator()] - - -Links zur PHP-Dokumentation ---------------------------- - -- `[php:substr]` -> [php:substr] - - -Quellcode -========= - -Ein Codeblock beginnt mit <code>```lang</code> und endet mit <code>```</code>. Unterstützte Sprachen sind `php`, `latte`, `neon`, `html`, `css`, `js` und `sql`. Verwenden Sie zum Einrücken immer Tabulatoren. - -``` - ```php - public function renderPage($id) - { - } - ``` -``` - -Sie können auch den Dateinamen angeben als <code>```php .{file: ArrayTest.php}</code>, der Codeblock wird dann so dargestellt: - -```php .{file: ArrayTest.php} -public function renderPage($id) -{ -} -``` - - -Überschriften -============= - -Die oberste Überschrift (also den Seitennamen) unterstreichen Sie mit Sternchen (`*`). Zum Trennen von Abschnitten verwenden Sie Gleichheitszeichen (`=`). Überschriften unterstreichen Sie zuerst mit Gleichheitszeichen (`=`) und danach mit Bindestrichen (`-`): - -``` -MVC-Anwendungen & Presenter -*************************** -... - - -Erstellen von Links -=================== -... - - -Links in Templates ------------------- -... -``` - - -Rahmen und Stile -================ - -Den Perex kennzeichnen wir mit der Klasse `.[perex]` .[perex] - -Einen Hinweis kennzeichnen wir mit der Klasse `.[note]` .[note] - -Einen Tipp kennzeichnen wir mit der Klasse `.[tip]` .[tip] - -Eine Warnung kennzeichnen wir mit der Klasse `.[caution]` .[caution] - -Eine eindringlichere Warnung kennzeichnen wir mit der Klasse `.[warning]` .[warning] - -Die Versionsnummer `.{data-version:2.4.10}` .{data-version:2.4.10} - -Die Klassen schreiben Sie vor die Zeile, für die sie gelten: - -``` -.[perex] -Das ist der Perex. -``` - -Bedenken Sie bitte, dass Rahmen wie `.[tip]` die Aufmerksamkeit auf sich ziehen und deshalb zum Hervorheben wichtiger Informationen dienen, nicht für weniger wesentliche Angaben. Gehen Sie deshalb sparsam mit ihnen um. - - -Inhaltsverzeichnis -================== - -Ein Inhaltsverzeichnis (die Links im rechten Menü) wird automatisch für alle Seiten erzeugt, deren Größe 4 000 Bytes übersteigt, wobei sich dieses Standardverhalten über den [Meta-Tag |#Meta-Tags] `{{toc}}` anpassen lässt. Der Text des Inhaltsverzeichnisses wird standardmäßig direkt den Überschriften entnommen, mit dem Modifikator `.{toc}` lässt sich im Inhaltsverzeichnis aber ein anderer Text anzeigen, was sich vor allem bei längeren Überschriften anbietet. - -``` - - -Lange und intelligente Überschrift .{toc: Ein anderer im Inhaltsverzeichnis angezeigter Text} -============================================================================================= -``` - - -Meta-Tags -========= - -- Einen eigenen Seitentitel setzen (in `<title>` und in der Brotkrumennavigation): `{{title: Another name}}` -- Weiterleitung: `{{redirect: pla:cs}}` - siehe [#Links] -- Das automatische Inhaltsverzeichnis (Rahmen mit Links zu den Überschriften) erzwingen `{{toc}}` oder abschalten `{{toc: no}}`. -- Das linke Menü setzen `{{leftbar: utils:@left-menu}}` oder abschalten `{{leftbar: no}}`. - -{{priority: -1}} diff --git a/contributing/en/@home.texy b/contributing/en/@home.texy deleted file mode 100644 index 73cb24e686..0000000000 --- a/contributing/en/@home.texy +++ /dev/null @@ -1,20 +0,0 @@ -Become a Contributor to Nette -***************************** - -.[perex] -Find out how you can get involved in our open source project. Learn the procedures for contributing to the source code and documentation, and become part of the community of developers who actively participate in improving Nette. - - -**Code** - -- [Contributing to Code |code] -- [Coding Standards |coding-standard] - -**Documentation** - -- [Contributing to Documentation |documentation] -- [Documentation Syntax |syntax] -- "Preview editor":https://editor.nette.org - -{{maintitle: Contributing to Nette: Become a Contributor}} -{{description: Find out how to get involved in the Nette open source project: contribute to the source code and documentation and join the community of developers.}} diff --git a/contributing/en/@left-menu.texy b/contributing/en/@left-menu.texy deleted file mode 100644 index 3e010b2217..0000000000 --- a/contributing/en/@left-menu.texy +++ /dev/null @@ -1,18 +0,0 @@ -Code -**** -- [Contributing to Code |code] -- [Coding Standards |coding-standard] - -Documentation -************* -- [Contributing to Documentation |documentation] -- [Documentation Syntax |syntax] -- "Preview Editor":https://editor.nette.org - - -Further Reading -*************** -- [Nette Documentation |nette:] -- [Tools |tools:] -- [Who Creates Nette |https://nette.org/contributors] -- [Nette on GitHub |https://github.com/nette] diff --git a/contributing/en/code.texy b/contributing/en/code.texy deleted file mode 100644 index 76c94255db..0000000000 --- a/contributing/en/code.texy +++ /dev/null @@ -1,108 +0,0 @@ -Contributing to Code -******************** - -.[perex] -Are you planning to contribute to the Nette Framework and need to familiarize yourself with the rules and procedures? This beginner's guide will walk you through the steps to effectively contribute code, work with repositories, and implement changes. - - -Procedure -========= - -To contribute code, it is essential to have an account on [GitHub|https://github.com] and be familiar with the basics of working with the Git version control system. If you are not familiar with Git, you can check out the [git - the simple guide|https://rogerdudler.github.io/git-guide/] and consider using one of the many [graphical clients|https://git-scm.com/downloads/guis]. - - -Preparing the Environment and Repository ----------------------------------------- - -1) On GitHub, create a [fork|https://help.github.com/en/github/getting-started-with-github/fork-a-repo] of the [package repository|www:packages] that you intend to modify -2) [Clone|https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] this repository to your computer -3) Install the dependencies, including [Nette Tester|tester:], using the `composer install` command -4) Verify that the tests are working by running `composer tester` -5) Create a [#new branch] based on the latest released version - - -Implementing Your Own Changes ------------------------------ - -Now you can make your own code adjustments: - -1) Implement the desired changes and do not forget about the tests -2) Make sure the tests run successfully using `composer tester` -3) Check if the code meets the [#coding standards] using `dresscode check` -4) Save (commit) the changes with a description in [this format |#Commit Description] - -You can create multiple commits, one for each logical step. Each commit should be meaningful on its own. - - -Submitting Changes ------------------- - -Once you are satisfied with the changes, you can submit them: - -1) Push the changes to GitHub to your fork -2) From there, submit them to the Nette repository by creating a [pull request|https://help.github.com/articles/creating-a-pull-request] (PR) -3) Provide [sufficient information |#Pull Request Description] in the description - - -Incorporating Feedback ----------------------- - -Your commits are now visible to others. It is common to receive comments with suggestions: - -1) Keep track of the proposed changes -2) Incorporate them as new commits or [merge them with previous ones|https://help.github.com/en/github/using-git/about-git-rebase] -3) Resubmit the commits to GitHub, and they will automatically appear in the pull request - -Never create a new pull request to modify an existing one. - - -Documentation -------------- - -If you have changed functionality or added a new one, don't forget to [add it to the documentation|documentation] as well. - - -New Branch -========== - -If possible, make changes against the latest released version, i.e., the last tag in the branch. For the tag `v3.2.1`, create a branch using this command: - -```shell -git checkout -b new_branch_name v3.2.1 -``` - - -Coding Standards -================ - -Your code must meet the [coding standard] used in the Nette Framework. To check and automatically fix your code, use [DressCode |tools:coding-standard], where you will also find installation and usage instructions. - - -Commit Description -================== - -In Nette, commit subjects have the following format: `Presenter: fixed AJAX detection [Closes #69]` - -- Area followed by a colon -- Purpose of the commit in the past tense; if possible, start with words like: "added (new feature)", "fixed (correction)", "refactored (code change without behavior change)", "changed", "removed" -- If the commit breaks backward compatibility, add "BC break" -- Any link to the issue tracker, such as `(#123)` or `[Closes #69]` -- After the subject, there can be one blank line followed by a more detailed description, including, for example, links to the forum - - -Pull Request Description -======================== - -When creating a pull request, the GitHub interface will allow you to enter a title and description. Provide a concise title and include as much information as possible in the description about the reasons for your change. - -Also, specify in the header whether it is a new feature or a bug fix and whether it may cause backward compatibility issues (BC break). If there is a related issue, link to it so that it will be closed upon approval of the pull request. - -``` -- bug fix / new feature? <!-- #issue numbers, if any --> -- BC break? yes/no -``` - - -{{priority: -1}} -{{maintitle: Contributing Code to Nette: Pull Requests Step by Step}} -{{description: A beginner's guide to contributing code to Nette: repositories, a new branch, coding standards, and how to describe commits and pull requests.}} diff --git a/contributing/en/coding-standard.texy b/contributing/en/coding-standard.texy deleted file mode 100644 index 373f4cc4e6..0000000000 --- a/contributing/en/coding-standard.texy +++ /dev/null @@ -1,150 +0,0 @@ -Coding Standard -*************** - -.[perex] -This document describes the rules and recommendations for developing Nette. When contributing code to Nette, you must follow them. The easiest way to do this is to imitate the existing code. The goal is to make all the code look as if it were written by one person. - -Nette Coding Standard corresponds to [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/] with two main exceptions: it uses [#tabs instead of spaces] for indentation, and uses [PascalCase for class constants|https://blog.nette.org/en/for-less-screaming-in-the-code]. - -.[tip] -Many of these rules can be automatically checked and fixed by [DressCode |tools:coding-standard], so you don't have to check them by hand. It knows this standard under the name `nette`: put `use: [nette]` into `dresscode.neon`, and `dresscode check` reports where your code departs from it. - - -General Rules -============= - -- Every PHP file must contain `declare(strict_types=1)` -- Two empty lines are used to separate methods for better readability -- The reason for using the shut-up operator (`@`) must be documented: `@mkdir($dir); // @ - directory may exist` -- If a weakly typed comparison operator is used (i.e., `==`, `!=`, ...), the intention must be documented: `// == to accept null` -- You can write multiple exception classes into a single file named `exceptions.php`, and multiple enums into `enums.php` -- The visibility of methods is not specified for interfaces because they are always public -- Each property, return value, and parameter must have a type specified. Conversely, for final constants, we never specify the type because it is obvious -- Single quotes should be used to delimit strings, except when the literal itself contains apostrophes - - -Naming Conventions -================== - -- Avoid using abbreviations unless the full name is excessive -- Use uppercase for two-letter abbreviations, and PascalCase/camelCase for longer abbreviations -- Use a noun or noun phrase for the class name -- Class names must contain not only specificity (`Array`) but also generality (`ArrayIterator`). PHP attributes are an exception -- "Class constants and enums should use PascalCaps":https://blog.nette.org/en/for-less-screaming-in-the-code -- "Interfaces and abstract classes should not contain prefixes or suffixes":https://blog.nette.org/en/prefixes-and-suffixes-do-not-belong-in-interface-names like `Abstract`, `Interface` or `I` - - -Wrapping and Braces -=================== - -Nette Coding Standard corresponds to PSR-12 (or PER Coding Style), but specifies or modifies it in some points: - -- Arrow functions are written without a space before the parenthesis, i.e., `fn($a) => $b` -- No empty line is required between different types of `use` import statements -- The return type of a function/method and the opening curly brace are always on separate lines: - -```php - public function find( - string $dir, - array $options, - ): array - { - // method body - } -``` - -The opening curly brace on a separate line is important for visually separating the function/method signature from the body. If the signature is on one line, the separation is clear (image on the left). If it's on multiple lines, in PSR the signature and body blend together (middle), while in the Nette standard they remain separated (right): - -[* new-line-after.webp *] - - -Documentation Blocks (phpDoc) -============================= - -The main rule: **Never duplicate** any signature information like parameter type or return type without adding value. - -Documentation block for a class definition: - -- Starts with a description of the class -- Followed by an empty line -- Followed by `@property` (or `@property-read`, `@property-write`) annotations, one per line. Syntax: annotation, space, type, space, `$name` -- Followed by `@method` annotations, one per line. Syntax: annotation, space, return type, space, `name(type $param, ...)` -- The `@author` annotation is omitted. Authorship is kept in the source code history -- The `@internal` or `@deprecated` annotations can be used - -```php -/** - * MIME message part. - * - * @property string $encoding - * @property-read array $headers - * @method string getSomething(string $name) - * @method static bool isEnabled() - */ -``` - -A documentation block for a property containing only the `@var` annotation should be on a single line: - -```php -/** @var string[] */ -private array $name; -``` - -Documentation block for a method definition: - -- Starts with a short description of the method -- No empty line -- `@param` annotations, one per line -- `@return` annotation -- `@throws` annotations, one per line -- The `@internal` or `@deprecated` annotations can be used - -Every annotation is followed by one space, except for `@param`, which is followed by two spaces for better readability. - -```php -/** - * Finds a file in directory. - * @param string[] $options - * @return string[] - * @throws DirectoryNotFoundException - */ -public function find(string $dir, array $options): array -``` - - -Global Functions and Constants -============================== - -Global functions and constants are written without a leading backslash, i.e., `count($arr)` not `\count($arr)`. For functions that PHP can optimize, add `use function` at the beginning of the file so the compiler can translate them more efficiently. These include functions like `count`, `strlen`, `is_array`, `is_string`, `is_scalar`, `sprintf`, etc. Functions are listed on a single line to keep the import block compact: - -```php -use Nette; -use function count, is_array, is_scalar, sprintf; -``` - -Occasionally, we also import constants whose value knowledge may help the compiler: - -```php -use const PHP_OS_FAMILY; -``` - - -Tabs Instead of Spaces -====================== - -Tabs have several advantages over spaces: - -- The size of the indentation is customizable in editors and on the "web":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size -- They do not impose the user's indentation size preference on the code, making the code more portable -- They can be typed with a single keystroke (anywhere, not just in editors that convert tabs to spaces) -- Indentation is their purpose -- They respect the needs of visually impaired and blind colleagues - -By using tabs in our projects, we allow for width customization, which may seem unnecessary to most people, but is essential for people with visual impairments. - -For blind programmers who use braille displays, each space represents a braille cell. So if the default indentation is 4 spaces, a 3rd level indentation wastes 12 valuable braille cells before the code even begins. On a 40-cell display, the most common for laptops, that's more than a quarter of the available cells wasted without providing any information. - - -{{priority: -1}} -{{maintitle: Nette Coding Standard Rules for PHP Code}} -{{description: The rules for writing code in Nette: naming conventions, wrapping and braces, phpDoc, global functions and constants, and tabs instead of spaces.}} diff --git a/contributing/en/documentation.texy b/contributing/en/documentation.texy deleted file mode 100644 index 18ed7a89e7..0000000000 --- a/contributing/en/documentation.texy +++ /dev/null @@ -1,71 +0,0 @@ -Contributing to Documentation -***************************** - -.[perex] -Contributing to the documentation is one of the most valuable activities, as it helps others understand the framework. - - -How to Write? -------------- - -Documentation is primarily intended for people who are new to the topic. Therefore, it should meet several important points: - -- Start with simple and general concepts. Move on to more advanced topics only at the end. -- Try to explain the topic as clearly as possible. For example, try explaining it to a colleague first. -- Provide only the information that the user actually needs for the given topic. -- Verify that your information is accurate. Test every piece of code. -- Be concise - cut what you write in half. Then feel free to do it again. -- Use highlighting sparingly, from bold text to boxes like `.[note]`. -- Follow the [Coding Standard] in the code examples. - -Also, learn the [syntax]. To preview the article while writing, you can use the [preview editor |https://editor.nette.org/]. - - -Language Versions ------------------ - -English is the primary language, so your changes should ideally be in English. If English is not your strong suit, use [DeepL Translator |https://www.deepl.com/translator] and others will review your text. - -Translation into other languages will be done automatically after your edit is approved and finalized. - - -Trivial Edits -------------- - -To contribute to the documentation, you need to have an account on [GitHub |https://github.com]. - -The easiest way to make a small change in the documentation is to use the links at the end of each page: - -- *Show on GitHub* opens the source version of the page on GitHub. Then just press the `E` key to start editing (you must be logged in to GitHub). -- *Open preview* opens an editor where you can immediately see the final visual appearance. - -Since the [preview editor |https://editor.nette.org/] cannot save changes directly to GitHub, after finishing your edits, you need to copy the source text to the clipboard (using the *Copy to clipboard* button) and then paste it into the editor on GitHub. Below the editing field is a submission form. Here, don't forget to briefly summarize and explain the reason for your edit. After submitting, a pull request (PR) is created, which can be further edited. - - -Larger Edits ------------- - -Instead of relying solely on the GitHub interface, it's better to be familiar with the basics of working with the Git version control system. If you're not familiar with Git, you can refer to the [git - the simple guide |https://rogerdudler.github.io/git-guide/] and consider using one of the many available [graphical clients |https://git-scm.com/downloads/guis]. - -Edit the documentation as follows: - -1) On GitHub, create a [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] of the [nette/docs |https://github.com/nette/docs] repository. -2) [Clone |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] this repository to your computer. -3) Then, make changes in the [appropriate branch |#Documentation Structure]. -4) Check for extra spaces in the text using the [Code-Checker |tools:code-checker] tool. -5) Save (commit) the changes. -6) If you are satisfied with the changes, push them to GitHub to your fork. -7) From there, submit them to the `nette/docs` repository by creating a [pull request|https://help.github.com/articles/creating-a-pull-request] (PR). - -It is common to receive comments with suggestions. Keep track of the proposed changes and incorporate them. Add the suggested changes as new commits and push them to GitHub again. Never create a new pull request just to modify an existing one. - - -Documentation Structure ------------------------ - -The entire documentation is located on GitHub in the [nette/docs |https://github.com/nette/docs] repository. The current version is in the `master` branch, while older versions are located in branches like `doc-3.x`, `doc-2.x`. - -The content of each branch is divided into main folders representing individual documentation areas. For example, `application/` corresponds to `https://doc.nette.org/en/application`, `latte/` corresponds to `https://latte.nette.org`, etc. Each of these folders contains subfolders representing language versions (`cs`, `en`, ...) and optionally a `files` subfolder with images that can be included in the documentation pages. - -{{maintitle: Contributing to Nette Documentation: How to Help}} -{{description: Contributing to the documentation helps others understand the framework. How to write for newcomers, which language versions to edit and how to preview text.}} diff --git a/contributing/en/syntax.texy b/contributing/en/syntax.texy deleted file mode 100644 index d8b1c3753b..0000000000 --- a/contributing/en/syntax.texy +++ /dev/null @@ -1,145 +0,0 @@ -Documentation Syntax -******************** - -Documentation uses Markdown & [Texy syntax |https://texy.deegee.dev/syntax] with several enhancements. - - -Links -===== - -For internal links, notation in square brackets `[link]` is used. This is either in the form with a pipe `[link text |link target]`, or in the abbreviated form `[link text]` if the target is the same as the text (after transformation to lowercase and hyphens): - -- `[Page name]` -> `<a href="/en/page-name">Page name</a>` -- `[link text |Page name]` -> `<a href="/en/page-name">link text</a>` - -We can link to another language version or another section. A section refers to a Nette library (e.g., `forms`, `latte`, etc.) or special sections like `best-practices`, `quickstart`, etc.: - -- `[cs:Page name]` -> `<a href="/cs/page-name">Page name</a>` (same section, different language) -- `[tracy:Page name]` -> `<a href="//tracy.nette.org/en/page-name">Page name</a>` (different section, same language) -- `[tracy:cs:Page name]` -> `<a href="//tracy.nette.org/cs/page-name">Page name</a>` (different section and language) - -It's also possible to target a specific heading on the page using `#`. - -- `[#Heading]` -> `<a href="#toc-heading">Heading</a>` (heading on the current page) -- `[Page name#Heading]` -> `<a href="/en/page-name#toc-heading">Page name</a>` - -Link to the section's home page: (`@home` is a special term for the section's home page) - -- `[link text |@home]` -> `<a href="/en/">link text</a>` -- `[link text |tracy:]` -> `<a href="//tracy.nette.org/en/">link text</a>` - - -Links to API Documentation --------------------------- - -Always use the following notation: - -- `[api:Nette\SmartObject]` -> [api:Nette\SmartObject] -- `[api:Nette\Forms\Form::setTranslator()]` -> [api:Nette\Forms\Form::setTranslator()] -- `[api:Nette\Forms\Form::$onSubmit]` -> [api:Nette\Forms\Form::$onSubmit] -- `[api:Nette\Forms\Form::Required]` -> [api:Nette\Forms\Form::Required] - -Use fully qualified names only in the first mention. For subsequent links, use a simplified name: - -- `[Form::setTranslator() |api:Nette\Forms\Form::setTranslator()]` -> [Form::setTranslator() |api:Nette\Forms\Form::setTranslator()] - - -Links to PHP Documentation --------------------------- - -- `[php:substr]` -> [php:substr] - - -Source Code -=========== - -A code block starts with <code>```lang</code> and ends with <code>```</code>. Supported languages are `php`, `latte`, `neon`, `html`, `css`, `js`, and `sql`. Always use tabs for indentation. - -``` - ```php - public function renderPage($id) - { - } - ``` -``` - -You can also specify the filename as <code>```php .{file: ArrayTest.php}</code>, and the code block will be rendered this way: - -```php .{file: ArrayTest.php} -public function renderPage($id) -{ -} -``` - - -Headings -======== - -Underline the top heading (page name) with asterisks (`*`). Use equal signs (`=`) to separate sections. Underline headings first with equal signs (`=`) and then with hyphens (`-`): - -``` -MVC Applications & Presenters -***************************** -... - - -Link Creation -============= -... - - -Links in Templates ------------------- -... -``` - - -Boxes and Styles -================ - -Perex marked with class `.[perex]` .[perex] - -Note marked with class `.[note]` .[note] - -Tip marked with class `.[tip]` .[tip] - -Caution marked with class `.[caution]` .[caution] - -Strong warning marked with class `.[warning]` .[warning] - -Version number `.{data-version:2.4.10}` .{data-version:2.4.10} - -Classes should be written before the line they apply to: - -``` -.[perex] -This is the perex. -``` - -Please note that boxes like `.[tip]` draw attention and therefore should be used for emphasizing important information, not for less significant details. Use them sparingly. - - -Table of Contents -================= - -A table of contents (links in the right sidebar) is automatically generated for all pages exceeding 4,000 bytes in size. This default behavior can be modified using the `{{toc}}` [#Meta Tags]. The text for the TOC is taken directly from the headings by default, but it's possible to display different text using the `.{toc}` modifier, which is useful for longer headings. - -``` - - -Long and Intelligent Heading .{toc: A Different Text for TOC} -============================================================= -``` - - -Meta Tags -========= - -- Set a custom page title (in `<title>` and breadcrumbs): `{{title: Another name}}` -- Redirect: `{{redirect: pla:cs}}` - see [#Links] -- Force `{{toc}}` or disable `{{toc: no}}` the automatic table of contents (box with links to headings). -- Set the left menu `{{leftbar: utils:@left-menu}}` or disable it `{{leftbar: no}}`. - -{{priority: -1}} -{{maintitle: Nette Documentation Syntax: Texy Markup for Pages}} -{{description: The markup used in the Nette documentation: links, source code, headings, boxes and styles, tables of contents and meta tags.}} diff --git a/contributing/es/@home.texy b/contributing/es/@home.texy deleted file mode 100644 index e19fdf2f2b..0000000000 --- a/contributing/es/@home.texy +++ /dev/null @@ -1,17 +0,0 @@ -Conviértase en colaborador de Nette -*********************************** - -.[perex] -Descubra cómo puede participar en nuestro proyecto de código abierto. Aprenda los procedimientos para contribuir al código fuente y a la documentación, y forme parte de la comunidad de desarrolladores que participan activamente en la mejora de Nette. - - -**Código** - -- [Contribuir al código |code] -- [Estándares de codificación |coding-standard] - -**Documentación** - -- [Contribuir a la documentación |documentation] -- [Sintaxis de la documentación |syntax] -- "Editor de vista previa":https://editor.nette.org diff --git a/contributing/es/@left-menu.texy b/contributing/es/@left-menu.texy deleted file mode 100644 index 153ac072de..0000000000 --- a/contributing/es/@left-menu.texy +++ /dev/null @@ -1,18 +0,0 @@ -Código -****** -- [Contribuir al código |code] -- [Estándares de codificación |coding-standard] - -Documentación -************* -- [Contribuir a la documentación |documentation] -- [Sintaxis de la documentación |syntax] -- "Editor de vista previa":https://editor.nette.org - - -Lecturas adicionales -******************** -- [Documentación de Nette |nette:] -- [Herramientas |tools:] -- [Quién crea Nette |https://nette.org/contributors] -- [Nette en GitHub |https://github.com/nette] diff --git a/contributing/es/code.texy b/contributing/es/code.texy deleted file mode 100644 index 7b1d52da51..0000000000 --- a/contributing/es/code.texy +++ /dev/null @@ -1,107 +0,0 @@ -Contribuir al código -******************** - -.[perex] -¿Está pensando en contribuir a Nette Framework y necesita familiarizarse con las reglas y los procedimientos? Esta guía para principiantes le llevará por los pasos necesarios para contribuir código de forma eficaz, trabajar con los repositorios e implementar cambios. - - -Procedimiento -============= - -Para contribuir código es imprescindible tener una cuenta en [GitHub|https://github.com] y conocer los fundamentos del trabajo con el sistema de control de versiones Git. Si no conoce Git, puede echar un vistazo a [git - the simple guide|https://rogerdudler.github.io/git-guide/] y plantearse usar alguno de los muchos [clientes gráficos|https://git-scm.com/downloads/guis]. - - -Preparar el entorno y el repositorio ------------------------------------- - -1) En GitHub, cree un [fork|https://help.github.com/en/github/getting-started-with-github/fork-a-repo] del [repositorio del paquete|www:packages] que se dispone a modificar -2) [Clone|https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] ese repositorio en su ordenador -3) Instale las dependencias, incluido [Nette Tester|tester:], con el comando `composer install` -4) Compruebe que las pruebas funcionan ejecutando `composer tester` -5) Cree una [#Rama nueva] a partir de la última versión publicada - - -Implementar sus propios cambios -------------------------------- - -Ahora puede hacer sus propios ajustes en el código: - -1) Implemente los cambios deseados y no se olvide de las pruebas -2) Asegúrese de que las pruebas se ejecutan correctamente con `composer tester` -3) Compruebe si el código cumple los [#Estándares de codificación] -4) Guarde (commit) los cambios con una descripción en [este formato |#Descripción del commit] - -Puede crear varios commits, uno por cada paso lógico. Cada commit debería tener sentido por sí mismo. - - -Enviar los cambios ------------------- - -Cuando esté satisfecho con los cambios, puede enviarlos: - -1) Suba los cambios a GitHub, a su fork -2) Desde allí, envíelos al repositorio de Nette creando un [pull request|https://help.github.com/articles/creating-a-pull-request] (PR) -3) Proporcione en la descripción [información suficiente |#Descripción del pull request] - - -Incorporar los comentarios --------------------------- - -Sus commits son ahora visibles para los demás. Es habitual recibir comentarios con sugerencias: - -1) Siga los cambios propuestos -2) Incorpórelos como commits nuevos o [fusiónelos con los anteriores|https://help.github.com/en/github/using-git/about-git-rebase] -3) Vuelva a enviar los commits a GitHub y aparecerán automáticamente en el pull request - -Nunca cree un pull request nuevo para modificar uno existente. - - -Documentación -------------- - -Si ha cambiado una funcionalidad o ha añadido una nueva, no se olvide de [añadirla también a la documentación|documentation]. - - -Rama nueva -========== - -Si es posible, haga los cambios contra la última versión publicada, es decir, la última etiqueta de la rama. Para la etiqueta `v3.2.1`, cree una rama con este comando: - -```shell -git checkout -b nombre_de_la_rama v3.2.1 -``` - - -Estándares de codificación -========================== - -Su código debe cumplir el [estándar de codificación|coding-standard] que se usa en Nette Framework. Para comprobar y corregir automáticamente su código, use la herramienta [Nette Coding Standard |tools:coding-standard], donde encontrará también las instrucciones de instalación y uso. - - -Descripción del commit -====================== - -En Nette, los asuntos de los commits tienen el siguiente formato: `Presenter: fixed AJAX detection [Closes #69]` - -- El área seguida de dos puntos -- El propósito del commit en pasado; si es posible, empiece con palabras como: "added (funcionalidad nueva)", "fixed (corrección)", "refactored (cambio de código sin cambio de comportamiento)", "changed", "removed" -- Si el commit rompe la retrocompatibilidad, añada "BC break" -- Cualquier enlace al gestor de incidencias, como `(#123)` o `[Closes #69]` -- Tras el asunto puede haber una línea en blanco seguida de una descripción más detallada, que incluya, por ejemplo, enlaces al foro - - -Descripción del pull request -============================ - -Al crear un pull request, la interfaz de GitHub le permitirá introducir un título y una descripción. Ponga un título conciso e incluya en la descripción toda la información posible sobre los motivos de su cambio. - -Indique además en la cabecera si se trata de una funcionalidad nueva o de una corrección de un fallo, y si puede causar problemas de retrocompatibilidad (BC break). Si hay una incidencia relacionada, enlácela para que se cierre al aprobarse el pull request. - -``` -- bug fix / new feature? <!-- #issue numbers, if any --> -- BC break? yes/no -- doc PR: nette/docs#? <!-- highly welcome, see https://nette.org/en/writing --> -``` - - -{{priority: -1}} diff --git a/contributing/es/coding-standard.texy b/contributing/es/coding-standard.texy deleted file mode 100644 index cc96d1a1b2..0000000000 --- a/contributing/es/coding-standard.texy +++ /dev/null @@ -1,148 +0,0 @@ -Estándar de codificación -************************ - -.[perex] -Este documento describe las reglas y las recomendaciones para desarrollar Nette. Al contribuir código a Nette tiene que seguirlas. La forma más fácil de hacerlo es imitar el código existente. El objetivo es que todo el código parezca escrito por una sola persona. - -El estándar de codificación de Nette se corresponde con [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/] con dos excepciones principales: usa [tabuladores en lugar de espacios |#Tabuladores en lugar de espacios] para la indentación y usa [PascalCase para las constantes de clase|https://blog.nette.org/es/for-less-screaming-in-the-code]. - -.[tip] -Muchas de estas reglas las puede comprobar y corregir automáticamente la herramienta [Nette Coding Standard |tools:coding-standard], así que no tiene que revisarlas a mano. - - -Reglas generales -================ - -- Cada archivo PHP tiene que contener `declare(strict_types=1)` -- Para separar los métodos se usan dos líneas vacías, para mejorar la legibilidad -- El motivo de usar el operador de silencio (`@`) tiene que estar documentado: `@mkdir($dir); // @ - el directorio puede existir` -- Si se usa un operador de comparación con tipado débil (es decir, `==`, `!=`, ...), la intención tiene que estar documentada: `// == para aceptar null` -- Puede escribir varias clases de excepción en un único archivo llamado `exceptions.php`, y varios enums en `enums.php` -- En las interfaces no se indica la visibilidad de los métodos, porque siempre son públicos -- Cada propiedad, valor de retorno y parámetro tiene que tener el tipo indicado. Al contrario, en las constantes finales nunca indicamos el tipo, porque es evidente -- Para delimitar las cadenas hay que usar comillas simples, salvo cuando el propio literal contiene apóstrofos - - -Convenciones de nombres -======================= - -- Evite usar abreviaturas salvo que el nombre completo sea excesivo -- Use mayúsculas para las abreviaturas de dos letras, y PascalCase/camelCase para las más largas -- Use un sustantivo o un sintagma nominal para el nombre de la clase -- Los nombres de las clases tienen que contener no solo la especificidad (`Array`), sino también la generalidad (`ArrayIterator`). Los atributos de PHP son una excepción -- "Las constantes de clase y los enums deberían usar PascalCaps":https://blog.nette.org/es/for-less-screaming-in-the-code -- "Las interfaces y las clases abstractas no deberían contener prefijos ni sufijos":https://blog.nette.org/es/prefixes-and-suffixes-do-not-belong-in-interface-names como `Abstract`, `Interface` o `I` - - -Saltos de línea y llaves -======================== - -El estándar de codificación de Nette se corresponde con PSR-12 (o PER Coding Style), pero lo precisa o lo modifica en algunos puntos: - -- Las funciones flecha se escriben sin espacio antes del paréntesis, es decir, `fn($a) => $b` -- No hace falta una línea vacía entre los distintos tipos de sentencias de importación `use` -- El tipo de retorno de una función o método y la llave de apertura están siempre en líneas separadas: - -```php - public function find( - string $dir, - array $options, - ): array - { - // cuerpo del método - } -``` - -La llave de apertura en una línea aparte es importante para separar visualmente la firma de la función o método de su cuerpo. Si la firma está en una línea, la separación es clara (imagen de la izquierda). Si está en varias líneas, en PSR la firma y el cuerpo se funden (en el centro), mientras que en el estándar de Nette siguen separados (a la derecha): - -[* new-line-after.webp *] - - -Bloques de documentación (phpDoc) -================================= - -La regla principal: **nunca duplique** ninguna información de la firma, como el tipo de un parámetro o el tipo de retorno, sin aportar valor. - -Bloque de documentación de la definición de una clase: - -- Empieza con una descripción de la clase -- Le sigue una línea vacía -- Le siguen las anotaciones `@property` (o `@property-read`, `@property-write`), una por línea. Sintaxis: anotación, espacio, tipo, espacio, `$nombre` -- Le siguen las anotaciones `@method`, una por línea. Sintaxis: anotación, espacio, tipo de retorno, espacio, `nombre(tipo $param, ...)` -- La anotación `@author` se omite. La autoría se conserva en el historial del código fuente -- Se pueden usar las anotaciones `@internal` o `@deprecated` - -```php -/** - * MIME message part. - * - * @property string $encoding - * @property-read array $headers - * @method string getSomething(string $name) - * @method static bool isEnabled() - */ -``` - -Un bloque de documentación de una propiedad que contiene solo la anotación `@var` debería estar en una sola línea: - -```php -/** @var string[] */ -private array $name; -``` - -Bloque de documentación de la definición de un método: - -- Empieza con una descripción breve del método -- Sin línea vacía -- Anotaciones `@param`, una por línea -- Anotación `@return` -- Anotaciones `@throws`, una por línea -- Se pueden usar las anotaciones `@internal` o `@deprecated` - -A cada anotación le sigue un espacio, salvo a `@param`, a la que le siguen dos espacios para mejorar la legibilidad. - -```php -/** - * Finds a file in directory. - * @param string[] $options - * @return string[] - * @throws DirectoryNotFoundException - */ -public function find(string $dir, array $options): array -``` - - -Funciones y constantes globales -=============================== - -Las funciones y las constantes globales se escriben sin barra invertida inicial, es decir, `count($arr)` y no `\count($arr)`. En las funciones que PHP puede optimizar, añada `use function` al principio del archivo para que el compilador pueda traducirlas de forma más eficiente. Entre ellas están funciones como `count`, `strlen`, `is_array`, `is_string`, `is_scalar`, `sprintf`, etc. Las funciones se listan en una sola línea para mantener compacto el bloque de importaciones: - -```php -use Nette; -use function count, is_array, is_scalar, sprintf; -``` - -De vez en cuando importamos también constantes cuyo valor conocido puede ayudar al compilador: - -```php -use const PHP_OS_FAMILY; -``` - - -Tabuladores en lugar de espacios -================================ - -Los tabuladores tienen varias ventajas sobre los espacios: - -- El tamaño de la indentación se puede ajustar en los editores y en la "web":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size -- No imponen al código la preferencia de tamaño de indentación del usuario, lo que hace el código más portable -- Se pueden escribir con una sola pulsación (en cualquier sitio, no solo en los editores que convierten los tabuladores en espacios) -- La indentación es su razón de ser -- Respetan las necesidades de los colegas con discapacidad visual y ciegos - -Al usar tabuladores en nuestros proyectos permitimos ajustar el ancho, algo que a la mayoría de la gente puede parecerle innecesario, pero que es esencial para las personas con discapacidad visual. - -Para los programadores ciegos que usan líneas braille, cada espacio representa una celda braille. Así que si la indentación predeterminada es de 4 espacios, una indentación de tercer nivel desperdicia 12 valiosas celdas braille antes incluso de que empiece el código. En una línea de 40 celdas, la más habitual en los portátiles, eso es más de una cuarta parte de las celdas disponibles desperdiciadas sin aportar ninguna información. - - -{{priority: -1}} diff --git a/contributing/es/documentation.texy b/contributing/es/documentation.texy deleted file mode 100644 index 16d5e84c16..0000000000 --- a/contributing/es/documentation.texy +++ /dev/null @@ -1,68 +0,0 @@ -Contribuir a la documentación -***************************** - -.[perex] -Contribuir a la documentación es una de las actividades más valiosas, porque ayuda a otros a entender el framework. - - -¿Cómo escribir? ---------------- - -La documentación está pensada sobre todo para las personas que se acercan al tema por primera vez. Por eso debería cumplir varios puntos importantes: - -- Empiece por los conceptos sencillos y generales. Pase a los temas más avanzados solo al final. -- Intente explicar el tema con la mayor claridad posible. Por ejemplo, pruebe a explicárselo antes a un compañero. -- Dé solo la información que el usuario necesita realmente para el tema en cuestión. -- Compruebe que su información es exacta. Pruebe cada fragmento de código. -- Sea conciso: recorte a la mitad lo que escriba. Y luego, si quiere, hágalo otra vez. -- Use el resaltado con moderación, desde el texto en negrita hasta los recuadros tipo `.[note]`. -- Siga el [estándar de codificación|coding-standard] en los ejemplos de código. - -Aprenda también la [sintaxis |syntax]. Para previsualizar el artículo mientras escribe puede usar el [editor de vista previa |https://editor.nette.org/]. - - -Versiones de idioma -------------------- - -El inglés es el idioma principal, así que lo ideal es que sus cambios estén en inglés. Si el inglés no es su fuerte, use el [traductor DeepL |https://www.deepl.com/translator] y otros revisarán su texto. - -La traducción a los demás idiomas se hará automáticamente después de que su edición se apruebe y se finalice. - - -Ediciones triviales -------------------- - -Para contribuir a la documentación necesita tener una cuenta en [GitHub |https://github.com]. - -La forma más fácil de hacer un cambio pequeño en la documentación es usar los enlaces del final de cada página: - -- *Show on GitHub* abre la versión fuente de la página en GitHub. Después basta con pulsar la tecla `E` para empezar a editar (tiene que haber iniciado sesión en GitHub). -- *Open preview* abre un editor donde ve de inmediato el aspecto visual final. - -Como el [editor de vista previa |https://editor.nette.org/] no puede guardar los cambios directamente en GitHub, al terminar de editar tiene que copiar el texto fuente al portapapeles (con el botón *Copy to clipboard*) y pegarlo después en el editor de GitHub. Debajo del campo de edición hay un formulario de envío. Ahí no se olvide de resumir brevemente y explicar el motivo de su edición. Tras enviarlo se crea un pull request (PR), que se puede seguir editando. - - -Ediciones más grandes ---------------------- - -En lugar de depender solo de la interfaz de GitHub, es mejor conocer los fundamentos del trabajo con el sistema de control de versiones Git. Si no conoce Git, puede consultar [git - the simple guide |https://rogerdudler.github.io/git-guide/] y plantearse usar alguno de los muchos [clientes gráficos |https://git-scm.com/downloads/guis] disponibles. - -Edite la documentación así: - -1) En GitHub, cree un [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] del repositorio [nette/docs |https://github.com/nette/docs]. -2) [Clone |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] ese repositorio en su ordenador. -3) Después, haga los cambios en la [rama correspondiente |#Estructura de la documentación]. -4) Compruebe si hay espacios sobrantes en el texto con la herramienta [Code-Checker |tools:code-checker]. -5) Guarde (commit) los cambios. -6) Si está satisfecho con los cambios, súbalos a GitHub, a su fork. -7) Desde allí, envíelos al repositorio `nette/docs` creando un [pull request|https://help.github.com/articles/creating-a-pull-request] (PR). - -Es habitual recibir comentarios con sugerencias. Siga los cambios propuestos e incorpórelos. Añada los cambios sugeridos como commits nuevos y vuelva a subirlos a GitHub. Nunca cree un pull request nuevo solo para modificar uno existente. - - -Estructura de la documentación ------------------------------- - -Toda la documentación está en GitHub, en el repositorio [nette/docs |https://github.com/nette/docs]. La versión actual está en la rama `master`, mientras que las versiones antiguas están en ramas como `doc-3.x`, `doc-2.x`. - -El contenido de cada rama se divide en carpetas principales que representan las distintas áreas de la documentación. Por ejemplo, `application/` corresponde a `https://doc.nette.org/en/application`, `latte/` corresponde a `https://latte.nette.org`, etc. Cada una de estas carpetas contiene subcarpetas que representan las versiones de idioma (`cs`, `en`, ...) y, opcionalmente, una subcarpeta `files` con las imágenes que se pueden incluir en las páginas de la documentación. diff --git a/contributing/es/syntax.texy b/contributing/es/syntax.texy deleted file mode 100644 index e8e1733c92..0000000000 --- a/contributing/es/syntax.texy +++ /dev/null @@ -1,143 +0,0 @@ -Sintaxis de la documentación -**************************** - -La documentación usa Markdown y la [sintaxis de Texy |https://texy.deegee.dev/syntax] con varias mejoras. - - -Enlaces -======= - -Para los enlaces internos se usa la notación entre corchetes `[enlace]`. Puede ser en la forma con barra vertical `[texto del enlace |destino del enlace]`, o en la forma abreviada `[texto del enlace]` si el destino coincide con el texto (tras convertirlo a minúsculas y guiones): - -- `[Nombre de la página]` -> `<a href="/es/nombre-de-la-pagina">Nombre de la página</a>` -- `[texto del enlace |Nombre de la página]` -> `<a href="/es/nombre-de-la-pagina">texto del enlace</a>` - -Podemos enlazar a otra versión de idioma o a otra sección. Una sección se refiere a una biblioteca de Nette (p. ej. `forms`, `latte`, etc.) o a secciones especiales como `best-practices`, `quickstart`, etc.: - -- `[cs:Nombre de la página]` -> `<a href="/cs/nombre-de-la-pagina">Nombre de la página</a>` (misma sección, otro idioma) -- `[tracy:Nombre de la página]` -> `<a href="//tracy.nette.org/es/nombre-de-la-pagina">Nombre de la página</a>` (otra sección, mismo idioma) -- `[tracy:cs:Nombre de la página]` -> `<a href="//tracy.nette.org/cs/nombre-de-la-pagina">Nombre de la página</a>` (otra sección y otro idioma) - -También es posible apuntar a un encabezado concreto de la página con `#`. - -- `[#Encabezado]` -> `<a href="#toc-encabezado">Encabezado</a>` (encabezado de la página actual) -- `[Nombre de la página#Encabezado]` -> `<a href="/es/nombre-de-la-pagina#toc-encabezado">Nombre de la página</a>` - -Enlace a la página principal de la sección: (`@home` es un término especial para la página principal de la sección) - -- `[texto del enlace |@home]` -> `<a href="/es/">texto del enlace</a>` -- `[texto del enlace |tracy:]` -> `<a href="//tracy.nette.org/es/">texto del enlace</a>` - - -Enlaces a la documentación de la API ------------------------------------- - -Use siempre la notación siguiente: - -- `[api:Nette\SmartObject]` -> [api:Nette\SmartObject] -- `[api:Nette\Forms\Form::setTranslator()]` -> [api:Nette\Forms\Form::setTranslator()] -- `[api:Nette\Forms\Form::$onSubmit]` -> [api:Nette\Forms\Form::$onSubmit] -- `[api:Nette\Forms\Form::Required]` -> [api:Nette\Forms\Form::Required] - -Use los nombres completamente cualificados solo en la primera mención. En los enlaces siguientes use un nombre simplificado: - -- `[Form::setTranslator() |api:Nette\Forms\Form::setTranslator()]` -> [Form::setTranslator() |api:Nette\Forms\Form::setTranslator()] - - -Enlaces a la documentación de PHP ---------------------------------- - -- `[php:substr]` -> [php:substr] - - -Código fuente -============= - -Un bloque de código empieza con <code>```lang</code> y termina con <code>```</code>. Los lenguajes soportados son `php`, `latte`, `neon`, `html`, `css`, `js` y `sql`. Use siempre tabuladores para la indentación. - -``` - ```php - public function renderPage($id) - { - } - ``` -``` - -También puede indicar el nombre del archivo como <code>```php .{file: ArrayTest.php}</code>, y el bloque de código se renderizará así: - -```php .{file: ArrayTest.php} -public function renderPage($id) -{ -} -``` - - -Encabezados -=========== - -El encabezado superior (el nombre de la página) se subraya con asteriscos (`*`). Use signos de igual (`=`) para separar las secciones. Los encabezados se subrayan primero con signos de igual (`=`) y después con guiones (`-`): - -``` -MVC Applications & Presenters -***************************** -... - - -Link Creation -============= -... - - -Links in Templates ------------------- -... -``` - - -Recuadros y estilos -=================== - -Perex marcado con la clase `.[perex]` .[perex] - -Nota marcada con la clase `.[note]` .[note] - -Consejo marcado con la clase `.[tip]` .[tip] - -Precaución marcada con la clase `.[caution]` .[caution] - -Advertencia fuerte marcada con la clase `.[warning]` .[warning] - -Número de versión `.{data-version:2.4.10}` .{data-version:2.4.10} - -Las clases se escriben antes de la línea a la que se aplican: - -``` -.[perex] -Esto es el perex. -``` - -Tenga en cuenta que los recuadros como `.[tip]` llaman la atención y por tanto deberían usarse para destacar información importante, no detalles menos significativos. Úselos con moderación. - - -Tabla de contenidos -=================== - -La tabla de contenidos (los enlaces de la barra lateral derecha) se genera automáticamente para todas las páginas que superan los 4000 bytes de tamaño. Este comportamiento predeterminado se puede modificar con la [#Metaetiquetas] `{{toc}}`. De forma predeterminada, el texto de la tabla de contenidos se toma directamente de los encabezados, pero es posible mostrar un texto distinto con el modificador `.{toc}`, lo que resulta útil en los encabezados más largos. - -``` - - -Encabezado largo e inteligente .{toc: Un texto distinto para la TOC} -==================================================================== -``` - - -Metaetiquetas -============= - -- Establecer un título de página propio (en `<title>` y en las migas de pan): `{{title: Otro nombre}}` -- Redirección: `{{redirect: pla:cs}}` - vea [#Enlaces] -- Forzar `{{toc}}` o desactivar `{{toc: no}}` la tabla de contenidos automática (el recuadro con enlaces a los encabezados). -- Establecer el menú izquierdo `{{leftbar: utils:@left-menu}}` o desactivarlo `{{leftbar: no}}`. - -{{priority: -1}} diff --git a/contributing/files/new-line-after.webp b/contributing/files/new-line-after.webp deleted file mode 100644 index 419492d01f..0000000000 Binary files a/contributing/files/new-line-after.webp and /dev/null differ diff --git a/contributing/fr/@home.texy b/contributing/fr/@home.texy deleted file mode 100644 index b530df7923..0000000000 --- a/contributing/fr/@home.texy +++ /dev/null @@ -1,17 +0,0 @@ -Devenez contributeur de Nette -***************************** - -.[perex] -Découvrez comment vous impliquer dans notre projet open source. Apprenez les procédures de contribution au code source et à la documentation, et rejoignez la communauté des développeurs qui participent activement à l'amélioration de Nette. - - -**Code** - -- [Contribuer au code |code] -- [Standards de codage |coding-standard] - -**Documentation** - -- [Contribuer à la documentation |documentation] -- [Syntaxe de la documentation |syntax] -- "Éditeur d'aperçu":https://editor.nette.org diff --git a/contributing/fr/@left-menu.texy b/contributing/fr/@left-menu.texy deleted file mode 100644 index 7dad1d7512..0000000000 --- a/contributing/fr/@left-menu.texy +++ /dev/null @@ -1,18 +0,0 @@ -Code -**** -- [Contribuer au code |code] -- [Standards de codage |coding-standard] - -Documentation -************* -- [Contribuer à la documentation |documentation] -- [Syntaxe de la documentation |syntax] -- "Éditeur d'aperçu":https://editor.nette.org - - -Pour aller plus loin -******************** -- [Documentation Nette |nette:] -- [Outils |tools:] -- [Qui crée Nette |https://nette.org/contributors] -- [Nette sur GitHub |https://github.com/nette] diff --git a/contributing/fr/code.texy b/contributing/fr/code.texy deleted file mode 100644 index f8002bacff..0000000000 --- a/contributing/fr/code.texy +++ /dev/null @@ -1,107 +0,0 @@ -Contribuer au code -****************** - -.[perex] -Vous envisagez de contribuer à Nette Framework et vous avez besoin de connaître les règles et les procédures ? Ce guide pour débutants vous accompagne pas à pas pour contribuer efficacement au code, travailler avec les dépôts et mettre en œuvre vos modifications. - - -Procédure -========= - -Pour contribuer au code, il est indispensable d'avoir un compte sur [GitHub|https://github.com] et de connaître les bases du système de gestion de versions Git. Si Git ne vous est pas familier, vous pouvez consulter [git - the simple guide|https://rogerdudler.github.io/git-guide/] et envisager l'un des nombreux [clients graphiques|https://git-scm.com/downloads/guis]. - - -Préparation de l'environnement et du dépôt ------------------------------------------- - -1) Sur GitHub, créez un [fork|https://help.github.com/en/github/getting-started-with-github/fork-a-repo] du [dépôt du paquet|www:packages] que vous comptez modifier -2) [Clonez|https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] ce dépôt sur votre ordinateur -3) Installez les dépendances, [Nette Tester|tester:] compris, avec la commande `composer install` -4) Vérifiez que les tests fonctionnent en lançant `composer tester` -5) Créez une [#Nouvelle branche] basée sur la dernière version publiée - - -Mise en œuvre de vos modifications ----------------------------------- - -Vous pouvez maintenant apporter vos propres modifications au code : - -1) Implémentez les changements souhaités et n'oubliez pas les tests -2) Assurez-vous que les tests passent avec `composer tester` -3) Vérifiez que le code respecte les [#Standards de codage] -4) Enregistrez (commit) les changements avec une description dans [ce format |#Description du commit] - -Vous pouvez créer plusieurs commits, un par étape logique. Chaque commit doit avoir du sens à lui seul. - - -Soumission des modifications ----------------------------- - -Une fois satisfait de vos modifications, vous pouvez les soumettre : - -1) Poussez les changements sur GitHub, dans votre fork -2) De là, soumettez-les au dépôt de Nette en créant une [pull request|https://help.github.com/articles/creating-a-pull-request] (PR) -3) Donnez [assez d'informations |#Description de la pull request] dans la description - - -Prise en compte des retours ---------------------------- - -Vos commits sont désormais visibles par les autres. Il est courant de recevoir des commentaires avec des suggestions : - -1) Suivez les changements proposés -2) Intégrez-les sous forme de nouveaux commits ou [fusionnez-les avec les précédents|https://help.github.com/en/github/using-git/about-git-rebase] -3) Renvoyez les commits sur GitHub, ils apparaîtront automatiquement dans la pull request - -Ne créez jamais une nouvelle pull request pour modifier une pull request existante. - - -Documentation -------------- - -Si vous avez modifié une fonctionnalité ou en avez ajouté une, n'oubliez pas de [l'ajouter aussi à la documentation|documentation]. - - -Nouvelle branche -================ - -Si possible, faites vos modifications par rapport à la dernière version publiée, c'est-à-dire au dernier tag de la branche. Pour le tag `v3.2.1`, créez une branche avec cette commande : - -```shell -git checkout -b new_branch_name v3.2.1 -``` - - -Standards de codage -=================== - -Votre code doit respecter le [standard de codage|coding-standard] utilisé dans Nette Framework. Pour le vérifier et le corriger automatiquement, servez-vous de l'outil [Nette Coding Standard |tools:coding-standard], où vous trouverez aussi les instructions d'installation et d'utilisation. - - -Description du commit -===================== - -Dans Nette, les sujets des commits ont le format suivant : `Presenter: fixed AJAX detection [Closes #69]` - -- Le domaine, suivi de deux-points -- L'objet du commit au passé ; si possible, commencez par des mots comme : "added (nouvelle fonctionnalité)", "fixed (correction)", "refactored (changement de code sans changement de comportement)", "changed", "removed" -- Si le commit casse la rétrocompatibilité, ajoutez "BC break" -- Éventuellement un lien vers le gestionnaire de tickets, par ex. `(#123)` ou `[Closes #69]` -- Après le sujet peut venir une ligne vide, suivie d'une description plus détaillée comprenant par exemple des liens vers le forum - - -Description de la pull request -============================== - -À la création d'une pull request, l'interface de GitHub vous permet de saisir un titre et une description. Donnez un titre concis et mettez dans la description le maximum d'informations sur les raisons de votre changement. - -Précisez aussi dans l'en-tête s'il s'agit d'une nouvelle fonctionnalité ou d'une correction de bug, et si cela peut poser des problèmes de rétrocompatibilité (BC break). S'il existe un ticket lié, faites-y référence pour qu'il se ferme à l'acceptation de la pull request. - -``` -- bug fix / new feature? <!-- #issue numbers, if any --> -- BC break? yes/no -- doc PR: nette/docs#? <!-- highly welcome, see https://nette.org/en/writing --> -``` - - -{{priority: -1}} diff --git a/contributing/fr/coding-standard.texy b/contributing/fr/coding-standard.texy deleted file mode 100644 index 7fb99402bd..0000000000 --- a/contributing/fr/coding-standard.texy +++ /dev/null @@ -1,148 +0,0 @@ -Standard de codage -****************** - -.[perex] -Ce document décrit les règles et les recommandations pour le développement de Nette. Quand vous contribuez du code à Nette, vous devez les respecter. Le plus simple est d'imiter le code existant. L'objectif est que tout le code paraisse écrit par une seule personne. - -Le Nette Coding Standard correspond au [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/] avec deux exceptions majeures : il indente avec des [tabulations au lieu d'espaces |#Tabulations au lieu d'espaces] et emploie le [PascalCase pour les constantes de classe|https://blog.nette.org/fr/for-less-screaming-in-the-code]. - -.[tip] -Beaucoup de ces règles peuvent être vérifiées et corrigées automatiquement par l'outil [Nette Coding Standard |tools:coding-standard], vous n'avez donc pas à les contrôler à la main. - - -Règles générales -================ - -- Chaque fichier PHP doit contenir `declare(strict_types=1)` -- Deux lignes vides séparent les méthodes, pour une meilleure lisibilité -- La raison d'employer l'opérateur de silence (`@`) doit être documentée : `@mkdir($dir); // @ - directory may exist` -- Si un opérateur de comparaison faible est utilisé (`==`, `!=`, ...), l'intention doit être documentée : `// == to accept null` -- Vous pouvez écrire plusieurs classes d'exception dans un seul fichier nommé `exceptions.php`, et plusieurs enums dans `enums.php` -- La visibilité des méthodes n'est pas précisée dans les interfaces, car elles sont toujours publiques -- Chaque propriété, valeur de retour et paramètre doit avoir un type. À l'inverse, pour les constantes finales, nous ne précisons jamais le type, car il est évident -- Les chaînes se délimitent par des apostrophes, sauf lorsque le littéral contient lui-même des apostrophes - - -Conventions de nommage -====================== - -- Évitez les abréviations, sauf si le nom complet est excessif -- Écrivez les abréviations de deux lettres en majuscules, et employez PascalCase/camelCase pour les plus longues -- Employez un nom ou un groupe nominal pour le nom d'une classe -- Un nom de classe doit contenir non seulement le spécifique (`Array`) mais aussi le générique (`ArrayIterator`). Les attributs PHP font exception -- "Les constantes de classe et les enums devraient utiliser PascalCaps":https://blog.nette.org/fr/for-less-screaming-in-the-code -- "Les interfaces et les classes abstraites ne devraient pas porter de préfixes ni de suffixes":https://blog.nette.org/fr/prefixes-and-suffixes-do-not-belong-in-interface-names comme `Abstract`, `Interface` ou `I` - - -Retours à la ligne et accolades -=============================== - -Le Nette Coding Standard correspond au PSR-12 (ou PER Coding Style), mais le précise ou le modifie sur certains points : - -- Les fonctions fléchées s'écrivent sans espace avant la parenthèse, c'est-à-dire `fn($a) => $b` -- Aucune ligne vide n'est exigée entre les différents types d'imports `use` -- Le type de retour d'une fonction/méthode et l'accolade ouvrante sont toujours sur des lignes distinctes : - -```php - public function find( - string $dir, - array $options, - ): array - { - // method body - } -``` - -L'accolade ouvrante sur une ligne à part est importante pour séparer visuellement la signature de la fonction/méthode de son corps. Si la signature tient sur une ligne, la séparation est claire (image de gauche). Si elle s'étale sur plusieurs lignes, en PSR la signature et le corps se confondent (au milieu), tandis que dans le standard de Nette ils restent séparés (à droite) : - -[* new-line-after.webp *] - - -Blocs de documentation (phpDoc) -=============================== - -La règle principale : **ne dupliquez jamais** une information de la signature, comme le type d'un paramètre ou le type de retour, sans apporter de valeur. - -Bloc de documentation d'une définition de classe : - -- Commence par une description de la classe -- Suivie d'une ligne vide -- Suivie des annotations `@property` (ou `@property-read`, `@property-write`), une par ligne. Syntaxe : annotation, espace, type, espace, `$nom` -- Suivies des annotations `@method`, une par ligne. Syntaxe : annotation, espace, type de retour, espace, `nom(type $param, ...)` -- L'annotation `@author` est omise. La paternité est conservée dans l'historique du code source -- Les annotations `@internal` ou `@deprecated` peuvent être utilisées - -```php -/** - * MIME message part. - * - * @property string $encoding - * @property-read array $headers - * @method string getSomething(string $name) - * @method static bool isEnabled() - */ -``` - -Un bloc de documentation de propriété ne contenant que l'annotation `@var` doit tenir sur une seule ligne : - -```php -/** @var string[] */ -private array $name; -``` - -Bloc de documentation d'une définition de méthode : - -- Commence par une courte description de la méthode -- Pas de ligne vide -- Les annotations `@param`, une par ligne -- L'annotation `@return` -- Les annotations `@throws`, une par ligne -- Les annotations `@internal` ou `@deprecated` peuvent être utilisées - -Chaque annotation est suivie d'une espace, sauf `@param`, qui est suivie de deux espaces pour une meilleure lisibilité. - -```php -/** - * Finds a file in directory. - * @param string[] $options - * @return string[] - * @throws DirectoryNotFoundException - */ -public function find(string $dir, array $options): array -``` - - -Fonctions et constantes globales -================================ - -Les fonctions et constantes globales s'écrivent sans barre oblique inverse initiale, donc `count($arr)` et non `\count($arr)`. Pour les fonctions que PHP sait optimiser, ajoutez `use function` au début du fichier, afin que le compilateur les traduise plus efficacement. Il s'agit de fonctions comme `count`, `strlen`, `is_array`, `is_string`, `is_scalar`, `sprintf`, etc. Les fonctions sont listées sur une seule ligne pour garder le bloc d'imports compact : - -```php -use Nette; -use function count, is_array, is_scalar, sprintf; -``` - -Il nous arrive aussi d'importer des constantes dont la connaissance de la valeur peut aider le compilateur : - -```php -use const PHP_OS_FAMILY; -``` - - -Tabulations au lieu d'espaces -============================= - -Les tabulations ont plusieurs avantages sur les espaces : - -- La taille de l'indentation est réglable dans les éditeurs et sur le "web":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size -- Elles n'imposent pas au code la préférence d'indentation de leur auteur, ce qui rend le code plus portable -- Elles s'obtiennent d'une seule frappe (partout, pas seulement dans les éditeurs qui convertissent les tabulations en espaces) -- L'indentation est leur raison d'être -- Elles respectent les besoins des collègues malvoyants et aveugles - -En utilisant des tabulations dans nos projets, nous permettons d'en régler la largeur, ce qui peut sembler superflu à la plupart des gens, mais qui est essentiel pour les personnes atteintes de déficience visuelle. - -Pour les programmeurs aveugles qui utilisent un afficheur braille, chaque espace occupe une cellule braille. Si l'indentation par défaut est de 4 espaces, une indentation de 3e niveau gaspille donc 12 précieuses cellules braille avant même que le code ne commence. Sur un afficheur de 40 cellules, le plus courant pour les portables, c'est plus d'un quart des cellules disponibles gaspillé sans apporter la moindre information. - - -{{priority: -1}} diff --git a/contributing/fr/documentation.texy b/contributing/fr/documentation.texy deleted file mode 100644 index 669bd7149f..0000000000 --- a/contributing/fr/documentation.texy +++ /dev/null @@ -1,68 +0,0 @@ -Contribuer à la documentation -***************************** - -.[perex] -Contribuer à la documentation est l'une des activités les plus précieuses, car elle aide les autres à comprendre le framework. - - -Comment écrire ? ----------------- - -La documentation s'adresse avant tout à des personnes qui découvrent le sujet. Elle doit donc satisfaire plusieurs points importants : - -- Commencez par des notions simples et générales. Ne passez aux sujets plus avancés qu'à la fin. -- Efforcez-vous d'expliquer le sujet le plus clairement possible. Essayez par exemple de l'expliquer d'abord à un collègue. -- Ne donnez que les informations dont le lecteur a réellement besoin sur le sujet. -- Vérifiez que vos informations sont exactes. Testez chaque bout de code. -- Soyez concis : coupez de moitié ce que vous écrivez. Puis recommencez sans hésiter. -- Utilisez la mise en valeur avec parcimonie, du texte en gras jusqu'aux encadrés comme `.[note]`. -- Respectez le [standard de codage|coding-standard] dans les exemples de code. - -Apprenez aussi la [syntaxe |syntax]. Pour prévisualiser l'article pendant que vous l'écrivez, vous pouvez utiliser l'[éditeur d'aperçu |https://editor.nette.org/]. - - -Versions linguistiques ----------------------- - -L'anglais est la langue principale, vos modifications devraient donc idéalement être en anglais. Si l'anglais n'est pas votre fort, utilisez [DeepL Translator |https://www.deepl.com/translator] et d'autres relirons votre texte. - -La traduction dans les autres langues se fera automatiquement une fois votre modification approuvée et finalisée. - - -Petites modifications ---------------------- - -Pour contribuer à la documentation, vous devez avoir un compte sur [GitHub |https://github.com]. - -Le moyen le plus simple d'apporter un petit changement à la documentation est d'utiliser les liens en bas de chaque page : - -- *Afficher sur GitHub* ouvre la version source de la page sur GitHub. Il suffit ensuite d'appuyer sur la touche `E` pour commencer à l'éditer (vous devez être connecté à GitHub). -- *Ouvrir l'aperçu* ouvre un éditeur où vous voyez immédiatement le rendu visuel final. - -Comme l'[éditeur d'aperçu |https://editor.nette.org/] ne sait pas enregistrer directement sur GitHub, une fois vos modifications terminées, vous devez copier le texte source dans le presse-papiers (avec le bouton *Copier dans le presse-papiers*) puis le coller dans l'éditeur de GitHub. Sous le champ d'édition se trouve un formulaire d'envoi. N'y oubliez pas de résumer brièvement et d'expliquer la raison de votre modification. Après l'envoi, une pull request (PR) est créée, qui peut encore être modifiée. - - -Modifications plus importantes ------------------------------- - -Plutôt que de vous en remettre à la seule interface de GitHub, mieux vaut connaître les bases du système de gestion de versions Git. Si Git ne vous est pas familier, vous pouvez consulter [git - the simple guide |https://rogerdudler.github.io/git-guide/] et envisager l'un des nombreux [clients graphiques |https://git-scm.com/downloads/guis] disponibles. - -Modifiez la documentation ainsi : - -1) Sur GitHub, créez un [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] du dépôt [nette/docs |https://github.com/nette/docs]. -2) [Clonez |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] ce dépôt sur votre ordinateur. -3) Apportez ensuite vos changements dans la [branche appropriée |#Structure de la documentation]. -4) Cherchez les espaces superflus dans le texte à l'aide de l'outil [Code-Checker |tools:code-checker]. -5) Enregistrez (commit) les changements. -6) Si vous êtes satisfait des changements, poussez-les sur GitHub, dans votre fork. -7) De là, soumettez-les au dépôt `nette/docs` en créant une [pull request|https://help.github.com/articles/creating-a-pull-request] (PR). - -Il est courant de recevoir des commentaires avec des suggestions. Suivez les changements proposés et intégrez-les. Ajoutez les changements suggérés sous forme de nouveaux commits et poussez-les de nouveau sur GitHub. Ne créez jamais une nouvelle pull request seulement pour modifier une pull request existante. - - -Structure de la documentation ------------------------------ - -Toute la documentation se trouve sur GitHub, dans le dépôt [nette/docs |https://github.com/nette/docs]. La version actuelle est dans la branche `master`, les versions plus anciennes dans des branches comme `doc-3.x`, `doc-2.x`. - -Le contenu de chaque branche est réparti en dossiers principaux représentant les différents domaines de la documentation. Par exemple, `application/` correspond à `https://doc.nette.org/en/application`, `latte/` correspond à `https://latte.nette.org`, etc. Chacun de ces dossiers contient des sous-dossiers représentant les versions linguistiques (`cs`, `en`, ...) et éventuellement un sous-dossier `files` avec les images que les pages de la documentation peuvent inclure. diff --git a/contributing/fr/syntax.texy b/contributing/fr/syntax.texy deleted file mode 100644 index c34b749e54..0000000000 --- a/contributing/fr/syntax.texy +++ /dev/null @@ -1,143 +0,0 @@ -Syntaxe de la documentation -*************************** - -La documentation utilise Markdown et la [syntaxe Texy |https://texy.deegee.dev/syntax] avec quelques enrichissements. - - -Liens -===== - -Pour les liens internes, on utilise l'écriture entre crochets `[lien]`. Soit sous la forme avec barre verticale `[texte du lien |cible du lien]`, soit sous la forme abrégée `[texte du lien]` quand la cible est identique au texte (après passage en minuscules et remplacement par des tirets) : - -- `[Nom de la page]` -> `<a href="/en/page-name">Nom de la page</a>` -- `[texte du lien |Nom de la page]` -> `<a href="/en/page-name">texte du lien</a>` - -Nous pouvons pointer vers une autre version linguistique ou une autre section. Une section désigne une bibliothèque de Nette (par ex. `forms`, `latte`, etc.) ou une section spéciale comme `best-practices`, `quickstart`, etc. : - -- `[cs:Nom de la page]` -> `<a href="/cs/page-name">Nom de la page</a>` (même section, autre langue) -- `[tracy:Nom de la page]` -> `<a href="//tracy.nette.org/en/page-name">Nom de la page</a>` (autre section, même langue) -- `[tracy:cs:Nom de la page]` -> `<a href="//tracy.nette.org/cs/page-name">Nom de la page</a>` (autre section et autre langue) - -Il est également possible de viser un titre précis de la page à l'aide de `#`. - -- `[#Titre]` -> `<a href="#toc-heading">Titre</a>` (titre de la page courante) -- `[Nom de la page#Titre]` -> `<a href="/en/page-name#toc-heading">Nom de la page</a>` - -Lien vers la page d'accueil de la section : (`@home` est un terme spécial désignant la page d'accueil de la section) - -- `[texte du lien |@home]` -> `<a href="/en/">texte du lien</a>` -- `[texte du lien |tracy:]` -> `<a href="//tracy.nette.org/en/">texte du lien</a>` - - -Liens vers la documentation de l'API ------------------------------------- - -Utilisez toujours l'écriture suivante : - -- `[api:Nette\SmartObject]` -> [api:Nette\SmartObject] -- `[api:Nette\Forms\Form::setTranslator()]` -> [api:Nette\Forms\Form::setTranslator()] -- `[api:Nette\Forms\Form::$onSubmit]` -> [api:Nette\Forms\Form::$onSubmit] -- `[api:Nette\Forms\Form::Required]` -> [api:Nette\Forms\Form::Required] - -N'employez les noms pleinement qualifiés qu'à la première mention. Pour les liens suivants, utilisez un nom simplifié : - -- `[Form::setTranslator() |api:Nette\Forms\Form::setTranslator()]` -> [Form::setTranslator() |api:Nette\Forms\Form::setTranslator()] - - -Liens vers la documentation de PHP ----------------------------------- - -- `[php:substr]` -> [php:substr] - - -Code source -=========== - -Un bloc de code commence par <code>```lang</code> et se termine par <code>```</code>. Les langages pris en charge sont `php`, `latte`, `neon`, `html`, `css`, `js` et `sql`. Utilisez toujours des tabulations pour l'indentation. - -``` - ```php - public function renderPage($id) - { - } - ``` -``` - -Vous pouvez aussi indiquer le nom du fichier avec <code>```php .{file: ArrayTest.php}</code>, et le bloc de code sera rendu ainsi : - -```php .{file: ArrayTest.php} -public function renderPage($id) -{ -} -``` - - -Titres -====== - -Soulignez le titre principal (nom de la page) par des astérisques (`*`). Utilisez les signes égal (`=`) pour séparer les sections. Soulignez les titres d'abord par des signes égal (`=`), puis par des tirets (`-`) : - -``` -MVC Applications & Presenters -***************************** -... - - -Link Creation -============= -... - - -Links in Templates ------------------- -... -``` - - -Encadrés et styles -================== - -Perex marqué par la classe `.[perex]` .[perex] - -Note marquée par la classe `.[note]` .[note] - -Astuce marquée par la classe `.[tip]` .[tip] - -Avertissement marqué par la classe `.[caution]` .[caution] - -Avertissement fort marqué par la classe `.[warning]` .[warning] - -Numéro de version `.{data-version:2.4.10}` .{data-version:2.4.10} - -Les classes s'écrivent avant la ligne à laquelle elles s'appliquent : - -``` -.[perex] -This is the perex. -``` - -Notez que des encadrés comme `.[tip]` attirent l'attention et doivent donc mettre en valeur une information importante, pas un détail secondaire. Utilisez-les avec parcimonie. - - -Table des matières -================== - -Une table des matières (les liens dans la barre latérale de droite) est générée automatiquement pour toutes les pages dépassant 4 000 octets. Ce comportement par défaut se modifie avec la [méta-balise |#Méta-balises] `{{toc}}`. Le texte de la table est repris tel quel des titres par défaut, mais il est possible d'en afficher un autre grâce au modificateur `.{toc}`, ce qui est pratique pour les titres longs. - -``` - - -Long and Intelligent Heading .{toc: A Different Text for TOC} -============================================================= -``` - - -Méta-balises -============ - -- Définir un titre de page personnalisé (dans `<title>` et le fil d'Ariane) : `{{title: Another name}}` -- Redirection : `{{redirect: pla:cs}}` - voir [#Liens] -- Forcer `{{toc}}` ou désactiver `{{toc: no}}` la table des matières automatique (encadré avec les liens vers les titres). -- Définir le menu de gauche `{{leftbar: utils:@left-menu}}` ou le désactiver `{{leftbar: no}}`. - -{{priority: -1}} diff --git a/contributing/it/@home.texy b/contributing/it/@home.texy deleted file mode 100644 index fa01dea43f..0000000000 --- a/contributing/it/@home.texy +++ /dev/null @@ -1,17 +0,0 @@ -Diventa un contributore di Nette -******************************** - -.[perex] -Scoprite come potete partecipare al nostro progetto open source. Imparate le procedure per contribuire al codice sorgente e alla documentazione e diventate parte della comunità di sviluppatori che partecipano attivamente al miglioramento di Nette. - - -**Codice** - -- [Contribuire al codice |code] -- [Standard di codifica |coding-standard] - -**Documentazione** - -- [Contribuire alla documentazione |documentation] -- [Sintassi della documentazione |syntax] -- "Editor di anteprima":https://editor.nette.org diff --git a/contributing/it/@left-menu.texy b/contributing/it/@left-menu.texy deleted file mode 100644 index 9ab88a7be8..0000000000 --- a/contributing/it/@left-menu.texy +++ /dev/null @@ -1,18 +0,0 @@ -Codice -****** -- [Contribuire al codice |code] -- [Standard di codifica |coding-standard] - -Documentazione -************** -- [Contribuire alla documentazione |documentation] -- [Sintassi della documentazione |syntax] -- "Editor di anteprima":https://editor.nette.org - - -Letture consigliate -******************* -- [Documentazione di Nette |nette:] -- [Strumenti |tools:] -- [Chi crea Nette |https://nette.org/contributors] -- [Nette su GitHub |https://github.com/nette] diff --git a/contributing/it/code.texy b/contributing/it/code.texy deleted file mode 100644 index cb1d18a645..0000000000 --- a/contributing/it/code.texy +++ /dev/null @@ -1,107 +0,0 @@ -Contribuire al codice -********************* - -.[perex] -State pensando di contribuire al Nette Framework e avete bisogno di conoscere regole e procedure? Questa guida per principianti vi accompagna nei passi per contribuire al codice in modo efficace, lavorare con i repository e realizzare le modifiche. - - -Procedura -========= - -Per contribuire al codice è indispensabile avere un account su [GitHub|https://github.com] e conoscere le basi del lavoro con il sistema di versionamento Git. Se non conoscete Git, potete consultare [git - the simple guide|https://rogerdudler.github.io/git-guide/] e considerare l'uso di uno dei tanti [client grafici|https://git-scm.com/downloads/guis]. - - -Preparazione dell'ambiente e del repository -------------------------------------------- - -1) Su GitHub create un [fork|https://help.github.com/en/github/getting-started-with-github/fork-a-repo] del [repository del pacchetto|www:packages] che volete modificare -2) [Clonate|https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] questo repository sul vostro computer -3) Installate le dipendenze, compreso [Nette Tester|tester:], con il comando `composer install` -4) Verificate che i test funzionino lanciando `composer tester` -5) Create un [#Nuovo ramo] basato sull'ultima versione rilasciata - - -Realizzazione delle vostre modifiche ------------------------------------- - -Ora potete fare le vostre modifiche al codice: - -1) Realizzate le modifiche desiderate e non dimenticate i test -2) Assicuratevi che i test passino con `composer tester` -3) Verificate che il codice rispetti gli [#Standard di codifica] -4) Salvate (commit) le modifiche con una descrizione in [questo formato |#Descrizione del commit] - -Potete creare più commit, uno per ogni passo logico. Ogni commit dovrebbe avere senso da solo. - - -Invio delle modifiche ---------------------- - -Quando siete soddisfatti delle modifiche, potete inviarle: - -1) Inviate (push) le modifiche su GitHub nel vostro fork -2) Da lì proponetele al repository di Nette creando una [pull request|https://help.github.com/articles/creating-a-pull-request] (PR) -3) Nella descrizione fornite [informazioni sufficienti |#Descrizione della pull request] - - -Incorporare i feedback ----------------------- - -I vostri commit sono ora visibili agli altri. È normale ricevere commenti con dei suggerimenti: - -1) Seguite le modifiche proposte -2) Incorporatele come nuovi commit oppure [uniteli con i precedenti|https://help.github.com/en/github/using-git/about-git-rebase] -3) Inviate di nuovo i commit su GitHub e compariranno automaticamente nella pull request - -Non create mai una nuova pull request per modificarne una esistente. - - -Documentazione --------------- - -Se avete cambiato una funzionalità o ne avete aggiunta una nuova, non dimenticate di [aggiungerla anche alla documentazione|documentation]. - - -Nuovo ramo -========== - -Se possibile, fate le modifiche a partire dall'ultima versione rilasciata, cioè dall'ultimo tag del ramo. Per il tag `v3.2.1` create un ramo con questo comando: - -```shell -git checkout -b nome_nuovo_ramo v3.2.1 -``` - - -Standard di codifica -==================== - -Il vostro codice deve rispettare lo [standard di codifica|coding-standard] usato nel Nette Framework. Per verificare e correggere automaticamente il vostro codice usate lo strumento [Nette Coding Standard |tools:coding-standard], dove trovate anche le istruzioni di installazione e uso. - - -Descrizione del commit -====================== - -In Nette il soggetto dei commit ha questo formato: `Presenter: fixed AJAX detection [Closes #69]` - -- L'area seguita dai due punti -- Lo scopo del commit al passato; se possibile cominciate con parole come: "added (nuova funzionalità)", "fixed (correzione)", "refactored (modifica del codice senza cambio di comportamento)", "changed", "removed" -- Se il commit rompe la retrocompatibilità, aggiungete "BC break" -- Un eventuale link all'issue tracker, per esempio `(#123)` oppure `[Closes #69]` -- Dopo il soggetto può esserci una riga vuota seguita da una descrizione più dettagliata, comprensiva per esempio di link al forum - - -Descrizione della pull request -============================== - -Quando create una pull request, l'interfaccia di GitHub vi permette di inserire un titolo e una descrizione. Indicate un titolo conciso e mettete nella descrizione quante più informazioni possibili sui motivi della vostra modifica. - -Indicate inoltre nell'intestazione se si tratta di una nuova funzionalità o di una correzione di bug e se può causare problemi di retrocompatibilità (BC break). Se esiste un issue collegato, mettete un link a esso perché venga chiuso all'approvazione della pull request. - -``` -- bug fix / new feature? <!-- #issue numbers, if any --> -- BC break? yes/no -- doc PR: nette/docs#? <!-- highly welcome, see https://nette.org/en/writing --> -``` - - -{{priority: -1}} diff --git a/contributing/it/coding-standard.texy b/contributing/it/coding-standard.texy deleted file mode 100644 index dc2a4592bc..0000000000 --- a/contributing/it/coding-standard.texy +++ /dev/null @@ -1,148 +0,0 @@ -Standard di codifica -******************** - -.[perex] -Questo documento descrive le regole e i consigli per lo sviluppo di Nette. Quando contribuite al codice di Nette dovete rispettarli. Il modo più semplice per farlo è imitare il codice esistente. L'obiettivo è che tutto il codice sembri scritto da una sola persona. - -Il Nette Coding Standard corrisponde a [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/] con due eccezioni principali: usa le [tabulazioni invece degli spazi |#Tabulazioni invece di spazi] per l'indentazione e usa il [PascalCase per le costanti di classe|https://blog.nette.org/en/for-less-screaming-in-the-code]. - -.[tip] -Molte di queste regole si possono controllare e correggere automaticamente con lo strumento [Nette Coding Standard |tools:coding-standard], quindi non dovete verificarle a mano. - - -Regole generali -=============== - -- Ogni file PHP deve contenere `declare(strict_types=1)` -- Per separare i metodi si usano due righe vuote, per una migliore leggibilità -- Il motivo per cui si usa l'operatore di soppressione (`@`) va documentato: `@mkdir($dir); // @ - directory may exist` -- Se si usa un operatore di confronto debole (cioè `==`, `!=`, ...), l'intenzione va documentata: `// == to accept null` -- Potete scrivere più classi di eccezione in un unico file chiamato `exceptions.php` e più enum in `enums.php` -- Per le interfacce non si indica la visibilità dei metodi, perché sono sempre pubblici -- Ogni proprietà, valore di ritorno e parametro deve avere il tipo indicato. Al contrario, per le costanti final non indichiamo mai il tipo, perché è ovvio -- Per delimitare le stringhe si usano gli apici singoli, tranne quando il letterale stesso contiene apostrofi - - -Convenzioni di denominazione -============================ - -- Evitate le abbreviazioni, a meno che il nome completo non sia eccessivo -- Usate le maiuscole per le abbreviazioni di due lettere e PascalCase/camelCase per quelle più lunghe -- Per il nome della classe usate un sostantivo o una locuzione nominale -- I nomi delle classi devono contenere non solo la specificità (`Array`), ma anche la genericità (`ArrayIterator`). Gli attributi PHP fanno eccezione -- "Le costanti di classe e gli enum dovrebbero usare il PascalCaps":https://blog.nette.org/en/for-less-screaming-in-the-code -- "Le interfacce e le classi astratte non dovrebbero contenere prefissi o suffissi":https://blog.nette.org/en/prefixes-and-suffixes-do-not-belong-in-interface-names come `Abstract`, `Interface` oppure `I` - - -A capo e parentesi -================== - -Il Nette Coding Standard corrisponde a PSR-12 (o PER Coding Style), ma lo precisa o lo modifica in alcuni punti: - -- Le arrow function si scrivono senza spazio prima della parentesi, cioè `fn($a) => $b` -- Tra i diversi tipi di istruzioni di import `use` non serve una riga vuota -- Il tipo di ritorno di una funzione o di un metodo e la parentesi graffa di apertura stanno sempre su righe separate: - -```php - public function find( - string $dir, - array $options, - ): array - { - // corpo del metodo - } -``` - -La parentesi graffa di apertura su una riga separata è importante per separare visivamente la firma della funzione o del metodo dal corpo. Se la firma sta su una riga, la separazione è chiara (immagine a sinistra). Se sta su più righe, in PSR la firma e il corpo si fondono (al centro), mentre nello standard Nette restano separati (a destra): - -[* new-line-after.webp *] - - -Blocchi di documentazione (phpDoc) -================================== - -La regola principale: **non duplicate mai** un'informazione della firma, come il tipo di un parametro o il tipo di ritorno, senza aggiungere valore. - -Blocco di documentazione per la definizione di una classe: - -- Inizia con la descrizione della classe -- Segue una riga vuota -- Seguono le annotazioni `@property` (oppure `@property-read`, `@property-write`), una per riga. Sintassi: annotazione, spazio, tipo, spazio, `$nome` -- Seguono le annotazioni `@method`, una per riga. Sintassi: annotazione, spazio, tipo di ritorno, spazio, `nome(tipo $param, ...)` -- L'annotazione `@author` si omette. La paternità è conservata nella storia del codice sorgente -- Si possono usare le annotazioni `@internal` oppure `@deprecated` - -```php -/** - * MIME message part. - * - * @property string $encoding - * @property-read array $headers - * @method string getSomething(string $name) - * @method static bool isEnabled() - */ -``` - -Un blocco di documentazione di una proprietà che contiene solo l'annotazione `@var` dovrebbe stare su una sola riga: - -```php -/** @var string[] */ -private array $name; -``` - -Blocco di documentazione per la definizione di un metodo: - -- Inizia con una breve descrizione del metodo -- Nessuna riga vuota -- Annotazioni `@param`, una per riga -- Annotazione `@return` -- Annotazioni `@throws`, una per riga -- Si possono usare le annotazioni `@internal` oppure `@deprecated` - -Dopo ogni annotazione c'è uno spazio, tranne dopo `@param`, dopo la quale ce ne sono due per una migliore leggibilità. - -```php -/** - * Finds a file in directory. - * @param string[] $options - * @return string[] - * @throws DirectoryNotFoundException - */ -public function find(string $dir, array $options): array -``` - - -Funzioni e costanti globali -=========================== - -Le funzioni e le costanti globali si scrivono senza la barra rovesciata iniziale, cioè `count($arr)` e non `\count($arr)`. Per le funzioni che PHP sa ottimizzare aggiungete all'inizio del file `use function`, così il compilatore le può tradurre in modo più efficiente. Si tratta di funzioni come `count`, `strlen`, `is_array`, `is_string`, `is_scalar`, `sprintf` ecc. Le funzioni si elencano su una sola riga, per mantenere compatto il blocco degli import: - -```php -use Nette; -use function count, is_array, is_scalar, sprintf; -``` - -Ogni tanto importiamo anche le costanti la cui conoscenza del valore può aiutare il compilatore: - -```php -use const PHP_OS_FAMILY; -``` - - -Tabulazioni invece di spazi -=========================== - -Le tabulazioni hanno diversi vantaggi rispetto agli spazi: - -- La dimensione dell'indentazione è personalizzabile negli editor e sul "web":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size -- Non impongono al codice la preferenza dell'utente sulla dimensione dell'indentazione, il che rende il codice più portabile -- Si digitano con un solo tasto (ovunque, non solo negli editor che convertono le tabulazioni in spazi) -- L'indentazione è il loro scopo -- Rispettano le esigenze dei colleghi ipovedenti e non vedenti - -Usando le tabulazioni nei nostri progetti permettiamo di personalizzare la larghezza, cosa che alla maggior parte delle persone può sembrare superflua, ma che per le persone con disabilità visive è essenziale. - -Per i programmatori non vedenti che usano display braille, ogni spazio rappresenta una cella braille. Se quindi l'indentazione predefinita è di 4 spazi, un'indentazione di terzo livello spreca 12 preziose celle braille prima ancora che cominci il codice. Su un display da 40 celle, il più diffuso per i portatili, è più di un quarto delle celle disponibili sprecato senza fornire alcuna informazione. - - -{{priority: -1}} diff --git a/contributing/it/documentation.texy b/contributing/it/documentation.texy deleted file mode 100644 index 3d8bc2dbb6..0000000000 --- a/contributing/it/documentation.texy +++ /dev/null @@ -1,68 +0,0 @@ -Contribuire alla documentazione -******************************* - -.[perex] -Contribuire alla documentazione è una delle attività più preziose, perché aiuta gli altri a capire il framework. - - -Come scrivere? --------------- - -La documentazione è pensata anzitutto per chi si avvicina all'argomento per la prima volta. Dovrebbe quindi soddisfare alcuni punti importanti: - -- Cominciate dai concetti semplici e generali. Passate ai temi più avanzati solo alla fine. -- Cercate di spiegare l'argomento nel modo più chiaro possibile. Provate per esempio a spiegarlo prima a un collega. -- Fornite solo le informazioni che l'utente ha davvero bisogno di sapere su quell'argomento. -- Verificate che le vostre informazioni siano esatte. Provate ogni pezzo di codice. -- Siate concisi: dimezzate quello che scrivete. E poi fatelo pure di nuovo. -- Usate con parsimonia le evidenziazioni, dal testo in grassetto ai riquadri come `.[note]`. -- Negli esempi di codice rispettate lo [standard di codifica|coding-standard]. - -Imparate anche la [sintassi |syntax]. Per vedere l'anteprima dell'articolo mentre scrivete potete usare l'[editor di anteprima |https://editor.nette.org/]. - - -Versioni linguistiche ---------------------- - -La lingua principale è l'inglese, quindi idealmente le vostre modifiche dovrebbero essere in inglese. Se l'inglese non è il vostro forte, usate [DeepL Translator |https://www.deepl.com/translator] e altri rivedranno il vostro testo. - -La traduzione nelle altre lingue avverrà automaticamente dopo che la vostra modifica sarà approvata e finalizzata. - - -Modifiche minime ----------------- - -Per contribuire alla documentazione dovete avere un account su [GitHub |https://github.com]. - -Il modo più semplice di fare una piccola modifica nella documentazione è usare i link in fondo a ogni pagina: - -- *Mostra su GitHub* apre la versione sorgente della pagina su GitHub. Poi basta premere il tasto `E` per cominciare a modificare (dovete essere connessi a GitHub). -- *Apri anteprima* apre un editor in cui vedete subito l'aspetto visivo finale. - -Poiché l'[editor di anteprima |https://editor.nette.org/] non sa salvare le modifiche direttamente su GitHub, dopo aver finito di modificare dovete copiare il testo sorgente negli appunti (con il pulsante *Copia negli appunti*) e poi incollarlo nell'editor su GitHub. Sotto il campo di modifica c'è un modulo di invio. Qui non dimenticate di riassumere brevemente e spiegare il motivo della vostra modifica. Dopo l'invio viene creata una pull request (PR), che si può modificare ulteriormente. - - -Modifiche più ampie -------------------- - -Invece di affidarvi solo all'interfaccia di GitHub, è meglio conoscere le basi del lavoro con il sistema di versionamento Git. Se non conoscete Git, potete consultare [git - the simple guide |https://rogerdudler.github.io/git-guide/] e considerare l'uso di uno dei tanti [client grafici |https://git-scm.com/downloads/guis] disponibili. - -Modificate la documentazione così: - -1) Su GitHub create un [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] del repository [nette/docs |https://github.com/nette/docs]. -2) [Clonate |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] questo repository sul vostro computer. -3) Poi fate le modifiche nel [ramo appropriato |#Struttura della documentazione]. -4) Controllate che nel testo non ci siano spazi superflui con lo strumento [Code-Checker |tools:code-checker]. -5) Salvate (commit) le modifiche. -6) Se siete soddisfatti delle modifiche, inviatele (push) su GitHub nel vostro fork. -7) Da lì proponetele al repository `nette/docs` creando una [pull request|https://help.github.com/articles/creating-a-pull-request] (PR). - -È normale ricevere commenti con dei suggerimenti. Seguite le modifiche proposte e incorporatele. Aggiungete le modifiche proposte come nuovi commit e inviatele di nuovo su GitHub. Non create mai una nuova pull request solo per modificarne una esistente. - - -Struttura della documentazione ------------------------------- - -Tutta la documentazione si trova su GitHub nel repository [nette/docs |https://github.com/nette/docs]. La versione attuale è nel ramo `master`, mentre le versioni più vecchie stanno in rami come `doc-3.x`, `doc-2.x`. - -Il contenuto di ogni ramo è diviso in cartelle principali che rappresentano le singole aree della documentazione. Per esempio `application/` corrisponde a `https://doc.nette.org/en/application`, `latte/` corrisponde a `https://latte.nette.org` ecc. Ognuna di queste cartelle contiene sottocartelle che rappresentano le versioni linguistiche (`cs`, `en`, ...) ed eventualmente una sottocartella `files` con le immagini che si possono inserire nelle pagine della documentazione. diff --git a/contributing/it/syntax.texy b/contributing/it/syntax.texy deleted file mode 100644 index 7ab5398618..0000000000 --- a/contributing/it/syntax.texy +++ /dev/null @@ -1,143 +0,0 @@ -Sintassi della documentazione -***************************** - -La documentazione usa Markdown e la [sintassi di Texy |https://texy.deegee.dev/syntax] con alcune aggiunte. - - -Link -==== - -Per i link interni si usa la notazione tra parentesi quadre `[link]`. O nella forma con la barra verticale `[testo del link |destinazione del link]`, oppure nella forma abbreviata `[testo del link]` se la destinazione coincide con il testo (dopo la conversione in minuscolo e con i trattini): - -- `[Nome pagina]` -> `<a href="/it/nome-pagina">Nome pagina</a>` -- `[testo del link |Nome pagina]` -> `<a href="/it/nome-pagina">testo del link</a>` - -Possiamo rimandare a un'altra versione linguistica o a un'altra sezione. Per sezione si intende una libreria di Nette (per esempio `forms`, `latte` ecc.) oppure sezioni particolari come `best-practices`, `quickstart` ecc.: - -- `[cs:Nome pagina]` -> `<a href="/cs/nome-pagina">Nome pagina</a>` (stessa sezione, lingua diversa) -- `[tracy:Nome pagina]` -> `<a href="//tracy.nette.org/it/nome-pagina">Nome pagina</a>` (sezione diversa, stessa lingua) -- `[tracy:cs:Nome pagina]` -> `<a href="//tracy.nette.org/cs/nome-pagina">Nome pagina</a>` (sezione e lingua diverse) - -Con `#` si può puntare anche a un'intestazione specifica della pagina. - -- `[#Intestazione]` -> `<a href="#toc-intestazione">Intestazione</a>` (intestazione della pagina corrente) -- `[Nome pagina#Intestazione]` -> `<a href="/it/nome-pagina#toc-intestazione">Nome pagina</a>` - -Link alla pagina principale della sezione: (`@home` è un termine speciale per la pagina principale della sezione) - -- `[testo del link |@home]` -> `<a href="/it/">testo del link</a>` -- `[testo del link |tracy:]` -> `<a href="//tracy.nette.org/it/">testo del link</a>` - - -Link alla documentazione dell'API ---------------------------------- - -Usate sempre questa notazione: - -- `[api:Nette\SmartObject]` -> [api:Nette\SmartObject] -- `[api:Nette\Forms\Form::setTranslator()]` -> [api:Nette\Forms\Form::setTranslator()] -- `[api:Nette\Forms\Form::$onSubmit]` -> [api:Nette\Forms\Form::$onSubmit] -- `[api:Nette\Forms\Form::Required]` -> [api:Nette\Forms\Form::Required] - -Usate i nomi completi solo alla prima menzione. Per i link successivi usate un nome semplificato: - -- `[Form::setTranslator() |api:Nette\Forms\Form::setTranslator()]` -> [Form::setTranslator() |api:Nette\Forms\Form::setTranslator()] - - -Link alla documentazione di PHP -------------------------------- - -- `[php:substr]` -> [php:substr] - - -Codice sorgente -=============== - -Un blocco di codice inizia con <code>```lang</code> e finisce con <code>```</code>. Le lingue supportate sono `php`, `latte`, `neon`, `html`, `css`, `js` e `sql`. Per l'indentazione usate sempre le tabulazioni. - -``` - ```php - public function renderPage($id) - { - } - ``` -``` - -Potete indicare anche il nome del file come <code>```php .{file: ArrayTest.php}</code> e il blocco di codice verrà renderizzato così: - -```php .{file: ArrayTest.php} -public function renderPage($id) -{ -} -``` - - -Intestazioni -============ - -L'intestazione principale (il nome della pagina) si sottolinea con gli asterischi (`*`). Per separare le sezioni usate i segni di uguale (`=`). Le intestazioni si sottolineano prima con i segni di uguale (`=`) e poi con i trattini (`-`): - -``` -MVC Applications & Presenters -***************************** -... - - -Link Creation -============= -... - - -Links in Templates ------------------- -... -``` - - -Riquadri e stili -================ - -Perex contrassegnato con la classe `.[perex]` .[perex] - -Nota contrassegnata con la classe `.[note]` .[note] - -Suggerimento contrassegnato con la classe `.[tip]` .[tip] - -Avvertenza contrassegnata con la classe `.[caution]` .[caution] - -Avviso forte contrassegnato con la classe `.[warning]` .[warning] - -Numero di versione `.{data-version:2.4.10}` .{data-version:2.4.10} - -Le classi si scrivono prima della riga a cui si riferiscono: - -``` -.[perex] -Questo è il perex. -``` - -Tenete presente che i riquadri come `.[tip]` attirano l'attenzione e andrebbero quindi usati per evidenziare informazioni importanti, non dettagli secondari. Usateli con parsimonia. - - -Indice -====== - -L'indice (i link nella barra laterale destra) viene generato automaticamente per tutte le pagine che superano i 4.000 byte. Questo comportamento predefinito si può modificare con i [meta tag |#Meta tag] `{{toc}}`. Il testo dell'indice viene preso per impostazione predefinita direttamente dalle intestazioni, ma si può mostrare un testo diverso con il modificatore `.{toc}`, il che torna utile per le intestazioni più lunghe. - -``` - - -Intestazione lunga e intelligente .{toc: Un testo diverso per l'indice} -======================================================================= -``` - - -Meta tag -======== - -- Impostare un titolo personalizzato della pagina (in `<title>` e nel breadcrumb): `{{title: Altro nome}}` -- Redirect: `{{redirect: pla:cs}}` - vedi [#Link] -- Forzare `{{toc}}` oppure disattivare `{{toc: no}}` l'indice automatico (il riquadro con i link alle intestazioni). -- Impostare il menu a sinistra `{{leftbar: utils:@left-menu}}` oppure disattivarlo `{{leftbar: no}}`. - -{{priority: -1}} diff --git a/contributing/ja/@home.texy b/contributing/ja/@home.texy deleted file mode 100644 index cb9d9cb30b..0000000000 --- a/contributing/ja/@home.texy +++ /dev/null @@ -1,17 +0,0 @@ -Nette に貢献しよう -************ - -.[perex] -私たちのオープンソースのプロジェクトにどう参加できるかをご紹介します。ソースコードとドキュメントに貢献する手順を学び、Nette を良くしていく開発者の集まりの一員になってください。 - - -**コード** - -- [コードへの貢献 |code] -- [コーディング規約 |coding-standard] - -**ドキュメント** - -- [ドキュメントへの貢献 |documentation] -- [ドキュメントの記法 |syntax] -- "プレビューエディタ":https://editor.nette.org diff --git a/contributing/ja/@left-menu.texy b/contributing/ja/@left-menu.texy deleted file mode 100644 index 92c36a8576..0000000000 --- a/contributing/ja/@left-menu.texy +++ /dev/null @@ -1,18 +0,0 @@ -コード -*** -- [コードへの貢献 |code] -- [コーディング規約 |coding-standard] - -ドキュメント -****** -- [ドキュメントへの貢献 |documentation] -- [ドキュメントの記法 |syntax] -- "プレビューエディタ":https://editor.nette.org - - -関連情報 -**** -- [Nette ドキュメント |nette:] -- [ツール |tools:] -- [Nette を作っている人たち |https://nette.org/contributors] -- [GitHub の Nette |https://github.com/nette] diff --git a/contributing/ja/code.texy b/contributing/ja/code.texy deleted file mode 100644 index d0bbc02a5e..0000000000 --- a/contributing/ja/code.texy +++ /dev/null @@ -1,107 +0,0 @@ -コードへの貢献 -******* - -.[perex] -Nette Framework に貢献しようと考えていて、その決まりと手順を知りたいですか。この初めての方への案内が、コードを効果的に貢献し、リポジトリを扱い、変更を実装するまでの手順を導きます。 - - -手順 -=== - -コードを貢献するには、[GitHub|https://github.com]のアカウントを持ち、Git のバージョン管理システムの基本を身につけていることが欠かせません。Git に馴染みがないなら、[git - the simple guide|https://rogerdudler.github.io/git-guide/]を見て、たくさんある[図の操作のクライアント|https://git-scm.com/downloads/guis]のどれかを使うことを考えてみてください。 - - -環境とリポジトリを整える ------------- - -1) GitHub で [fork|https://help.github.com/en/github/getting-started-with-github/fork-a-repo]を作ります。手を入れたい[パッケージのリポジトリ|www:packages]のものです -2) そのリポジトリを自分のコンピュータへ [clone|https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository]します -3) `composer install` のコマンドで、[Nette Tester|tester:]も含めた依存関係を入れます -4) `composer tester` を走らせてテストが動くことを確かめます -5) 最新のリリース版をもとに[新しいブランチ |#新しいブランチ]を作ります - - -自分の変更を実装する ----------- - -これで自分のコードに手を入れられます。 - -1) 望む変更を実装します。テストも忘れずに -2) `composer tester` でテストがうまく通ることを確かめます -3) コードが[コーディング規約 |#コーディング規約]に合っているかを確かめます -4) [この形式 |#コミットの説明]の説明とともに変更を保存(コミット)します - -コミットは複数作れます。論理的な一歩ごとにひとつです。それぞれのコミットは、それだけで意味の通るものにすべきです。 - - -変更を送る ------ - -変更に満足したら、それを送れます。 - -1) 変更を GitHub の自分の fork へ push します -2) そこから [pull request|https://help.github.com/articles/creating-a-pull-request](PR)を作って Nette のリポジトリへ送ります -3) 説明には[十分な情報 |#Pull request の説明]を書きます - - -意見を取り入れる --------- - -これであなたのコミットがほかの人にも見えるようになりました。提案のコメントを受け取るのはよくあることです。 - -1) 提案された変更を追いかけます -2) それを新しいコミットとして取り入れるか、[前のコミットと合わせます|https://help.github.com/en/github/using-git/about-git-rebase] -3) コミットを GitHub へ送り直すと、pull request に自動的に現れます - -既存の pull request を直すために、新しい pull request を作っては決していけません。 - - -ドキュメント ------- - -機能を変えたり新しく足したりしたなら、[ドキュメントに足す|documentation]のも忘れないでください。 - - -新しいブランチ -======= - -できるなら、最新のリリース版、つまりブランチの最後のタグに対して変更を加えてください。タグ `v3.2.1` なら、次のコマンドでブランチを作ります。 - -```shell -git checkout -b new_branch_name v3.2.1 -``` - - -コーディング規約 -======== - -あなたのコードは、Nette Framework で使われている[コーディング規約|coding-standard]に合っていなければなりません。コードを確かめて自動的に直すには、[Nette Coding Standard |tools:coding-standard]の道具を使ってください。そこにはインストールと使い方の説明もあります。 - - -コミットの説明 -======= - -Nette では、コミットの主題は次の形式です。`Presenter: fixed AJAX detection [Closes #69]` - -- 領域とそれに続くコロン -- コミットの目的を過去形で。できれば「added(新機能)」「fixed(修正)」「refactored(振る舞いを変えないコードの変更)」「changed」「removed」のような語で始めます -- そのコミットが後方互換を壊すなら「BC break」を足します -- 課題の管理へのリンクがあれば、`(#123)` や `[Closes #69]` のように書きます -- 主題のあとには空行をひとつ置いて、フォーラムへのリンクなどを含むより詳しい説明を書けます - - -Pull request の説明 -================ - -pull request を作るとき、GitHub の画面で題名と説明を入れられます。題名は簡潔にして、説明にはその変更の理由についてできるだけ多くの情報を書いてください。 - -また、それが新機能か不具合の修正か、そして後方互換の問題(BC break)を起こしうるかを見出しに書いてください。関連する課題があればリンクしてください。そうすれば pull request が受け入れられたときにそれが閉じられます。 - -``` -- bug fix / new feature? <!-- #issue numbers, if any --> -- BC break? yes/no -- doc PR: nette/docs#? <!-- highly welcome, see https://nette.org/en/writing --> -``` - - -{{priority: -1}} diff --git a/contributing/ja/coding-standard.texy b/contributing/ja/coding-standard.texy deleted file mode 100644 index 0b6c7f5a3a..0000000000 --- a/contributing/ja/coding-standard.texy +++ /dev/null @@ -1,148 +0,0 @@ -コーディング規約 -******** - -.[perex] -この文書は Nette を開発するときの決まりとおすすめをまとめたものです。Nette にコードを貢献するときは、これに従わなければなりません。いちばん簡単なやり方は、既存のコードをまねることです。目指すのは、すべてのコードがひとりの人によって書かれたように見えることです。 - -Nette のコーディング規約は [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/]に沿っていますが、大きな違いが 2 つあります。字下げに[空白ではなくタブ |#空白ではなくタブ]を使うことと、[クラスの定数に PascalCase|https://blog.nette.org/en/for-less-screaming-in-the-code]を使うことです。 - -.[tip] -これらの決まりの多くは [Nette Coding Standard |tools:coding-standard]の道具が自動的に確かめて直せるので、手で確かめる必要はありません。 - - -全般の決まり -====== - -- すべての PHP のファイルには `declare(strict_types=1)` を入れます -- 読みやすさのために、メソッドのあいだは空行 2 つで区切ります -- 黙らせる演算子(`@`)を使う理由は書き残さなければなりません。`@mkdir($dir); // @ - directory may exist` -- 緩い型の比較の演算子(つまり `==`、`!=` など)を使うなら、その意図を書き残さなければなりません。`// == to accept null` -- 例外のクラスは `exceptions.php` という名前のひとつのファイルに複数書けますし、enum は `enums.php` に複数書けます -- インターフェースではメソッドの可視性を書きません。いつも public だからです -- すべてのプロパティ、戻り値、パラメータには型を指定しなければなりません。逆に final の定数には型を決して書きません。自明だからです -- 文字列は単一引用符で囲むべきです。ただしその文字列自身がアポストロフィを含む場合は別です - - -名前の付け方 -====== - -- 完全な名前が長すぎる場合を除き、省略形は避けます -- 2 文字の省略形は大文字にし、それより長い省略形は PascalCase/camelCase にします -- クラス名には名詞か名詞句を使います -- クラス名には具体さ(`Array`)だけでなく一般さ(`ArrayIterator`)も含めなければなりません。PHP のアトリビュートは例外です -- "クラスの定数と enum は PascalCaps を使うべきです":https://blog.nette.org/en/for-less-screaming-in-the-code -- "インターフェースと抽象クラスには接頭辞も接尾辞も付けるべきではありません":https://blog.nette.org/en/prefixes-and-suffixes-do-not-belong-in-interface-names `Abstract`、`Interface`、`I` のようなものです - - -改行と波かっこ -======= - -Nette のコーディング規約は PSR-12(または PER Coding Style)に沿っていますが、いくつかの点でそれを細かく定めたり変えたりしています。 - -- アロー関数はかっこの前に空白を入れずに書きます。つまり `fn($a) => $b` です -- 種類の違う `use` の import の文のあいだに空行は要りません -- 関数やメソッドの戻り値の型と、開く波かっこは、いつも別々の行に置きます。 - -```php - public function find( - string $dir, - array $options, - ): array - { - // method body - } -``` - -開く波かっこを別の行に置くのは、関数やメソッドの見出しを本体から目で分けるために大事です。見出しが 1 行なら区切りははっきりしています(左の図)。複数行なら、PSR では見出しと本体が溶け合ってしまいますが(真ん中)、Nette の規約では分かれたままです(右)。 - -[* new-line-after.webp *] - - -文書のかたまり(phpDoc) -=============== - -いちばんの決まりはこれです。**価値を足さずに**、パラメータの型や戻り値の型のような見出しの情報を**重ねて書かないこと**。 - -クラスの定義の文書のかたまりです。 - -- クラスの説明から始めます -- 続いて空行 -- 続いて `@property`(あるいは `@property-read`、`@property-write`)のアノテーションを 1 行にひとつずつ。書き方は、アノテーション、空白、型、空白、`$name` -- 続いて `@method` のアノテーションを 1 行にひとつずつ。書き方は、アノテーション、空白、戻り値の型、空白、`name(type $param, ...)` -- `@author` のアノテーションは書きません。誰が書いたかはソースコードの履歴に残ります -- `@internal` や `@deprecated` のアノテーションは使えます - -```php -/** - * MIME message part. - * - * @property string $encoding - * @property-read array $headers - * @method string getSomething(string $name) - * @method static bool isEnabled() - */ -``` - -`@var` のアノテーションだけを含むプロパティの文書のかたまりは、1 行に書くべきです。 - -```php -/** @var string[] */ -private array $name; -``` - -メソッドの定義の文書のかたまりです。 - -- メソッドの短い説明から始めます -- 空行は入れません -- `@param` のアノテーションを 1 行にひとつずつ -- `@return` のアノテーション -- `@throws` のアノテーションを 1 行にひとつずつ -- `@internal` や `@deprecated` のアノテーションは使えます - -どのアノテーションのうしろにも空白をひとつ置きます。ただし `@param` だけは、読みやすさのために空白 2 つを置きます。 - -```php -/** - * Finds a file in directory. - * @param string[] $options - * @return string[] - * @throws DirectoryNotFoundException - */ -public function find(string $dir, array $options): array -``` - - -大域の関数と定数 -======== - -大域の関数と定数は先頭のバックスラッシュなしで書きます。つまり `\count($arr)` ではなく `count($arr)` です。PHP が最適化できる関数には、コンパイラがより効率よく翻訳できるよう、ファイルの先頭に `use function` を足します。`count`、`strlen`、`is_array`、`is_string`、`is_scalar`、`sprintf` などの関数がそれに当たります。import のかたまりをこぢんまり保つために、関数は 1 行に並べます。 - -```php -use Nette; -use function count, is_array, is_scalar, sprintf; -``` - -ときには、その値を知ることがコンパイラの助けになる定数も import します。 - -```php -use const PHP_OS_FAMILY; -``` - - -空白ではなくタブ -======== - -タブには空白に対していくつかの利点があります。 - -- 字下げの大きさをエディタでも "ウェブ":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size でも変えられます -- 書き手の好みの字下げの大きさをコードに押し付けないので、コードが持ち運びやすくなります -- ひと打ちで入力できます(タブを空白に変えるエディタに限らず、どこでもです) -- 字下げこそがその役目です -- 目の不自由な仲間や見えない仲間の必要に応えます - -プロジェクトでタブを使うことで幅を変えられるようにしています。多くの人には要らないことに見えるかもしれませんが、目の不自由な人には欠かせません。 - -点字ディスプレイを使う目の見えないプログラマーにとって、空白ひとつは点字のマスひとつです。ですから既定の字下げが空白 4 つなら、3 段めの字下げは、コードが始まる前に貴重な点字のマスを 12 個も無駄にします。ノートパソコンでもっともよくある 40 マスのディスプレイなら、それは使えるマスの 4 分の 1 以上を、何の情報も与えずに無駄にすることになります。 - - -{{priority: -1}} diff --git a/contributing/ja/documentation.texy b/contributing/ja/documentation.texy deleted file mode 100644 index c332519c91..0000000000 --- a/contributing/ja/documentation.texy +++ /dev/null @@ -1,68 +0,0 @@ -ドキュメントへの貢献 -********** - -.[perex] -ドキュメントへの貢献はもっとも価値ある活動のひとつです。ほかの人がフレームワークを理解する助けになるからです。 - - -どう書くか ------ - -ドキュメントは主に、その話題にはじめて触れる人のためのものです。ですからいくつかの大事な点を満たすべきです。 - -- 単純で一般的な考え方から始めます。より進んだ話題へ進むのは最後にします。 -- できるだけ分かりやすく説明するよう努めます。たとえば、まず同僚に説明してみてください。 -- その話題で利用者が本当に必要とする情報だけを書きます。 -- 書いた情報が正しいかを確かめます。すべてのコードを試してください。 -- 簡潔にします。書いたものを半分に削ってください。そしてもう一度やっても構いません。 -- 強調は控えめに使います。太字から `.[note]` のような囲みまで同じです。 -- コードの例では[コーディング規約|coding-standard]に従ってください。 - -[記法 |syntax]も学んでください。書きながら記事を下見するには、[プレビューエディタ |https://editor.nette.org/]を使えます。 - - -言語版 ---- - -英語が主の言語なので、変更は英語であるのが理想です。英語が得意でないなら [DeepL 翻訳 |https://www.deepl.com/translator]を使ってください。ほかの人が文を見直します。 - -ほかの言語への翻訳は、あなたの変更が受け入れられて確定したあとに自動的に行われます。 - - -ごく小さな修正 -------- - -ドキュメントに貢献するには、[GitHub |https://github.com]のアカウントが要ります。 - -ドキュメントに小さな変更を加えるいちばん簡単な方法は、各ページの終わりのリンクを使うことです。 - -- *Show on GitHub* は GitHub でそのページのもとの版を開きます。あとは `E` のキーを押せば編集を始められます(GitHub にログインしている必要があります)。 -- *Open preview* はエディタを開き、最終的な見た目をすぐに確かめられます。 - -[プレビューエディタ |https://editor.nette.org/]は変更を GitHub へ直接保存できないので、編集を終えたら(*Copy to clipboard* のボタンで)もとの文をクリップボードにコピーし、GitHub のエディタへ貼り付ける必要があります。編集の欄の下には送信のフォームがあります。ここに、あなたの修正の理由を手短にまとめて説明するのを忘れないでください。送信すると pull request(PR)が作られ、そのあとも編集できます。 - - -大きめの修正 ------- - -GitHub の画面だけに頼るより、Git のバージョン管理システムの基本を身につけておくほうがよいでしょう。Git に馴染みがないなら、[git - the simple guide |https://rogerdudler.github.io/git-guide/]を見て、たくさんある[図の操作のクライアント |https://git-scm.com/downloads/guis]のどれかを使うことを考えてみてください。 - -ドキュメントは次のように編集します。 - -1) GitHub で [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo]を作ります。[nette/docs |https://github.com/nette/docs]のリポジトリのものです。 -2) そのリポジトリを自分のコンピュータへ [clone |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository]します。 -3) そして[ふさわしいブランチ |#ドキュメントの構造]で変更を加えます。 -4) [Code-Checker |tools:code-checker]の道具で、文に余計な空白がないかを確かめます。 -5) 変更を保存(コミット)します。 -6) 変更に満足したら、それを GitHub の自分の fork へ push します。 -7) そこから [pull request|https://help.github.com/articles/creating-a-pull-request](PR)を作って `nette/docs` のリポジトリへ送ります。 - -提案のコメントを受け取るのはよくあることです。提案された変更を追いかけて取り入れてください。提案された変更は新しいコミットとして足し、GitHub へまた push します。既存の pull request を直すためだけに、新しい pull request を作っては決していけません。 - - -ドキュメントの構造 ---------- - -ドキュメント全体は GitHub の [nette/docs |https://github.com/nette/docs]のリポジトリにあります。今の版は `master` のブランチに、古い版は `doc-3.x`、`doc-2.x` のようなブランチにあります。 - -それぞれのブランチの中身は、ドキュメントの領域ごとの主なフォルダに分かれています。たとえば `application/` は `https://doc.nette.org/en/application` に、`latte/` は `https://latte.nette.org` に対応します。これらのフォルダにはそれぞれ、言語版を表す下位のフォルダ(`cs`、`en` など)と、必要なら、ドキュメントのページに差し込める画像の入った `files` の下位のフォルダがあります。 diff --git a/contributing/ja/syntax.texy b/contributing/ja/syntax.texy deleted file mode 100644 index ab7b90211b..0000000000 --- a/contributing/ja/syntax.texy +++ /dev/null @@ -1,143 +0,0 @@ -ドキュメントの記法 -********* - -ドキュメントは Markdown と [Texy の記法 |https://texy.deegee.dev/syntax]に、いくつかの拡張を加えたものを使います。 - - -リンク -=== - -内部のリンクには角かっこの書き方 `[link]` を使います。これは縦棒を使う形 `[リンクの文 |リンクの行き先]`、あるいは行き先が文と同じ場合(小文字とハイフンへ変換したうえで)の短い形 `[リンクの文]` です。 - -- `[Page name]` -> `<a href="/en/page-name">Page name</a>` -- `[link text |Page name]` -> `<a href="/en/page-name">link text</a>` - -別の言語版や別の区画へもリンクできます。区画とは Nette のライブラリ(たとえば `forms`、`latte` など)や、`best-practices`、`quickstart` などの特別な区画のことです。 - -- `[cs:Page name]` -> `<a href="/cs/page-name">Page name</a>`(同じ区画、違う言語) -- `[tracy:Page name]` -> `<a href="//tracy.nette.org/en/page-name">Page name</a>`(違う区画、同じ言語) -- `[tracy:cs:Page name]` -> `<a href="//tracy.nette.org/cs/page-name">Page name</a>`(違う区画と言語) - -`#` を使ってページの特定の見出しを指すこともできます。 - -- `[#Heading]` -> `<a href="#toc-heading">Heading</a>`(今のページの見出し) -- `[Page name#Heading]` -> `<a href="/en/page-name#toc-heading">Page name</a>` - -区画のトップページへのリンクです(`@home` は区画のトップページを表す特別な語です)。 - -- `[link text |@home]` -> `<a href="/en/">link text</a>` -- `[link text |tracy:]` -> `<a href="//tracy.nette.org/en/">link text</a>` - - -API のドキュメントへのリンク ----------------- - -いつも次の書き方を使ってください。 - -- `[api:Nette\SmartObject]` -> [api:Nette\SmartObject] -- `[api:Nette\Forms\Form::setTranslator()]` -> [api:Nette\Forms\Form::setTranslator()] -- `[api:Nette\Forms\Form::$onSubmit]` -> [api:Nette\Forms\Form::$onSubmit] -- `[api:Nette\Forms\Form::Required]` -> [api:Nette\Forms\Form::Required] - -完全修飾の名前は最初に触れるときだけ使ってください。そのあとのリンクには短くした名前を使います。 - -- `[Form::setTranslator() |api:Nette\Forms\Form::setTranslator()]` -> [Form::setTranslator() |api:Nette\Forms\Form::setTranslator()] - - -PHP のドキュメントへのリンク ----------------- - -- `[php:substr]` -> [php:substr] - - -ソースコード -====== - -コードのかたまりは <code>```lang</code> で始まり <code>```</code> で終わります。対応している言語は `php`、`latte`、`neon`、`html`、`css`、`js`、`sql` です。字下げにはいつもタブを使ってください。 - -``` - ```php - public function renderPage($id) - { - } - ``` -``` - -<code>```php .{file: ArrayTest.php}</code> のようにファイル名も指定でき、そのコードのかたまりは次のように描かれます。 - -```php .{file: ArrayTest.php} -public function renderPage($id) -{ -} -``` - - -見出し -=== - -いちばん上の見出し(ページの名前)はアスタリスク(`*`)で下線を引きます。節を分けるには等号(`=`)を使います。見出しはまず等号(`=`)で、次にハイフン(`-`)で下線を引きます。 - -``` -MVC Applications & Presenters -***************************** -... - - -Link Creation -============= -... - - -Links in Templates ------------------- -... -``` - - -囲みとスタイル -======= - -クラス `.[perex]` で印を付けた perex .[perex] - -クラス `.[note]` で印を付けた note .[note] - -クラス `.[tip]` で印を付けた tip .[tip] - -クラス `.[caution]` で印を付けた caution .[caution] - -クラス `.[warning]` で印を付けた強い警告 .[warning] - -バージョン番号 `.{data-version:2.4.10}` .{data-version:2.4.10} - -クラスは、それが当たる行の前に書くべきです。 - -``` -.[perex] -これが perex です。 -``` - -`.[tip]` のような囲みは目を引くので、大事な情報を強調するのに使うべきで、あまり重要でない細部には使わないでください。控えめに使ってください。 - - -目次 -=== - -目次(右の欄のリンク)は、大きさが 4,000 バイトを超えるすべてのページに自動的に作られます。この既定の振る舞いは `{{toc}}` の[メタタグ |#メタタグ]で変えられます。目次の文は既定では見出しからそのまま取られますが、`.{toc}` の修飾子で違う文を表示させられます。長い見出しに便利です。 - -``` - - -Long and Intelligent Heading .{toc: A Different Text for TOC} -============================================================= -``` - - -メタタグ -==== - -- ページの題名を独自に決めます(`<title>` とパンくずで)。`{{title: Another name}}` -- リダイレクト。`{{redirect: pla:cs}}` - [#リンク]をご覧ください -- 自動の目次(見出しへのリンクの囲み)を強いる `{{toc}}` か、切る `{{toc: no}}`。 -- 左のメニューを設定する `{{leftbar: utils:@left-menu}}` か、切る `{{leftbar: no}}`。 - -{{priority: -1}} diff --git a/contributing/meta.json b/contributing/meta.json deleted file mode 100644 index 0967ef424b..0000000000 --- a/contributing/meta.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/contributing/pl/@home.texy b/contributing/pl/@home.texy deleted file mode 100644 index afd1a1164b..0000000000 --- a/contributing/pl/@home.texy +++ /dev/null @@ -1,17 +0,0 @@ -Zostań kontrybutorem Nette -************************** - -.[perex] -Dowiedz się, jak możesz zaangażować się w nasz projekt open source. Poznaj procedury kontrybuowania do kodu źródłowego i dokumentacji i stań się częścią społeczności programistów aktywnie uczestniczących w ulepszaniu Nette. - - -**Kod** - -- [Kontrybuowanie do kodu |code] -- [Standardy kodowania |coding-standard] - -**Dokumentacja** - -- [Kontrybuowanie do dokumentacji |documentation] -- [Składnia dokumentacji |syntax] -- "Edytor z podglądem":https://editor.nette.org diff --git a/contributing/pl/@left-menu.texy b/contributing/pl/@left-menu.texy deleted file mode 100644 index ce68b1225b..0000000000 --- a/contributing/pl/@left-menu.texy +++ /dev/null @@ -1,18 +0,0 @@ -Kod -*** -- [Jak współtworzyć kod |code] -- [Standardy kodowania |coding-standard] - -Dokumentacja -************ -- [Jak współtworzyć dokumentację |documentation] -- [Składnia dokumentacji |syntax] -- "Edytor podglądu":https://editor.nette.org - - -Dalsza lektura -************** -- [Dokumentacja Nette |nette:] -- [Narzędzia |tools:] -- [Kto tworzy Nette |https://nette.org/contributors] -- [Nette na GitHubie |https://github.com/nette] diff --git a/contributing/pl/code.texy b/contributing/pl/code.texy deleted file mode 100644 index cf48768c04..0000000000 --- a/contributing/pl/code.texy +++ /dev/null @@ -1,107 +0,0 @@ -Kontrybuowanie do kodu -********************** - -.[perex] -Planujesz kontrybuować do Nette Framework i musisz zapoznać się z regułami i procedurami? Ten przewodnik dla początkujących przeprowadzi Cię przez kroki, jak efektywnie kontrybuować kod, pracować z repozytoriami i wprowadzać zmiany. - - -Procedura -========= - -Żeby kontrybuować kod, konieczne jest posiadanie konta na [GitHubie|https://github.com] i znajomość podstaw pracy z systemem kontroli wersji Git. Jeśli nie znasz Gita, możesz zajrzeć do [git - the simple guide|https://rogerdudler.github.io/git-guide/] i rozważyć użycie jednego z wielu [klientów graficznych|https://git-scm.com/downloads/guis]. - - -Przygotowanie środowiska i repozytorium ---------------------------------------- - -1) Na GitHubie utwórz [fork|https://help.github.com/en/github/getting-started-with-github/fork-a-repo] [repozytorium pakietu|www:packages], który zamierzasz zmodyfikować -2) [Sklonuj|https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] to repozytorium na swój komputer -3) Zainstaluj zależności, wraz z [Nette Testerem|tester:], poleceniem `composer install` -4) Sprawdź, czy testy działają, uruchamiając `composer tester` -5) Utwórz [nową gałąź |#Nowa gałąź] na podstawie ostatnio wydanej wersji - - -Wprowadzanie własnych zmian ---------------------------- - -Teraz możesz wprowadzać własne poprawki w kodzie: - -1) Zaimplementuj pożądane zmiany i nie zapomnij o testach -2) Upewnij się, że testy przechodzą pomyślnie, za pomocą `composer tester` -3) Sprawdź, czy kod spełnia [standardy kodowania |#Standardy kodowania] -4) Zapisz (zacommituj) zmiany z opisem w [tym formacie |#Opis commitu] - -Możesz utworzyć wiele commitów, po jednym na każdy logiczny krok. Każdy commit powinien mieć sens sam w sobie. - - -Zgłaszanie zmian ----------------- - -Gdy jesteś zadowolony ze zmian, możesz je zgłosić: - -1) Wypchnij zmiany na GitHuba do swojego forka -2) Stamtąd zgłoś je do repozytorium Nette, tworząc [pull request|https://help.github.com/articles/creating-a-pull-request] (PR) -3) Podaj w opisie [wystarczające informacje |#Opis pull requesta] - - -Uwzględnianie uwag ------------------- - -Twoje commity są teraz widoczne dla innych. Często dostaje się komentarze z sugestiami: - -1) Śledź proponowane zmiany -2) Uwzględniaj je jako nowe commity albo [łącz je z poprzednimi|https://help.github.com/en/github/using-git/about-git-rebase] -3) Wypchnij commity ponownie na GitHuba, a pojawią się automatycznie w pull requeście - -Nigdy nie twórz nowego pull requesta, żeby zmodyfikować istniejący. - - -Dokumentacja ------------- - -Jeśli zmieniłeś funkcjonalność albo dodałeś nową, nie zapomnij [dodać jej także do dokumentacji|documentation]. - - -Nowa gałąź -========== - -Jeśli to możliwe, wprowadzaj zmiany względem ostatnio wydanej wersji, czyli ostatniego tagu w gałęzi. Dla tagu `v3.2.1` utwórz gałąź tym poleceniem: - -```shell -git checkout -b nazwa_nowej_galezi v3.2.1 -``` - - -Standardy kodowania -=================== - -Twój kod musi spełniać [standard kodowania|coding-standard] używany w Nette Framework. Do sprawdzenia i automatycznej poprawy kodu użyj narzędzia [Nette Coding Standard |tools:coding-standard], gdzie znajdziesz też instrukcje instalacji i użycia. - - -Opis commitu -============ - -W Nette tematy commitów mają następujący format: `Presenter: fixed AJAX detection [Closes #69]` - -- Obszar, po nim dwukropek -- Cel commitu w czasie przeszłym; jeśli to możliwe, zaczynaj od słów: "added (nowa funkcja)", "fixed (poprawka)", "refactored (zmiana kodu bez zmiany zachowania)", "changed", "removed" -- Jeśli commit łamie kompatybilność wsteczną, dodaj "BC break" -- Ewentualny odnośnik do issue trackera, jak `(#123)` albo `[Closes #69]` -- Za tematem może być jedna pusta linia, a po niej bardziej szczegółowy opis wraz z, na przykład, odnośnikami do forum - - -Opis pull requesta -================== - -Przy tworzeniu pull requesta interfejs GitHuba pozwoli Ci wpisać tytuł i opis. Podaj zwięzły tytuł i umieść w opisie jak najwięcej informacji o powodach swojej zmiany. - -Podaj też w nagłówku, czy chodzi o nową funkcję, czy o poprawkę błędu i czy może to spowodować problemy z kompatybilnością wsteczną (BC break). Jeśli istnieje powiązane issue, podlinkuj je, żeby zostało zamknięte po zatwierdzeniu pull requesta. - -``` -- bug fix / new feature? <!-- #issue numbers, if any --> -- BC break? yes/no -- doc PR: nette/docs#? <!-- highly welcome, see https://nette.org/en/writing --> -``` - - -{{priority: -1}} diff --git a/contributing/pl/coding-standard.texy b/contributing/pl/coding-standard.texy deleted file mode 100644 index c4eccea1f9..0000000000 --- a/contributing/pl/coding-standard.texy +++ /dev/null @@ -1,148 +0,0 @@ -Standard kodowania -****************** - -.[perex] -Ten dokument opisuje reguły i zalecenia dotyczące rozwoju Nette. Kontrybuując kod do Nette, musisz się do nich stosować. Najprościej zrobisz to, naśladując istniejący kod. Celem jest, żeby cały kod wyglądał tak, jakby napisała go jedna osoba. - -Standard kodowania Nette odpowiada [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/] z dwoma głównymi wyjątkami: do wcięć używa [tabulatorów zamiast spacji |#Tabulatory zamiast spacji] i używa [PascalCase dla stałych klas|https://blog.nette.org/en/for-less-screaming-in-the-code]. - -.[tip] -Wiele z tych reguł da się automatycznie sprawdzić i poprawić narzędziem [Nette Coding Standard |tools:coding-standard], więc nie musisz sprawdzać ich ręcznie. - - -Reguły ogólne -============= - -- Każdy plik PHP musi zawierać `declare(strict_types=1)` -- Do oddzielenia metod używa się dwóch pustych linii dla lepszej czytelności -- Powód użycia operatora wyciszającego (`@`) musi być udokumentowany: `@mkdir($dir); // @ - directory may exist` -- Jeśli używany jest operator porównania słabo typowany (czyli `==`, `!=`, ...), intencja musi być udokumentowana: `// == to accept null` -- Wiele klas wyjątków możesz zapisać do jednego pliku o nazwie `exceptions.php`, a wiele enumów do `enums.php` -- Widoczności metod nie podaje się dla interfejsów, bo zawsze są publiczne -- Każda właściwość, wartość zwracana i parametr muszą mieć podany typ. Odwrotnie, dla stałych finalnych typu nigdy nie podajemy, bo jest oczywisty -- Do ograniczania ciągów należy używać apostrofów, z wyjątkiem sytuacji, gdy sam literał zawiera apostrofy - - -Konwencje nazewnicze -==================== - -- Unikaj używania skrótów, chyba że pełna nazwa jest nadmierna -- Dla skrótów dwuliterowych używaj wielkich liter, a dla dłuższych PascalCase/camelCase -- Na nazwę klasy używaj rzeczownika albo frazy rzeczownikowej -- Nazwy klas muszą zawierać nie tylko konkret (`Array`), ale też ogólność (`ArrayIterator`). Wyjątkiem są atrybuty PHP -- "Stałe klas i enumy powinny używać PascalCaps":https://blog.nette.org/en/for-less-screaming-in-the-code -- "Interfejsy i klasy abstrakcyjne nie powinny zawierać przedrostków ani przyrostków":https://blog.nette.org/en/prefixes-and-suffixes-do-not-belong-in-interface-names jak `Abstract`, `Interface` czy `I` - - -Łamanie linii i nawiasy -======================= - -Standard kodowania Nette odpowiada PSR-12 (albo PER Coding Style), ale w niektórych punktach go doprecyzowuje albo modyfikuje: - -- Funkcje strzałkowe zapisuje się bez spacji przed nawiasem, czyli `fn($a) => $b` -- Między różnymi typami instrukcji importu `use` nie jest wymagana pusta linia -- Typ zwracany funkcji/metody i otwierający nawias klamrowy są zawsze w osobnych liniach: - -```php - public function find( - string $dir, - array $options, - ): array - { - // treść metody - } -``` - -Otwierający nawias klamrowy w osobnej linii jest ważny dla wizualnego oddzielenia sygnatury funkcji/metody od ciała. Jeśli sygnatura jest w jednej linii, oddzielenie jest wyraźne (obrazek po lewej). Jeśli jest w wielu liniach, w PSR sygnatura i ciało zlewają się (środek), podczas gdy w standardzie Nette pozostają oddzielone (po prawej): - -[* new-line-after.webp *] - - -Bloki dokumentacyjne (phpDoc) -============================= - -Główna reguła: **nigdy nie duplikuj** żadnej informacji z sygnatury, jak typ parametru czy typ zwracany, bez wnoszenia wartości. - -Blok dokumentacyjny definicji klasy: - -- Zaczyna się opisem klasy -- Po nim pusta linia -- Po niej adnotacje `@property` (albo `@property-read`, `@property-write`), po jednej w linii. Składnia: adnotacja, spacja, typ, spacja, `$nazwa` -- Po nich adnotacje `@method`, po jednej w linii. Składnia: adnotacja, spacja, typ zwracany, spacja, `nazwa(typ $param, ...)` -- Adnotację `@author` pomija się. Autorstwo trzymane jest w historii kodu źródłowego -- Można użyć adnotacji `@internal` albo `@deprecated` - -```php -/** - * MIME message part. - * - * @property string $encoding - * @property-read array $headers - * @method string getSomething(string $name) - * @method static bool isEnabled() - */ -``` - -Blok dokumentacyjny właściwości zawierający tylko adnotację `@var` powinien być w jednej linii: - -```php -/** @var string[] */ -private array $name; -``` - -Blok dokumentacyjny definicji metody: - -- Zaczyna się krótkim opisem metody -- Bez pustej linii -- Adnotacje `@param`, po jednej w linii -- Adnotacja `@return` -- Adnotacje `@throws`, po jednej w linii -- Można użyć adnotacji `@internal` albo `@deprecated` - -Po każdej adnotacji następuje jedna spacja, z wyjątkiem `@param`, po której następują dwie spacje dla lepszej czytelności. - -```php -/** - * Finds a file in directory. - * @param string[] $options - * @return string[] - * @throws DirectoryNotFoundException - */ -public function find(string $dir, array $options): array -``` - - -Funkcje i stałe globalne -======================== - -Funkcje i stałe globalne zapisuje się bez wiodącego odwrotnego ukośnika, czyli `count($arr)`, a nie `\count($arr)`. Dla funkcji, które PHP potrafi zoptymalizować, dodaj na początku pliku `use function`, żeby kompilator mógł przetłumaczyć je efektywniej. Należą do nich funkcje jak `count`, `strlen`, `is_array`, `is_string`, `is_scalar`, `sprintf` itd. Funkcje wypisuje się w jednej linii, żeby blok importów pozostał zwięzły: - -```php -use Nette; -use function count, is_array, is_scalar, sprintf; -``` - -Sporadycznie importujemy też stałe, których znajomość wartości może pomóc kompilatorowi: - -```php -use const PHP_OS_FAMILY; -``` - - -Tabulatory zamiast spacji -========================= - -Tabulatory mają nad spacjami kilka zalet: - -- Rozmiar wcięcia da się dostosować w edytorach i w "sieci":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size -- Nie narzucają kodowi preferencji użytkownika co do rozmiaru wcięcia, przez co kod jest bardziej przenośny -- Da się je wpisać jednym naciśnięciem klawisza (wszędzie, nie tylko w edytorach zamieniających tabulatory na spacje) -- Wcięcia są ich przeznaczeniem -- Szanują potrzeby słabowidzących i niewidomych kolegów - -Używając w naszych projektach tabulatorów, umożliwiamy dostosowanie szerokości, co większości ludzi może wydawać się zbędne, ale dla osób z wadami wzroku jest niezbędne. - -Dla niewidomych programistów używających monitorów brajlowskich każda spacja reprezentuje jedną komórkę brajlowską. Jeśli więc domyślne wcięcie to 4 spacje, wcięcie 3. poziomu marnuje 12 cennych komórek brajlowskich, zanim kod w ogóle się zacznie. Na monitorze 40-komórkowym, najczęstszym przy laptopach, to ponad jedna czwarta dostępnych komórek zmarnowana bez dostarczenia jakiejkolwiek informacji. - - -{{priority: -1}} diff --git a/contributing/pl/documentation.texy b/contributing/pl/documentation.texy deleted file mode 100644 index 4d12a28dc2..0000000000 --- a/contributing/pl/documentation.texy +++ /dev/null @@ -1,68 +0,0 @@ -Kontrybuowanie do dokumentacji -****************************** - -.[perex] -Kontrybuowanie do dokumentacji to jedna z najcenniejszych czynności, bo pomaga innym zrozumieć framework. - - -Jak pisać? ----------- - -Dokumentacja przeznaczona jest przede wszystkim dla osób, które są w danym temacie nowe. Powinna więc spełniać kilka ważnych punktów: - -- Zaczynaj od prostych i ogólnych pojęć. Do bardziej zaawansowanych tematów przechodź dopiero na końcu. -- Staraj się wyjaśnić temat jak najjaśniej. Spróbuj na przykład wyjaśnić go najpierw koledze. -- Podawaj tylko te informacje, których użytkownik faktycznie potrzebuje w danym temacie. -- Weryfikuj, czy Twoje informacje są prawdziwe. Testuj każdy kawałek kodu. -- Bądź zwięzły, skróć to, co napisałeś, o połowę. A potem śmiało zrób to jeszcze raz. -- Używaj wyróżnień oszczędnie, od pogrubionego tekstu po ramki jak `.[note]`. -- W przykładach kodu trzymaj się [standardu kodowania|coding-standard]. - -Poznaj też [składnię |syntax]. Do podglądu artykułu w trakcie pisania możesz użyć [edytora z podglądem |https://editor.nette.org/]. - - -Wersje językowe ---------------- - -Angielski jest językiem podstawowym, więc Twoje zmiany powinny być najlepiej po angielsku. Jeśli angielski nie jest Twoją mocną stroną, użyj [DeepL Translatora |https://www.deepl.com/translator], a inni Twój tekst przejrzą. - -Tłumaczenie na pozostałe języki zostanie wykonane automatycznie po zatwierdzeniu i sfinalizowaniu Twojej poprawki. - - -Drobne poprawki ---------------- - -Żeby kontrybuować do dokumentacji, musisz mieć konto na [GitHubie |https://github.com]. - -Najprostszym sposobem na wprowadzenie drobnej zmiany w dokumentacji jest użycie odnośników na końcu każdej strony: - -- *Pokaż na GitHubie* otwiera źródłową wersję strony na GitHubie. Potem wystarczy nacisnąć klawisz `E`, żeby zacząć edycję (musisz być zalogowany na GitHubie). -- *Otwórz podgląd* otwiera edytor, w którym od razu widzisz ostateczny wygląd wizualny. - -Ponieważ [edytor z podglądem |https://editor.nette.org/] nie potrafi zapisywać zmian bezpośrednio na GitHuba, po zakończeniu edycji musisz skopiować tekst źródłowy do schowka (przyciskiem *Kopiuj do schowka*), a potem wkleić go do edytora na GitHubie. Pod polem edycji jest formularz wysyłania. Tutaj nie zapomnij krótko podsumować i wyjaśnić powodu swojej poprawki. Po wysłaniu tworzony jest pull request (PR), który można dalej edytować. - - -Większe poprawki ----------------- - -Zamiast polegać wyłącznie na interfejsie GitHuba, lepiej znać podstawy pracy z systemem kontroli wersji Git. Jeśli nie znasz Gita, możesz zajrzeć do [git - the simple guide |https://rogerdudler.github.io/git-guide/] i rozważyć użycie jednego z wielu dostępnych [klientów graficznych |https://git-scm.com/downloads/guis]. - -Dokumentację edytuj tak: - -1) Na GitHubie utwórz [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] repozytorium [nette/docs |https://github.com/nette/docs]. -2) [Sklonuj |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] to repozytorium na swój komputer. -3) Następnie wprowadź zmiany w [odpowiedniej gałęzi |#Struktura dokumentacji]. -4) Sprawdź nadmiarowe spacje w tekście narzędziem [Code-Checker |tools:code-checker]. -5) Zapisz (zacommituj) zmiany. -6) Jeśli jesteś zadowolony ze zmian, wypchnij je na GitHuba do swojego forka. -7) Stamtąd zgłoś je do repozytorium `nette/docs`, tworząc [pull request|https://help.github.com/articles/creating-a-pull-request] (PR). - -Często dostaje się komentarze z sugestiami. Śledź proponowane zmiany i uwzględniaj je. Proponowane zmiany dodawaj jako nowe commity i wypychaj je znowu na GitHuba. Nigdy nie twórz nowego pull requesta tylko po to, żeby zmodyfikować istniejący. - - -Struktura dokumentacji ----------------------- - -Cała dokumentacja znajduje się na GitHubie w repozytorium [nette/docs |https://github.com/nette/docs]. Aktualna wersja jest w gałęzi `master`, a starsze wersje leżą w gałęziach takich jak `doc-3.x`, `doc-2.x`. - -Zawartość każdej gałęzi podzielona jest na główne foldery reprezentujące poszczególne obszary dokumentacji. Na przykład `application/` odpowiada `https://doc.nette.org/en/application`, `latte/` odpowiada `https://latte.nette.org` itd. Każdy z tych folderów zawiera podfoldery reprezentujące wersje językowe (`cs`, `en`, ...) i opcjonalnie podfolder `files` z obrazkami, które można wstawiać na strony dokumentacji. diff --git a/contributing/pl/syntax.texy b/contributing/pl/syntax.texy deleted file mode 100644 index a27f8af6a6..0000000000 --- a/contributing/pl/syntax.texy +++ /dev/null @@ -1,143 +0,0 @@ -Składnia dokumentacji -********************* - -Dokumentacja używa Markdowna i [składni Texy |https://texy.deegee.dev/syntax] z kilkoma rozszerzeniami. - - -Odnośniki -========= - -Do odnośników wewnętrznych używa się zapisu w nawiasach kwadratowych `[link]`. Jest to albo postać z kreską pionową `[tekst odnośnika |cel odnośnika]`, albo postać skrócona `[tekst odnośnika]`, jeśli cel jest taki sam jak tekst (po zamianie na małe litery i myślniki): - -- `[Nazwa strony]` -> `<a href="/en/page-name">Nazwa strony</a>` -- `[tekst odnośnika |Nazwa strony]` -> `<a href="/en/page-name">tekst odnośnika</a>` - -Możemy linkować do innej wersji językowej albo innej sekcji. Sekcja oznacza bibliotekę Nette (np. `forms`, `latte` itd.) albo sekcje specjalne jak `best-practices`, `quickstart` itd.: - -- `[cs:Nazwa strony]` -> `<a href="/cs/page-name">Nazwa strony</a>` (ta sama sekcja, inny język) -- `[tracy:Nazwa strony]` -> `<a href="//tracy.nette.org/en/page-name">Nazwa strony</a>` (inna sekcja, ten sam język) -- `[tracy:cs:Nazwa strony]` -> `<a href="//tracy.nette.org/cs/page-name">Nazwa strony</a>` (inna sekcja i język) - -Można też wskazać konkretny nagłówek na stronie za pomocą `#`. - -- `[#Nagłówek]` -> `<a href="#toc-heading">Nagłówek</a>` (nagłówek na bieżącej stronie) -- `[Nazwa strony#Nagłówek]` -> `<a href="/en/page-name#toc-heading">Nazwa strony</a>` - -Odnośnik do strony głównej sekcji: (`@home` to specjalne określenie strony głównej sekcji) - -- `[tekst odnośnika |@home]` -> `<a href="/en/">tekst odnośnika</a>` -- `[tekst odnośnika |tracy:]` -> `<a href="//tracy.nette.org/en/">tekst odnośnika</a>` - - -Odnośniki do dokumentacji API ------------------------------ - -Używaj zawsze poniższego zapisu: - -- `[api:Nette\SmartObject]` -> [api:Nette\SmartObject] -- `[api:Nette\Forms\Form::setTranslator()]` -> [api:Nette\Forms\Form::setTranslator()] -- `[api:Nette\Forms\Form::$onSubmit]` -> [api:Nette\Forms\Form::$onSubmit] -- `[api:Nette\Forms\Form::Required]` -> [api:Nette\Forms\Form::Required] - -W pełni kwalifikowanych nazw używaj tylko przy pierwszej wzmiance. Przy kolejnych odnośnikach używaj nazwy uproszczonej: - -- `[Form::setTranslator() |api:Nette\Forms\Form::setTranslator()]` -> [Form::setTranslator() |api:Nette\Forms\Form::setTranslator()] - - -Odnośniki do dokumentacji PHP ------------------------------ - -- `[php:substr]` -> [php:substr] - - -Kod źródłowy -============ - -Blok kodu zaczyna się od <code>```lang</code> i kończy <code>```</code>. Wspierane języki to `php`, `latte`, `neon`, `html`, `css`, `js` i `sql`. Do wcięć zawsze używaj tabulatorów. - -``` - ```php - public function renderPage($id) - { - } - ``` -``` - -Możesz też podać nazwę pliku jako <code>```php .{file: ArrayTest.php}</code>, a blok kodu wyrenderuje się tak: - -```php .{file: ArrayTest.php} -public function renderPage($id) -{ -} -``` - - -Nagłówki -======== - -Górny nagłówek (nazwę strony) podkreślaj gwiazdkami (`*`). Do oddzielania sekcji używaj znaków równości (`=`). Nagłówki podkreślaj najpierw znakami równości (`=`), a potem myślnikami (`-`): - -``` -MVC Applications & Presenters -***************************** -... - - -Link Creation -============= -... - - -Links in Templates ------------------- -... -``` - - -Ramki i style -============= - -Perex oznaczony klasą `.[perex]` .[perex] - -Notatka oznaczona klasą `.[note]` .[note] - -Wskazówka oznaczona klasą `.[tip]` .[tip] - -Uwaga oznaczona klasą `.[caution]` .[caution] - -Mocne ostrzeżenie oznaczone klasą `.[warning]` .[warning] - -Numer wersji `.{data-version:2.4.10}` .{data-version:2.4.10} - -Klasy należy zapisywać przed linią, której dotyczą: - -``` -.[perex] -To jest perex. -``` - -Zwróć uwagę, że ramki jak `.[tip]` przyciągają uwagę, dlatego powinny służyć do podkreślania ważnych informacji, a nie mniej istotnych szczegółów. Używaj ich oszczędnie. - - -Spis treści -=========== - -Spis treści (odnośniki w prawym pasku bocznym) generowany jest automatycznie dla wszystkich stron przekraczających rozmiar 4000 bajtów. To domyślne zachowanie da się zmienić [metatagiem |#Metatagi] `{{toc}}`. Tekst do spisu treści brany jest domyślnie bezpośrednio z nagłówków, ale można wyświetlić inny tekst modyfikatorem `.{toc}`, co przydaje się przy dłuższych nagłówkach. - -``` - - -Long and Intelligent Heading .{toc: A Different Text for TOC} -============================================================= -``` - - -Metatagi -======== - -- Ustawienie własnego tytułu strony (w `<title>` i okruszkach): `{{title: Inna nazwa}}` -- Przekierowanie: `{{redirect: pla:cs}}` - patrz [#Odnośniki] -- Wymuszenie `{{toc}}` albo wyłączenie `{{toc: no}}` automatycznego spisu treści (ramka z odnośnikami do nagłówków). -- Ustawienie lewego menu `{{leftbar: utils:@left-menu}}` albo jego wyłączenie `{{leftbar: no}}`. - -{{priority: -1}} diff --git a/contributing/ru/@home.texy b/contributing/ru/@home.texy deleted file mode 100644 index 256a114e1e..0000000000 --- a/contributing/ru/@home.texy +++ /dev/null @@ -1,17 +0,0 @@ -Станьте участником Nette -************************ - -.[perex] -Узнайте, как вы можете принять участие в нашем открытом проекте. Познакомьтесь с порядком внесения вклада в исходный код и документацию и станьте частью сообщества разработчиков, которые активно участвуют в улучшении Nette. - - -**Код** - -- [Вклад в код |code] -- [Стандарты кодирования |coding-standard] - -**Документация** - -- [Вклад в документацию |documentation] -- [Синтаксис документации |syntax] -- "Редактор с предпросмотром":https://editor.nette.org diff --git a/contributing/ru/@left-menu.texy b/contributing/ru/@left-menu.texy deleted file mode 100644 index 18544a5ab0..0000000000 --- a/contributing/ru/@left-menu.texy +++ /dev/null @@ -1,18 +0,0 @@ -Код -*** -- [Как внести вклад в код |code] -- [Стандарты кодирования |coding-standard] - -Документация -************ -- [Как внести вклад в документацию |documentation] -- [Синтаксис документации |syntax] -- "Редактор предпросмотра":https://editor.nette.org - - -Дополнительные материалы -************************ -- [Документация Nette |nette:] -- [Инструменты |tools:] -- [Кто создаёт Nette |https://nette.org/contributors] -- [Nette на GitHub |https://github.com/nette] diff --git a/contributing/ru/code.texy b/contributing/ru/code.texy deleted file mode 100644 index cd82375ec0..0000000000 --- a/contributing/ru/code.texy +++ /dev/null @@ -1,107 +0,0 @@ -Вклад в код -*********** - -.[perex] -Собираетесь внести вклад в Nette Framework и вам нужно познакомиться с правилами и порядком работы? Это руководство для начинающих шаг за шагом проведёт вас через то, как эффективно вносить код, работать с репозиториями и реализовывать изменения. - - -Порядок работы -============== - -Чтобы вносить код, необходимо иметь учётную запись на [GitHub|https://github.com] и знать основы работы с системой контроля версий Git. Если вы с Git не знакомы, можете заглянуть в [git - the simple guide|https://rogerdudler.github.io/git-guide/] и подумать об одном из множества [графических клиентов|https://git-scm.com/downloads/guis]. - - -Подготовка окружения и репозитория ----------------------------------- - -1) На GitHub создайте [форк|https://help.github.com/en/github/getting-started-with-github/fork-a-repo] [репозитория пакета|www:packages], который собираетесь менять -2) [Клонируйте|https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] этот репозиторий к себе на компьютер -3) Установите зависимости, включая [Nette Tester|tester:], командой `composer install` -4) Убедитесь, что тесты работают, запустив `composer tester` -5) Создайте [новую ветку |#Новая ветка] на основе последней выпущенной версии - - -Реализация собственных изменений --------------------------------- - -Теперь вы можете вносить собственные правки в код: - -1) Реализуйте нужные изменения и не забудьте о тестах -2) Убедитесь, что тесты успешно проходят, командой `composer tester` -3) Проверьте, отвечает ли код [стандартам кодирования |#Стандарты кодирования] -4) Сохраните (закоммитьте) изменения с описанием в [таком формате |#Описание коммита] - -Вы можете создать несколько коммитов, по одному на каждый логический шаг. Каждый коммит должен быть осмысленным сам по себе. - - -Отправка изменений ------------------- - -Когда изменения вас устроят, вы можете их отправить: - -1) Отправьте изменения на GitHub в свой форк -2) Оттуда отправьте их в репозиторий Nette, создав [pull request|https://help.github.com/articles/creating-a-pull-request] (PR) -3) Приведите в описании [достаточно сведений |#Описание pull request] - - -Учёт замечаний --------------- - -Ваши коммиты теперь видны другим. Обычно вы получаете замечания с предложениями: - -1) Следите за предлагаемыми изменениями -2) Вносите их новыми коммитами или [объединяйте с предыдущими|https://help.github.com/en/github/using-git/about-git-rebase] -3) Снова отправьте коммиты на GitHub, и они автоматически появятся в pull request - -Никогда не создавайте новый pull request, чтобы изменить существующий. - - -Документация ------------- - -Если вы изменили функциональность или добавили новую, не забудьте [добавить её и в документацию|documentation]. - - -Новая ветка -=========== - -По возможности вносите изменения против последней выпущенной версии, то есть последнего тега в ветке. Для тега `v3.2.1` создайте ветку такой командой: - -```shell -git checkout -b new_branch_name v3.2.1 -``` - - -Стандарты кодирования -===================== - -Ваш код должен отвечать [стандарту кодирования|coding-standard], используемому в Nette Framework. Для проверки и автоматического исправления кода воспользуйтесь инструментом [Nette Coding Standard |tools:coding-standard], там же вы найдёте указания по установке и использованию. - - -Описание коммита -================ - -В Nette у темы коммита такой формат: `Presenter: fixed AJAX detection [Closes #69]` - -- область, за ней двоеточие -- назначение коммита в прошедшем времени; по возможности начинайте словами: "added (новая возможность)", "fixed (исправление)", "refactored (изменение кода без изменения поведения)", "changed", "removed" -- если коммит ломает обратную совместимость, добавьте "BC break" -- любая ссылка на трекер задач, например `(#123)` или `[Closes #69]` -- после темы может идти пустая строка, а за ней более подробное описание, включающее, например, ссылки на форум - - -Описание pull request -===================== - -При создании pull request интерфейс GitHub позволит вам ввести заголовок и описание. Дайте краткий заголовок и приведите в описании как можно больше сведений о причинах вашего изменения. - -Укажите в шапке и то, идёт ли речь о новой возможности или об исправлении ошибки и может ли это вызвать проблемы с обратной совместимостью (BC break). Если есть связанная задача, сошлитесь на неё, чтобы она закрылась при одобрении pull request. - -``` -- bug fix / new feature? <!-- #issue numbers, if any --> -- BC break? yes/no -- doc PR: nette/docs#? <!-- highly welcome, see https://nette.org/en/writing --> -``` - - -{{priority: -1}} diff --git a/contributing/ru/coding-standard.texy b/contributing/ru/coding-standard.texy deleted file mode 100644 index 671da0c6a1..0000000000 --- a/contributing/ru/coding-standard.texy +++ /dev/null @@ -1,148 +0,0 @@ -Стандарт кодирования -******************** - -.[perex] -Этот документ описывает правила и рекомендации для разработки Nette. Внося код в Nette, вы обязаны им следовать. Проще всего добиться этого, подражая существующему коду. Цель в том, чтобы весь код выглядел так, будто его написал один человек. - -Стандарт кодирования Nette соответствует [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/] с двумя главными исключениями: для отступов он использует [табуляции вместо пробелов |#Табуляции вместо пробелов] и [PascalCase для констант классов|https://blog.nette.org/en/for-less-screaming-in-the-code]. - -.[tip] -Многие из этих правил умеет автоматически проверять и исправлять инструмент [Nette Coding Standard |tools:coding-standard], так что проверять их вручную вам не придётся. - - -Общие правила -============= - -- Каждый PHP-файл должен содержать `declare(strict_types=1)` -- Для разделения методов ради лучшей читаемости используются две пустые строки -- Причина использования оператора подавления (`@`) должна быть задокументирована: `@mkdir($dir); // @ - directory may exist` -- Если используется оператор нестрогого сравнения (то есть `==`, `!=`, ...), намерение должно быть задокументировано: `// == to accept null` -- Несколько классов исключений можно записать в один файл с именем `exceptions.php`, а несколько перечислений - в `enums.php` -- У интерфейсов видимость методов не указывается, потому что они всегда публичные -- У каждого свойства, возвращаемого значения и параметра должен быть указан тип. И наоборот, у финальных констант тип мы никогда не указываем, потому что он очевиден -- Для ограничения строк следует использовать одинарные кавычки, кроме случаев, когда сам литерал содержит апострофы - - -Соглашения об именовании -======================== - -- Избегайте сокращений, если только полное имя не окажется чрезмерным -- Для двухбуквенных сокращений используйте прописные буквы, а для более длинных - PascalCase/camelCase -- Для имени класса используйте существительное или именное словосочетание -- Имена классов должны содержать не только конкретику (`Array`), но и общность (`ArrayIterator`). Исключение - атрибуты PHP -- "Константы классов и перечисления следует писать в PascalCaps":https://blog.nette.org/en/for-less-screaming-in-the-code -- "Интерфейсы и абстрактные классы не должны содержать приставок или суффиксов":https://blog.nette.org/en/prefixes-and-suffixes-do-not-belong-in-interface-names вроде `Abstract`, `Interface` или `I` - - -Переносы и скобки -================= - -Стандарт кодирования Nette соответствует PSR-12 (или PER Coding Style), но в некоторых пунктах уточняет или меняет его: - -- Стрелочные функции пишутся без пробела перед скобкой, то есть `fn($a) => $b` -- Пустая строка между разными видами импортов `use` не требуется -- Возвращаемый тип функции или метода и открывающая фигурная скобка всегда находятся на разных строках: - -```php - public function find( - string $dir, - array $options, - ): array - { - // тело метода - } -``` - -Открывающая фигурная скобка на отдельной строке важна для визуального отделения сигнатуры функции или метода от тела. Если сигнатура на одной строке, разделение очевидно (изображение слева). Если она на нескольких строках, в PSR сигнатура и тело сливаются (посередине), а в стандарте Nette остаются разделёнными (справа): - -[* new-line-after.webp *] - - -Блоки документации (phpDoc) -=========================== - -Главное правило: **никогда не дублируйте** сведения из сигнатуры, такие как тип параметра или возвращаемый тип, если это не добавляет ценности. - -Блок документации к определению класса: - -- Начинается с описания класса -- Дальше пустая строка -- Дальше аннотации `@property` (или `@property-read`, `@property-write`), по одной на строку. Синтаксис: аннотация, пробел, тип, пробел, `$имя` -- Дальше аннотации `@method`, по одной на строку. Синтаксис: аннотация, пробел, возвращаемый тип, пробел, `имя(тип $параметр, ...)` -- Аннотация `@author` опускается. Авторство хранится в истории исходного кода -- Можно использовать аннотации `@internal` или `@deprecated` - -```php -/** - * MIME message part. - * - * @property string $encoding - * @property-read array $headers - * @method string getSomething(string $name) - * @method static bool isEnabled() - */ -``` - -Блок документации к свойству, содержащий только аннотацию `@var`, должен быть на одной строке: - -```php -/** @var string[] */ -private array $name; -``` - -Блок документации к определению метода: - -- Начинается с краткого описания метода -- Без пустой строки -- Аннотации `@param`, по одной на строку -- Аннотация `@return` -- Аннотации `@throws`, по одной на строку -- Можно использовать аннотации `@internal` или `@deprecated` - -За каждой аннотацией следует один пробел, кроме `@param`, за которой ради лучшей читаемости следуют два пробела. - -```php -/** - * Finds a file in directory. - * @param string[] $options - * @return string[] - * @throws DirectoryNotFoundException - */ -public function find(string $dir, array $options): array -``` - - -Глобальные функции и константы -============================== - -Глобальные функции и константы пишутся без ведущего обратного слеша, то есть `count($arr)`, а не `\count($arr)`. Для функций, которые PHP умеет оптимизировать, добавьте в начало файла `use function`, чтобы компилятор мог перевести их эффективнее. К ним относятся функции вроде `count`, `strlen`, `is_array`, `is_string`, `is_scalar`, `sprintf` и т. п. Функции перечисляются на одной строке, чтобы блок импортов оставался компактным: - -```php -use Nette; -use function count, is_array, is_scalar, sprintf; -``` - -Изредка мы импортируем и константы, знание значения которых может помочь компилятору: - -```php -use const PHP_OS_FAMILY; -``` - - -Табуляции вместо пробелов -========================= - -У табуляций есть несколько преимуществ перед пробелами: - -- Размер отступа настраивается в редакторах и в "вебе":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size -- Они не навязывают коду предпочтения автора по размеру отступа, благодаря чему код становится переносимее -- Их можно ввести одним нажатием клавиши (где угодно, а не только в редакторах, которые преобразуют табуляции в пробелы) -- Отступ - их прямое назначение -- Они учитывают нужды слабовидящих и слепых коллег - -Используя в наших проектах табуляции, мы даём возможность настроить ширину, что большинству людей может показаться ненужным, но для людей с нарушениями зрения это принципиально важно. - -Для слепых программистов, использующих брайлевские дисплеи, каждый пробел означает одну брайлевскую ячейку. Так что если отступ по умолчанию равен 4 пробелам, отступ 3-го уровня тратит 12 ценных брайлевских ячеек ещё до начала кода. На дисплее из 40 ячеек, самом распространённом для ноутбуков, это больше четверти доступных ячеек, потраченных впустую и не несущих никаких сведений. - - -{{priority: -1}} diff --git a/contributing/ru/documentation.texy b/contributing/ru/documentation.texy deleted file mode 100644 index 28182374e3..0000000000 --- a/contributing/ru/documentation.texy +++ /dev/null @@ -1,68 +0,0 @@ -Вклад в документацию -******************** - -.[perex] -Вклад в документацию - одно из самых ценных занятий, потому что он помогает другим понять фреймворк. - - -Как писать? ------------ - -Документация в первую очередь предназначена для людей, которые в теме новички. Поэтому она должна отвечать нескольким важным пунктам: - -- Начинайте с простых и общих понятий. К более продвинутым темам переходите только в конце. -- Старайтесь объяснить тему как можно понятнее. Попробуйте, например, сначала объяснить её коллеге. -- Приводите только те сведения, которые пользователю по данной теме действительно нужны. -- Проверяйте, что ваши сведения верны. Проверяйте каждый кусок кода. -- Будьте кратки: сократите написанное вдвое. А потом смело сделайте это ещё раз. -- Используйте выделение умеренно, от жирного текста до блоков вроде `.[note]`. -- В примерах кода придерживайтесь [стандарта кодирования|coding-standard]. - -Изучите также [синтаксис |syntax]. Чтобы просматривать статью прямо при написании, можно воспользоваться [редактором с предпросмотром |https://editor.nette.org/]. - - -Языковые версии ---------------- - -Английский - основной язык, так что ваши изменения в идеале должны быть на английском. Если английский не ваша сильная сторона, воспользуйтесь [переводчиком DeepL |https://www.deepl.com/translator], а другие ваш текст проверят. - -Перевод на другие языки будет выполнен автоматически после того, как ваша правка будет одобрена и завершена. - - -Мелкие правки -------------- - -Чтобы вносить вклад в документацию, вам нужна учётная запись на [GitHub |https://github.com]. - -Проще всего внести небольшое изменение в документацию с помощью ссылок в конце каждой страницы: - -- *Показать на GitHub* открывает исходную версию страницы на GitHub. Дальше достаточно нажать клавишу `E`, чтобы начать редактирование (нужно быть авторизованным на GitHub). -- *Открыть предпросмотр* открывает редактор, в котором вы сразу видите итоговый внешний вид. - -Поскольку [редактор с предпросмотром |https://editor.nette.org/] не может сохранять изменения прямо на GitHub, после окончания правок нужно скопировать исходный текст в буфер обмена (кнопкой *Copy to clipboard*), а затем вставить его в редактор на GitHub. Под полем редактирования находится форма отправки. Здесь не забудьте кратко изложить и объяснить причину вашей правки. После отправки создаётся pull request (PR), который можно править дальше. - - -Более крупные правки --------------------- - -Вместо того чтобы полагаться только на интерфейс GitHub, лучше познакомиться с основами работы с системой контроля версий Git. Если вы с Git не знакомы, можете обратиться к [git - the simple guide |https://rogerdudler.github.io/git-guide/] и подумать об одном из множества доступных [графических клиентов |https://git-scm.com/downloads/guis]. - -Правьте документацию так: - -1) На GitHub создайте [форк |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] репозитория [nette/docs |https://github.com/nette/docs]. -2) [Клонируйте |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] этот репозиторий к себе на компьютер. -3) Затем вносите изменения в [подходящей ветке |#Структура документации]. -4) Проверьте текст на лишние пробелы инструментом [Code-Checker |tools:code-checker]. -5) Сохраните (закоммитьте) изменения. -6) Если изменения вас устраивают, отправьте их на GitHub в свой форк. -7) Оттуда отправьте их в репозиторий `nette/docs`, создав [pull request|https://help.github.com/articles/creating-a-pull-request] (PR). - -Обычно вы получаете замечания с предложениями. Следите за предлагаемыми изменениями и вносите их. Добавляйте предложенные изменения новыми коммитами и снова отправляйте их на GitHub. Никогда не создавайте новый pull request только для того, чтобы изменить существующий. - - -Структура документации ----------------------- - -Вся документация находится на GitHub в репозитории [nette/docs |https://github.com/nette/docs]. Текущая версия - в ветке `master`, а более старые версии находятся в ветках вроде `doc-3.x`, `doc-2.x`. - -Содержимое каждой ветки разделено на основные папки, представляющие отдельные области документации. Например, `application/` соответствует `https://doc.nette.org/en/application`, `latte/` соответствует `https://latte.nette.org` и т. д. В каждой из этих папок есть подпапки, представляющие языковые версии (`cs`, `en`, ...), и, возможно, подпапка `files` с изображениями, которые можно вставлять в страницы документации. diff --git a/contributing/ru/syntax.texy b/contributing/ru/syntax.texy deleted file mode 100644 index 60cbbbc08c..0000000000 --- a/contributing/ru/syntax.texy +++ /dev/null @@ -1,143 +0,0 @@ -Синтаксис документации -********************** - -Документация использует Markdown и [синтаксис Texy |https://texy.deegee.dev/syntax] с несколькими дополнениями. - - -Ссылки -====== - -Для внутренних ссылок используется запись в квадратных скобках `[link]`. Это либо вид с вертикальной чертой `[текст ссылки |цель ссылки]`, либо сокращённый вид `[текст ссылки]`, если цель совпадает с текстом (после преобразования в строчные буквы и дефисы): - -- `[Page name]` -> `<a href="/en/page-name">Page name</a>` -- `[link text |Page name]` -> `<a href="/en/page-name">link text</a>` - -Мы можем сослаться на другую языковую версию или другой раздел. Раздел означает библиотеку Nette (например, `forms`, `latte` и т. д.) или особые разделы вроде `best-practices`, `quickstart` и т. п.: - -- `[cs:Page name]` -> `<a href="/cs/page-name">Page name</a>` (тот же раздел, другой язык) -- `[tracy:Page name]` -> `<a href="//tracy.nette.org/en/page-name">Page name</a>` (другой раздел, тот же язык) -- `[tracy:cs:Page name]` -> `<a href="//tracy.nette.org/cs/page-name">Page name</a>` (другой раздел и язык) - -С помощью `#` можно нацелиться и на конкретный заголовок на странице. - -- `[#Heading]` -> `<a href="#toc-heading">Heading</a>` (заголовок на текущей странице) -- `[Page name#Heading]` -> `<a href="/en/page-name#toc-heading">Page name</a>` - -Ссылка на главную страницу раздела: (`@home` - особое обозначение главной страницы раздела) - -- `[link text |@home]` -> `<a href="/en/">link text</a>` -- `[link text |tracy:]` -> `<a href="//tracy.nette.org/en/">link text</a>` - - -Ссылки на документацию API --------------------------- - -Всегда используйте такую запись: - -- `[api:Nette\SmartObject]` -> [api:Nette\SmartObject] -- `[api:Nette\Forms\Form::setTranslator()]` -> [api:Nette\Forms\Form::setTranslator()] -- `[api:Nette\Forms\Form::$onSubmit]` -> [api:Nette\Forms\Form::$onSubmit] -- `[api:Nette\Forms\Form::Required]` -> [api:Nette\Forms\Form::Required] - -Полные имена используйте только при первом упоминании. Для дальнейших ссылок используйте упрощённое имя: - -- `[Form::setTranslator() |api:Nette\Forms\Form::setTranslator()]` -> [Form::setTranslator() |api:Nette\Forms\Form::setTranslator()] - - -Ссылки на документацию PHP --------------------------- - -- `[php:substr]` -> [php:substr] - - -Исходный код -============ - -Блок кода начинается с <code>```lang</code> и заканчивается <code>```</code>. Поддерживаемые языки: `php`, `latte`, `neon`, `html`, `css`, `js` и `sql`. Для отступов всегда используйте табуляции. - -``` - ```php - public function renderPage($id) - { - } - ``` -``` - -Можно указать и имя файла как <code>```php .{file: ArrayTest.php}</code>, и блок кода отрисуется так: - -```php .{file: ArrayTest.php} -public function renderPage($id) -{ -} -``` - - -Заголовки -========= - -Самый верхний заголовок (имя страницы) подчёркивайте звёздочками (`*`). Для разделения секций используйте знаки равенства (`=`). Заголовки подчёркивайте сначала знаками равенства (`=`), а затем дефисами (`-`): - -``` -MVC Applications & Presenters -***************************** -... - - -Link Creation -============= -... - - -Links in Templates ------------------- -... -``` - - -Блоки и стили -============= - -Перекс, помеченный классом `.[perex]` .[perex] - -Замечание, помеченное классом `.[note]` .[note] - -Совет, помеченный классом `.[tip]` .[tip] - -Предостережение, помеченное классом `.[caution]` .[caution] - -Строгое предупреждение, помеченное классом `.[warning]` .[warning] - -Номер версии `.{data-version:2.4.10}` .{data-version:2.4.10} - -Классы следует писать перед строкой, к которой они относятся: - -``` -.[perex] -Это перекс. -``` - -Учтите, что блоки вроде `.[tip]` привлекают внимание и поэтому должны использоваться для выделения важных сведений, а не менее значимых подробностей. Используйте их умеренно. - - -Содержание -========== - -Содержание (ссылки в правой колонке) порождается автоматически для всех страниц размером более 4000 байт. Это поведение по умолчанию можно изменить [метатегом |#Метатеги] `{{toc}}`. Текст для содержания по умолчанию берётся прямо из заголовков, но можно вывести другой текст модификатором `.{toc}`, что полезно для более длинных заголовков. - -``` - - -Long and Intelligent Heading .{toc: A Different Text for TOC} -============================================================= -``` - - -Метатеги -======== - -- Задать собственный заголовок страницы (в `<title>` и хлебных крошках): `{{title: Another name}}` -- Перенаправление: `{{redirect: pla:cs}}` - см. [#Ссылки] -- Принудительно включить `{{toc}}` или отключить `{{toc: no}}` автоматическое содержание (блок со ссылками на заголовки). -- Задать левое меню `{{leftbar: utils:@left-menu}}` или отключить его `{{leftbar: no}}`. - -{{priority: -1}} diff --git a/contributing/tr/@home.texy b/contributing/tr/@home.texy deleted file mode 100644 index be73a4a3d4..0000000000 --- a/contributing/tr/@home.texy +++ /dev/null @@ -1,17 +0,0 @@ -Nette'e Katkıda Bulunun -*********************** - -.[perex] -Açık kaynak projemize nasıl katılabileceğinizi öğrenin. Kaynak koda ve belgelere katkıda bulunmanın yollarını öğrenin ve Nette'i iyileştirmeye etkin biçimde katılan geliştiriciler topluluğunun parçası olun. - - -**Kod** - -- [Koda katkıda bulunma |code] -- [Kodlama standartları |coding-standard] - -**Belgeler** - -- [Belgelere katkıda bulunma |documentation] -- [Belge sözdizimi |syntax] -- "Önizleme düzenleyicisi":https://editor.nette.org diff --git a/contributing/tr/@left-menu.texy b/contributing/tr/@left-menu.texy deleted file mode 100644 index 0a20b30b47..0000000000 --- a/contributing/tr/@left-menu.texy +++ /dev/null @@ -1,18 +0,0 @@ -Kod -*** -- [Koda katkıda bulunma |code] -- [Kodlama standartları |coding-standard] - -Belgeler -******** -- [Belgelere katkıda bulunma |documentation] -- [Belge sözdizimi |syntax] -- "Önizleme düzenleyicisi":https://editor.nette.org - - -Devamını okuyun -*************** -- [Nette dokümantasyonu |nette:] -- [Araçlar |tools:] -- [Nette'i kimler geliştiriyor |https://nette.org/contributors] -- [GitHub'da Nette |https://github.com/nette] diff --git a/contributing/tr/code.texy b/contributing/tr/code.texy deleted file mode 100644 index b0bb188db3..0000000000 --- a/contributing/tr/code.texy +++ /dev/null @@ -1,107 +0,0 @@ -Koda Katkıda Bulunma -******************** - -.[perex] -Nette Framework'e katkıda bulunmayı planlıyor ve kurallarla yöntemleri öğrenmeniz mi gerekiyor? Bu başlangıç kılavuzu sizi, koda etkili biçimde katkıda bulunma, depolarla çalışma ve değişiklikleri gerçekleştirme adımlarında yönlendirecek. - - -Yöntem -====== - -Koda katkıda bulunmak için [GitHub|https://github.com] üzerinde bir hesabınızın olması ve Git sürüm denetim sistemiyle çalışmanın temellerini bilmeniz gerekir. Git'e aşina değilseniz [git - the simple guide|https://rogerdudler.github.io/git-guide/] kaynağına göz atabilir ve pek çok [grafik istemciden|https://git-scm.com/downloads/guis] birini kullanmayı düşünebilirsiniz. - - -Ortamı ve Depoyu Hazırlama --------------------------- - -1) GitHub'da bir [fork|https://help.github.com/en/github/getting-started-with-github/fork-a-repo] oluşturun; yani değiştirmeyi düşündüğünüz [paket deposunun|www:packages] bir kopyasını kendi hesabınıza alın -2) Bu depoyu bilgisayarınıza [klonlayın|https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] -3) [Nette Tester|tester:] dahil bağımlılıkları `composer install` komutuyla kurun -4) `composer tester` çalıştırarak testlerin çalıştığını doğrulayın -5) En son yayımlanan sürümü temel alan bir [#Yeni Dal] oluşturun - - -Kendi Değişikliklerinizi Gerçekleştirme ---------------------------------------- - -Artık kendi kod düzenlemelerinizi yapabilirsiniz: - -1) İstediğiniz değişiklikleri gerçekleştirin ve testleri unutmayın -2) `composer tester` ile testlerin başarıyla çalıştığından emin olun -3) Kodun [#Kodlama Standartları] kurallarını karşılayıp karşılamadığını denetleyin -4) Değişiklikleri [şu biçimde |#Commit Açıklaması] bir açıklamayla kaydedin (commit) - -Her mantıksal adım için bir tane olmak üzere birden çok commit oluşturabilirsiniz. Her commit kendi başına anlamlı olmalıdır. - - -Değişiklikleri Sunma --------------------- - -Değişikliklerden memnun olduğunuzda onları sunabilirsiniz: - -1) Değişiklikleri GitHub'da kendi fork'unuza gönderin (push) -2) Oradan bir [pull request|https://help.github.com/articles/creating-a-pull-request] (PR) oluşturarak Nette deposuna sunun -3) Açıklamada [yeterli bilgi |#Pull Request Açıklaması] verin - - -Geri Bildirimi Uygulama ------------------------ - -Commit'leriniz artık başkalarınca görülebilir. Öneriler içeren yorumlar almak olağandır: - -1) Önerilen değişiklikleri izleyin -2) Onları yeni commit'ler olarak uygulayın ya da [öncekilerle birleştirin|https://help.github.com/en/github/using-git/about-git-rebase] -3) Commit'leri yeniden GitHub'a gönderin; onlar pull request'te otomatik görünecek - -Var olan bir pull request'i değiştirmek için asla yeni bir pull request oluşturmayın. - - -Belgeler --------- - -Bir işlevselliği değiştirdiyseniz ya da yenisini eklediyseniz, onu [belgelere de eklemeyi|documentation] unutmayın. - - -Yeni Dal -======== - -Mümkünse değişiklikleri en son yayımlanan sürüme, yani daldaki son etikete karşı yapın. `v3.2.1` etiketi için şu komutla bir dal oluşturun: - -```shell -git checkout -b new_branch_name v3.2.1 -``` - - -Kodlama Standartları -==================== - -Kodunuz, Nette Framework'te kullanılan [kodlama standardını|coding-standard] karşılamalıdır. Kodunuzu denetlemek ve otomatik düzeltmek için, kurulum ve kullanım yönergelerini de bulacağınız [Nette Coding Standard |tools:coding-standard] aracını kullanın. - - -Commit Açıklaması -================= - -Nette'de commit konuları şu biçimdedir: `Presenter: fixed AJAX detection [Closes #69]` - -- Alan, ardından iki nokta üst üste -- Commit'in amacı geçmiş zamanda; mümkünse şu sözcüklerle başlayın: "added (yeni özellik)", "fixed (düzeltme)", "refactored (davranış değişmeden kod değişikliği)", "changed", "removed" -- Commit geriye dönük uyumluluğu bozuyorsa "BC break" ekleyin -- Varsa issue tracker'a bağlantı, örneğin `(#123)` ya da `[Closes #69]` -- Konudan sonra bir boş satır ve ardından, örneğin foruma bağlantılar da içeren daha ayrıntılı bir açıklama olabilir - - -Pull Request Açıklaması -======================= - -Bir pull request oluştururken GitHub arayüzü size bir başlık ve açıklama girme olanağı verir. Özlü bir başlık verin ve açıklamada değişikliğinizin nedenleri hakkında olabildiğince çok bilgi ekleyin. - -Ayrıca başlıkta bunun yeni bir özellik mi yoksa bir hata düzeltmesi mi olduğunu ve geriye dönük uyumluluk sorunlarına (BC break) yol açıp açmayacağını belirtin. İlgili bir issue varsa, pull request onaylandığında kapanması için ona bağlantı verin. - -``` -- bug fix / new feature? <!-- #issue numbers, if any --> -- BC break? yes/no -- doc PR: nette/docs#? <!-- highly welcome, see https://nette.org/en/writing --> -``` - - -{{priority: -1}} diff --git a/contributing/tr/coding-standard.texy b/contributing/tr/coding-standard.texy deleted file mode 100644 index f527f40ee4..0000000000 --- a/contributing/tr/coding-standard.texy +++ /dev/null @@ -1,148 +0,0 @@ -Kodlama Standardı -***************** - -.[perex] -Bu belge, Nette'in geliştirilmesine ilişkin kuralları ve önerileri anlatır. Nette'e kod katkısı yaparken onlara uymalısınız. Bunu yapmanın en kolay yolu var olan kodu taklit etmektir. Amaç, tüm kodun tek bir kişi tarafından yazılmış gibi görünmesidir. - -Nette Kodlama Standardı, iki ana ayrım dışında [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/] standardına karşılık gelir: girinti için [boşluk yerine sekme |#Boşluk Yerine Sekme] kullanır ve [sınıf sabitleri için PascalCase|https://blog.nette.org/tr/for-less-screaming-in-the-code] kullanır. - -.[tip] -Bu kuralların çoğu [Nette Coding Standard |tools:coding-standard] aracıyla otomatik denetlenip düzeltilebilir, dolayısıyla onları elle denetlemeniz gerekmez. - - -Genel Kurallar -============== - -- Her PHP dosyası `declare(strict_types=1)` içermelidir -- Daha iyi okunabilirlik için metotları ayırmak üzere iki boş satır kullanılır -- Sustur operatörünün (`@`) kullanım nedeni belgelenmelidir: `@mkdir($dir); // @ - directory may exist` -- Zayıf tipli bir karşılaştırma operatörü kullanılıyorsa (yani `==`, `!=`, ...), niyet belgelenmelidir: `// == to accept null` -- Birden çok istisna sınıfını `exceptions.php` adlı tek bir dosyaya, birden çok enum'ı ise `enums.php` dosyasına yazabilirsiniz -- Arayüzlerde metotların görünürlüğü belirtilmez, çünkü onlar her zaman geneldir -- Her özelliğin, dönüş değerinin ve parametrenin tipi belirtilmelidir. Tersine, final sabitler için tipi asla belirtmeyiz, çünkü apaçıktır -- Dizeleri sınırlamak için tek tırnak kullanılmalıdır; ancak dizenin kendisi kesme işareti içeriyorsa değil - - -Adlandırma Kuralları -==================== - -- Tam ad aşırı uzun değilse kısaltma kullanmaktan kaçının -- İki harfli kısaltmalar için büyük harf, daha uzun kısaltmalar için PascalCase/camelCase kullanın -- Sınıf adı için bir ad ya da ad öbeği kullanın -- Sınıf adları yalnızca özgüllüğü (`Array`) değil, genelliği de (`ArrayIterator`) içermelidir. PHP nitelikleri bunun dışındadır -- "Sınıf sabitleri ve enum'lar PascalCaps kullanmalıdır":https://blog.nette.org/tr/for-less-screaming-in-the-code -- "Arayüzler ve soyut sınıflar önek ya da sonek içermemelidir":https://blog.nette.org/tr/prefixes-and-suffixes-do-not-belong-in-interface-names, örneğin `Abstract`, `Interface` ya da `I` - - -Satır Sarma ve Parantezler -========================== - -Nette Kodlama Standardı PSR-12 (ya da PER Coding Style) standardına karşılık gelir, ama onu bazı noktalarda özelleştirir ya da değiştirir: - -- Ok fonksiyonları parantezden önce boşluk olmadan yazılır, yani `fn($a) => $b` -- Farklı `use` import deyimi tipleri arasında boş satır gerekmez -- Bir fonksiyonun/metodun dönüş tipi ile açılan süslü parantez her zaman ayrı satırlardadır: - -```php - public function find( - string $dir, - array $options, - ): array - { - // method body - } -``` - -Açılan süslü parantezin ayrı satırda olması, fonksiyon/metot imzasını gövdeden görsel olarak ayırmak açısından önemlidir. İmza tek satırdaysa ayrım açıktır (soldaki görsel). Birden çok satırdaysa, PSR'de imza ile gövde birbirine karışır (ortada), Nette standardında ise ayrı kalırlar (sağda): - -[* new-line-after.webp *] - - -Belge Blokları (phpDoc) -======================= - -Ana kural: Parametre tipi ya da dönüş tipi gibi imza bilgilerini, değer katmadan **asla yinelemeyin**. - -Bir sınıf tanımı için belge bloğu: - -- Sınıfın açıklamasıyla başlar -- Ardından boş bir satır gelir -- Ardından, her satıra bir tane olmak üzere `@property` (ya da `@property-read`, `@property-write`) açıklamaları gelir. Sözdizimi: açıklama, boşluk, tip, boşluk, `$name` -- Ardından, her satıra bir tane olmak üzere `@method` açıklamaları gelir. Sözdizimi: açıklama, boşluk, dönüş tipi, boşluk, `name(type $param, ...)` -- `@author` açıklaması atlanır. Yazarlık, kaynak kodun geçmişinde tutulur -- `@internal` ya da `@deprecated` açıklamaları kullanılabilir - -```php -/** - * MIME message part. - * - * @property string $encoding - * @property-read array $headers - * @method string getSomething(string $name) - * @method static bool isEnabled() - */ -``` - -Yalnızca `@var` açıklamasını içeren, bir özelliğe ait belge bloğu tek satırda olmalıdır: - -```php -/** @var string[] */ -private array $name; -``` - -Bir metot tanımı için belge bloğu: - -- Metodun kısa bir açıklamasıyla başlar -- Boş satır yok -- Her satıra bir tane olmak üzere `@param` açıklamaları -- `@return` açıklaması -- Her satıra bir tane olmak üzere `@throws` açıklamaları -- `@internal` ya da `@deprecated` açıklamaları kullanılabilir - -Her açıklamadan sonra bir boşluk gelir; daha iyi okunabilirlik için iki boşluk gelen `@param` bunun dışındadır. - -```php -/** - * Finds a file in directory. - * @param string[] $options - * @return string[] - * @throws DirectoryNotFoundException - */ -public function find(string $dir, array $options): array -``` - - -Küresel Fonksiyonlar ve Sabitler -================================ - -Küresel fonksiyonlar ve sabitler baştaki ters eğik çizgi olmadan yazılır, yani `\count($arr)` değil `count($arr)`. PHP'nin iyileştirebildiği fonksiyonlar için, derleyicinin onları daha verimli çevirebilmesi amacıyla dosyanın başına `use function` ekleyin. Bunlar arasında `count`, `strlen`, `is_array`, `is_string`, `is_scalar`, `sprintf` gibi fonksiyonlar vardır. Import bloğunu derli toplu tutmak için fonksiyonlar tek satırda listelenir: - -```php -use Nette; -use function count, is_array, is_scalar, sprintf; -``` - -Ara sıra, değerinin bilinmesi derleyiciye yardım edebilecek sabitleri de import ederiz: - -```php -use const PHP_OS_FAMILY; -``` - - -Boşluk Yerine Sekme -=================== - -Sekmelerin boşluklara göre birkaç avantajı vardır: - -- Girintinin boyutu düzenleyicilerde ve "web'de":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size özelleştirilebilir -- Kullanıcının girinti boyutu tercihini koda dayatmazlar, bu da kodu daha taşınabilir kılar -- Tek tuş vuruşuyla yazılabilirler (yalnızca sekmeleri boşluğa çeviren düzenleyicilerde değil, her yerde) -- Girinti onların varlık nedenidir -- Görme engelli ve kör meslektaşların gereksinimlerine saygı gösterirler - -Projelerimizde sekme kullanarak genişlik özelleştirmesine olanak tanırız; bu çoğu insana gereksiz görünebilir, ama görme engelli insanlar için temeldir. - -Braille ekran kullanan kör programcılar için her boşluk bir braille hücresini temsil eder. Yani varsayılan girinti 4 boşluksa, 3. düzey bir girinti, kod daha başlamadan 12 değerli braille hücresini harcar. Dizüstü bilgisayarlarda en yaygın olan 40 hücrelik bir ekranda bu, kullanılabilir hücrelerin dörtte birinden fazlasının hiçbir bilgi vermeden harcanması demektir. - - -{{priority: -1}} diff --git a/contributing/tr/documentation.texy b/contributing/tr/documentation.texy deleted file mode 100644 index fb728d7442..0000000000 --- a/contributing/tr/documentation.texy +++ /dev/null @@ -1,68 +0,0 @@ -Belgelere Katkıda Bulunma -************************* - -.[perex] -Belgelere katkıda bulunmak, başkalarının framework'ü anlamasına yardım ettiği için en değerli etkinliklerden biridir. - - -Nasıl Yazmalı? --------------- - -Belgeler öncelikle konuya yeni olan kişiler içindir. Bu yüzden birkaç önemli noktayı karşılamalıdır: - -- Basit ve genel kavramlarla başlayın. Daha ileri konulara ancak sonda geçin. -- Konuyu olabildiğince anlaşılır anlatmaya çalışın. Örneğin önce bir meslektaşınıza anlatmayı deneyin. -- Yalnızca kullanıcının o konu için gerçekten gereksinim duyduğu bilgiyi verin. -- Bilginizin doğru olduğunu doğrulayın. Her kod parçasını sınayın. -- Özlü olun; yazdığınızı yarıya indirin. Sonra çekinmeden bunu bir kez daha yapın. -- Vurgulamayı, kalın metinden `.[note]` gibi kutulara dek, ölçülü kullanın. -- Kod örneklerinde [Kodlama standardını|coding-standard] izleyin. - -Ayrıca [sözdizimini |syntax] öğrenin. Yazarken makaleyi önizlemek için [önizleme düzenleyicisini |https://editor.nette.org/] kullanabilirsiniz. - - -Dil Sürümleri -------------- - -İngilizce birincil dildir, dolayısıyla değişiklikleriniz ideal olarak İngilizce olmalıdır. İngilizce güçlü yanınız değilse [DeepL Translator |https://www.deepl.com/translator] kullanın; başkaları metninizi gözden geçirecek. - -Diğer dillere çeviri, düzenlemeniz onaylanıp sonlandırıldıktan sonra otomatik yapılacak. - - -Önemsiz Düzenlemeler --------------------- - -Belgelere katkıda bulunmak için [GitHub |https://github.com] üzerinde bir hesabınızın olması gerekir. - -Belgelerde küçük bir değişiklik yapmanın en kolay yolu, her sayfanın sonundaki bağlantıları kullanmaktır: - -- *Show on GitHub* sayfanın kaynak sürümünü GitHub'da açar. Sonra düzenlemeye başlamak için `E` tuşuna basmanız yeter (GitHub'da giriş yapmış olmalısınız). -- *Open preview* nihai görsel görünümü hemen görebileceğiniz bir düzenleyici açar. - -[Önizleme düzenleyicisi |https://editor.nette.org/] değişiklikleri doğrudan GitHub'a kaydedemediğinden, düzenlemelerinizi bitirdikten sonra kaynak metni panoya kopyalamanız (*Copy to clipboard* düğmesiyle) ve sonra GitHub'daki düzenleyiciye yapıştırmanız gerekir. Düzenleme alanının altında bir gönderme formu vardır. Burada düzenlemenizin nedenini kısaca özetlemeyi ve açıklamayı unutmayın. Gönderdikten sonra, daha sonra da düzenlenebilen bir pull request (PR) oluşturulur. - - -Daha Büyük Düzenlemeler ------------------------ - -Yalnızca GitHub arayüzüne dayanmak yerine, Git sürüm denetim sistemiyle çalışmanın temellerini bilmek daha iyidir. Git'e aşina değilseniz [git - the simple guide |https://rogerdudler.github.io/git-guide/] kaynağına başvurabilir ve mevcut pek çok [grafik istemciden |https://git-scm.com/downloads/guis] birini kullanmayı düşünebilirsiniz. - -Belgeleri şöyle düzenleyin: - -1) GitHub'da bir [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] oluşturun; yani [nette/docs |https://github.com/nette/docs] deposunun bir kopyasını kendi hesabınıza alın. -2) Bu depoyu bilgisayarınıza [klonlayın |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository]. -3) Sonra [uygun dalda |#Belgelerin Yapısı] değişiklikleri yapın. -4) Metindeki fazladan boşlukları [Code-Checker |tools:code-checker] aracıyla denetleyin. -5) Değişiklikleri kaydedin (commit). -6) Değişikliklerden memnunsanız, onları GitHub'da kendi fork'unuza gönderin (push). -7) Oradan bir [pull request|https://help.github.com/articles/creating-a-pull-request] (PR) oluşturarak `nette/docs` deposuna sunun. - -Öneriler içeren yorumlar almak olağandır. Önerilen değişiklikleri izleyin ve onları uygulayın. Önerilen değişiklikleri yeni commit'ler olarak ekleyin ve yeniden GitHub'a gönderin. Var olan bir pull request'i değiştirmek için asla yeni bir pull request oluşturmayın. - - -Belgelerin Yapısı ------------------ - -Belgelerin tamamı GitHub'da [nette/docs |https://github.com/nette/docs] deposunda bulunur. Güncel sürüm `master` dalında, daha eski sürümler ise `doc-3.x`, `doc-2.x` gibi dallardadır. - -Her dalın içeriği, tek tek belge alanlarını temsil eden ana klasörlere bölünmüştür. Örneğin `application/` klasörü `https://doc.nette.org/en/application` adresine, `latte/` klasörü `https://latte.nette.org` adresine karşılık gelir vb. Bu klasörlerin her biri, dil sürümlerini temsil eden alt klasörler (`cs`, `en`, ...) ve isteğe bağlı olarak, belge sayfalarına eklenebilen görselleri içeren bir `files` alt klasörü barındırır. diff --git a/contributing/tr/syntax.texy b/contributing/tr/syntax.texy deleted file mode 100644 index 58be02bc73..0000000000 --- a/contributing/tr/syntax.texy +++ /dev/null @@ -1,143 +0,0 @@ -Belge Sözdizimi -*************** - -Belgeler, birkaç geliştirmeyle birlikte Markdown ve [Texy sözdizimini |https://texy.deegee.dev/syntax] kullanır. - - -Bağlantılar -=========== - -İç bağlantılar için köşeli parantez yazımı `[link]` kullanılır. Bu ya dikey çizgili biçimdedir `[bağlantı metni |bağlantı hedefi]` ya da hedef metinle aynıysa (küçük harfe ve tirelere dönüştürüldükten sonra) kısaltılmış biçimdedir `[bağlantı metni]`: - -- `[Sayfa adı]` -> `<a href="/tr/sayfa-adi">Sayfa adı</a>` -- `[bağlantı metni |Sayfa adı]` -> `<a href="/tr/sayfa-adi">bağlantı metni</a>` - -Başka bir dil sürümüne ya da başka bir bölüme bağlantı verebiliriz. Bölüm, bir Nette kütüphanesini (örneğin `forms`, `latte` vb.) ya da `best-practices`, `quickstart` gibi özel bölümleri anlatır: - -- `[cs:Sayfa adı]` -> `<a href="/cs/sayfa-adi">Sayfa adı</a>` (aynı bölüm, farklı dil) -- `[tracy:Sayfa adı]` -> `<a href="//tracy.nette.org/tr/sayfa-adi">Sayfa adı</a>` (farklı bölüm, aynı dil) -- `[tracy:cs:Sayfa adı]` -> `<a href="//tracy.nette.org/cs/sayfa-adi">Sayfa adı</a>` (farklı bölüm ve dil) - -`#` kullanarak sayfadaki belirli bir başlığı hedeflemek de mümkündür. - -- `[#Başlık]` -> `<a href="#toc-baslik">Başlık</a>` (geçerli sayfadaki başlık) -- `[Sayfa adı#Başlık]` -> `<a href="/tr/sayfa-adi#toc-baslik">Sayfa adı</a>` - -Bölümün ana sayfasına bağlantı: (`@home`, bölümün ana sayfası için özel bir terimdir) - -- `[bağlantı metni |@home]` -> `<a href="/tr/">bağlantı metni</a>` -- `[bağlantı metni |tracy:]` -> `<a href="//tracy.nette.org/tr/">bağlantı metni</a>` - - -API Belgelerine Bağlantılar ---------------------------- - -Her zaman şu yazımı kullanın: - -- `[api:Nette\SmartObject]` -> [api:Nette\SmartObject] -- `[api:Nette\Forms\Form::setTranslator()]` -> [api:Nette\Forms\Form::setTranslator()] -- `[api:Nette\Forms\Form::$onSubmit]` -> [api:Nette\Forms\Form::$onSubmit] -- `[api:Nette\Forms\Form::Required]` -> [api:Nette\Forms\Form::Required] - -Tam nitelikli adları yalnızca ilk anıldığında kullanın. Sonraki bağlantılarda basitleştirilmiş bir ad kullanın: - -- `[Form::setTranslator() |api:Nette\Forms\Form::setTranslator()]` -> [Form::setTranslator() |api:Nette\Forms\Form::setTranslator()] - - -PHP Belgelerine Bağlantılar ---------------------------- - -- `[php:substr]` -> [php:substr] - - -Kaynak Kodu -=========== - -Bir kod bloğu <code>```lang</code> ile başlar ve <code>```</code> ile biter. Desteklenen diller `php`, `latte`, `neon`, `html`, `css`, `js` ve `sql` dilleridir. Girinti için her zaman sekme kullanın. - -``` - ```php - public function renderPage($id) - { - } - ``` -``` - -Dosya adını <code>```php .{file: ArrayTest.php}</code> biçiminde de belirtebilirsiniz; kod bloğu o zaman şöyle render edilir: - -```php .{file: ArrayTest.php} -public function renderPage($id) -{ -} -``` - - -Başlıklar -========= - -En üstteki başlığın (sayfa adının) altını yıldızlarla (`*`) çizin. Bölümleri ayırmak için eşittir işaretlerini (`=`) kullanın. Başlıkların altını önce eşittir işaretleriyle (`=`), sonra tirelerle (`-`) çizin: - -``` -MVC Applications & Presenters -***************************** -... - - -Link Creation -============= -... - - -Links in Templates ------------------- -... -``` - - -Kutular ve Stiller -================== - -`.[perex]` sınıfıyla işaretlenmiş perex .[perex] - -`.[note]` sınıfıyla işaretlenmiş not .[note] - -`.[tip]` sınıfıyla işaretlenmiş ipucu .[tip] - -`.[caution]` sınıfıyla işaretlenmiş dikkat .[caution] - -`.[warning]` sınıfıyla işaretlenmiş güçlü uyarı .[warning] - -Sürüm numarası `.{data-version:2.4.10}` .{data-version:2.4.10} - -Sınıflar, uygulandıkları satırın önüne yazılmalıdır: - -``` -.[perex] -Bu perex'tir. -``` - -`.[tip]` gibi kutuların dikkat çektiğini ve bu yüzden daha önemsiz ayrıntılar için değil, önemli bilgileri vurgulamak için kullanılması gerektiğini unutmayın. Onları ölçülü kullanın. - - -İçindekiler -=========== - -Boyutu 4.000 baytı aşan tüm sayfalar için otomatik olarak bir içindekiler tablosu (sağ kenar çubuğundaki bağlantılar) üretilir. Bu varsayılan davranış `{{toc}}` [#Meta Etiketleri] ile değiştirilebilir. TOC metni varsayılan olarak doğrudan başlıklardan alınır, ama uzun başlıklarda yararlı olan `.{toc}` değiştiricisiyle farklı bir metin göstermek mümkündür. - -``` - - -Uzun ve Akıllı Başlık .{toc: TOC için farklı bir metin} -======================================================= -``` - - -Meta Etiketleri -=============== - -- Özel bir sayfa başlığı ayarlayın (`<title>` içinde ve breadcrumb'larda): `{{title: Another name}}` -- Yönlendirme: `{{redirect: pla:cs}}` - bkz. [#Bağlantılar] -- Otomatik içindekiler tablosunu (başlıklara bağlantı içeren kutu) zorlayın `{{toc}}` ya da kapatın `{{toc: no}}`. -- Sol menüyü ayarlayın `{{leftbar: utils:@left-menu}}` ya da kapatın `{{leftbar: no}}`. - -{{priority: -1}} diff --git a/database/cs/@home.texy b/database/cs/@home.texy deleted file mode 100644 index 1ca7af906a..0000000000 --- a/database/cs/@home.texy +++ /dev/null @@ -1,16 +0,0 @@ -Podporované databáze -==================== - -Nette podporuje následující databáze: - -|* Databázový server |* DSN jméno |* Podpora v Core |* Podpora v Explorer -| MySQL (>= 5.1) | mysql | ANO | ANO -| PostgreSQL (>= 9.0) | pgsql | ANO | ANO -| Sqlite 3 (>= 3.8) | sqlite | ANO | ANO -| Oracle | oci | ANO | - -| MS SQL (PDO_SQLSRV) | sqlsrv | ANO | ANO -| MS SQL (PDO_DBLIB) | mssql | ANO | - -| ODBC | odbc | ANO | - - -{{maintitle: Nette Database: jednoduchá a chytrá databázová vrstva pro PHP}} -{{description: Nette Database zásadním způsobem zjednodušuje získávání dat bez nutnosti psát SQL dotazy. Pokládá efektivní dotazy a nepřenáší zbytečná data.}} diff --git a/database/cs/@left-menu.texy b/database/cs/@left-menu.texy deleted file mode 100644 index f96b60ebe1..0000000000 --- a/database/cs/@left-menu.texy +++ /dev/null @@ -1,21 +0,0 @@ -Nette Database -************** -- [Úvod |guide] -- [SQL přístup |sql way] -- [Explorer] -- [Transakce |transactions] -- [Výjimky |exceptions] -- [Reflexe |reflection] -- [Konverze typů |type-conversion] -- [Konfigurace |configuration] -- [Bezpečnostní rizika |security] -- [Upgrade |upgrading] - - -Další četba -*********** -- [Dokumentace Nette |nette:] -- [Aplikace v Nette |application:how-it-works] -- [Utilities |utils:] -- [Návody a postupy |best-practices:] -- [Řešení problémů |nette:troubleshooting] diff --git a/database/cs/@meta.texy b/database/cs/@meta.texy deleted file mode 100644 index 462d9add80..0000000000 --- a/database/cs/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokumentace}} diff --git a/database/cs/exceptions.texy b/database/cs/exceptions.texy deleted file mode 100644 index 6f47eb1026..0000000000 --- a/database/cs/exceptions.texy +++ /dev/null @@ -1,40 +0,0 @@ -Výjimky -******* - -Nette Database používá hierarchii výjimek. Základní třídou je `Nette\Database\DriverException`, která dědí z `PDOException` a poskytuje rozšířené možnosti pro práci s chybami databáze: - -- Metoda `getDriverCode()` vrací kód chyby od databázového driveru -- Metoda `getSqlState()` vrací SQLSTATE kód -- Metody `getQueryString()` a `getParameters()` umožňují získat původní dotaz a jeho parametry - -Z `DriverException` dědí následující specializované výjimky: - -- `ConnectionException` - signalizuje selhání připojení k databázovému serveru - - `ConnectionLostException` .{data-version:3.2.9} - spojení bylo ztraceno během operace (restart serveru, výpadek sítě, idle timeout); před dalším použitím je potřeba se znovu připojit -- `ConstraintViolationException` - základní třída pro porušení databázových omezení, ze které dědí: - - `ForeignKeyConstraintViolationException` - porušení cizího klíče - - `NotNullConstraintViolationException` - porušení NOT NULL omezení - - `UniqueConstraintViolationException` - porušení unikátnosti hodnoty - - `CheckConstraintViolationException` .{data-version:3.2.9} - porušení CHECK omezení -- `DeadlockException` .{data-version:3.2.9} - deadlock nebo serializační konflikt zjištěný serverem; transakce byla zrušena a operaci lze zopakovat -- `LockTimeoutException` .{data-version:3.2.9} - vypršel časový limit při čekání na zámek; příkaz byl přerušen, okolní transakce obvykle zůstává otevřená - -Příklad zachytávání výjimky `UniqueConstraintViolationException`, která nastane, když se snažíme vložit uživatele s emailem, který už v databázi existuje (za předpokladu, že sloupec email má unikátní index). - -```php -try { - $database->query('INSERT INTO users', [ - 'email' => 'john@example.com', - 'name' => 'John Doe', - 'password' => $hashedPassword, - ]); -} catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'Uživatel s tímto emailem již existuje.'; - -} catch (Nette\Database\DriverException $e) { - echo 'Došlo k chybě při registraci: ' . $e->getMessage(); -} -``` - -{{maintitle: Výjimky v Nette Database: hierarchie a zpracování chyb}} -{{description: Hierarchie výjimek v Nette Database: jak rozpoznat selhání připojení, porušení omezení nebo deadlock a zjistit kód chyby ovladače, SQLSTATE a původní dotaz.}} diff --git a/database/cs/guide.texy b/database/cs/guide.texy deleted file mode 100644 index 94ffc8bd57..0000000000 --- a/database/cs/guide.texy +++ /dev/null @@ -1,221 +0,0 @@ -Nette Database -************** - -.[perex] -Nette Database je výkonná a elegantní databázová vrstva pro PHP s důrazem na jednoduchost a chytré funkce. Nabízí dva způsoby práce s databází - [Explorer] pro rychlý vývoj aplikací, nebo [SQL přístup |SQL way] pro přímou práci s dotazy. - -<div class="grid gap-3"> -<div> - - -[SQL přístup |SQL way] -====================== -- Bezpečné parametrizované dotazy -- Přesná kontrola nad podobou SQL dotazů -- Když píšete komplexní dotazy s pokročilými funkcemi -- Optimalizujete výkon pomocí specifických SQL funkcí - -</div> - -<div> - - -[Explorer] -========== -- Vyvíjíte rychle bez psaní SQL -- Intuitivní práce s relacemi mezi tabulkami -- Oceníte automatickou optimalizaci dotazů -- Vhodné pro rychlou a pohodlnou práci s databází - -</div> - -</div> - - -Instalace -========= - -Knihovnu stáhnete a nainstalujete pomocí nástroje [Composer|best-practices:composer]: - -```shell -composer require nette/database -``` - - -Podporované databáze -==================== - -Nette Database podporuje následující databáze: - -|* Databázový server |* DSN jméno |* Podpora v Explorer -|---------------------|-------------|----------------------- -| MySQL (>= 5.1) | mysql | ANO -| PostgreSQL (>= 9.0) | pgsql | ANO -| Sqlite 3 (>= 3.8) | sqlite | ANO -| Oracle | oci | - -| MS SQL (PDO_SQLSRV) | sqlsrv | ANO -| MS SQL (PDO_DBLIB) | mssql | - -| ODBC | odbc | - - - -Dva přístupy k databázi -======================= - -Nette Database vám dává na výběr: můžete buď psát SQL dotazy přímo (SQL přístup), nebo je nechat generovat automaticky (Explorer). Podívejme se, jak oba přístupy řeší stejné úkoly: - -[SQL přístup|sql way] - SQL dotazy - -```php -// vložení záznamu -$database->query('INSERT INTO books', [ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// získání záznamů: autoři knih -$result = $database->query(' - SELECT authors.*, COUNT(books.id) AS books_count - FROM authors - LEFT JOIN books ON authors.id = books.author_id - WHERE authors.active = 1 - GROUP BY authors.id -'); - -// výpis (není optimální, generuje N dalších dotazů) -foreach ($result as $author) { - $books = $database->query(' - SELECT * FROM books - WHERE author_id = ? - ORDER BY published_at DESC - ', $author->id); - - echo "Autor $author->name napsal $author->books_count knih:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -[Explorer přístup|explorer] - automatické generování SQL - -```php -// vložení záznamu -$database->table('books')->insert([ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// získání záznamů: autoři knih -$authors = $database->table('authors') - ->where('active', 1); - -// výpis (automaticky generuje jen 2 optimalizované dotazy) -foreach ($authors as $author) { - $books = $author->related('books') - ->order('published_at DESC'); - - echo "Autor $author->name napsal {$books->count()} knih:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -Explorer přístup generuje a optimalizuje SQL dotazy automaticky. V uvedeném příkladu SQL přístup vygeneruje N+1 dotazů (jeden pro autory a pak jeden pro knihy každého autora), zatímco Explorer automaticky optimalizuje dotazy a provede pouze dva - jeden pro autory a jeden pro všechny jejich knihy. - -Oba přístupy lze v aplikaci libovolně kombinovat podle potřeby. - - -Připojení a konfigurace -======================= - -Pro připojení k databázi stačí vytvořit instanci třídy [api:Nette\Database\Connection]: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password); -``` - -Parametr `$dsn` (data source name) je stejný, [jaký používá PDO |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], např. `host=127.0.0.1;dbname=test`. V případě selhání vyhodí výjimku `Nette\Database\ConnectionException`. - -Nicméně šikovnější způsob nabízí [aplikační konfigurace |configuration], kam stačí přidat sekci `database` a vytvoří se potřebné objekty a také databázový panel v [Tracy |tracy:] baru. - -```neon -database: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password -``` - -Poté objekt spojení [získáme jako službu z DI kontejneru |dependency-injection:passing-dependencies], např.: - -```php -class Model -{ - public function __construct( - // nebo Nette\Database\Explorer - private Nette\Database\Connection $database, - ) { - } -} -``` - -Více informací o [konfiguraci databáze|configuration]. - - -Ruční vytvoření Explorer ------------------------- - -Pokud nepoužíváte Nette DI kontejner, můžete instanci `Nette\Database\Explorer` vytvořit ručně: - -```php -// připojení k databázi -$connection = new Nette\Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); -// úložiště pro cache, implementuje Nette\Caching\Storage, např.: -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); -// stará se o reflexi databázové struktury -$structure = new Nette\Database\Structure($connection, $storage); -// definuje pravidla pro mapování názvů tabulek, sloupců a cizích klíčů -$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); -$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage); -``` - - -Správa připojení -================ - -Při vytvoření objektu `Connection` dojde automaticky k připojení. Pokud chcete připojení odložit, použijte lazy režim - ten zapnete v [konfiguraci|configuration] nastavením `lazy`, nebo takto: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => true]); -``` - -Pro správu připojení využijte metody `connect()`, `disconnect()` a `reconnect()`. -- `connect()` vytvoří připojení, pokud ještě neexistuje, přičemž může vyvolat výjimku `Nette\Database\ConnectionException`. -- `disconnect()` odpojí aktuální připojení k databázi. -- `reconnect()` provede odpojení a následné opětovné připojení k databázi. Tato metoda může rovněž vyvolat výjimku `Nette\Database\ConnectionException`. - -Kromě toho můžete sledovat události spojené s připojením pomocí události `onConnect`, což je pole callbacků, které se zavolají po navázání spojení s databází. - -```php -// proběhne po připojení k databázi -$database->onConnect[] = function($database) { - echo "Připojeno k databázi"; -}; -``` - -Událost `onQuery` funguje podobně - je to pole callbacků, které se zavolají po každém provedeném dotazu (i při jeho selhání), což se hodí pro logování nebo profilování. - - -Tracy Debug Bar -=============== - -Pokud používáte [Tracy |tracy:], aktivuje se automaticky panel Database v Debug baru, který zobrazuje všechny provedené dotazy, jejich parametry, dobu vykonání a místo v kódu, kde byly zavolány. - -[* db-panel.webp *] - -{{maintitle: Začínáme s Nette Database: Explorer, nebo SQL}} -{{description: Nainstalujte Nette Database, připojte se k MySQL, PostgreSQL, SQLite nebo MS SQL a vyberte si přístup: Explorer pro rychlý vývoj, nebo SQL pro plnou kontrolu.}} diff --git a/database/cs/reflection.texy b/database/cs/reflection.texy deleted file mode 100644 index 963224446e..0000000000 --- a/database/cs/reflection.texy +++ /dev/null @@ -1,130 +0,0 @@ -Reflexe struktury -***************** - -.{data-version:3.2.1} -Nette Database poskytuje nástroje pro introspekci databázové struktury pomocí třídy [api:Nette\Database\Reflection]. Ta umožňuje získávat informace o tabulkách, sloupcích, indexech a cizích klíčích. Reflexi můžete využít ke generování schémat, vytváření flexibilních aplikací pracujících s databází nebo obecných databázových nástrojů. - -Objekt reflexe získáme z instance připojení k databázi: - -```php -$reflection = $database->getReflection(); -``` - - -Získání tabulek ---------------- - -Readonly vlastnost `$reflection->tables` obsahuje asociativní pole všech tabulek v databázi: - -```php -// Výpis názvů všech tabulek -foreach ($reflection->tables as $name => $table) { - echo $name . "\n"; -} -``` - -K dispozici jsou ještě dvě metody: - -```php -// Ověření existence tabulky -if ($reflection->hasTable('users')) { - echo "Tabulka users existuje"; -} - -// Vrátí objekt tabulky; pokud neexistuje, vyhodí výjimku -$table = $reflection->getTable('users'); -``` - - -Informace o tabulce -------------------- - -Tabulka je reprezentována objektem [Table|api:Nette\Database\Reflection\Table], který poskytuje následující readonly vlastnosti: - -- `$name: string` - název tabulky -- `$view: bool` - zda se jedná o pohled -- `$fullName: ?string` - plný název tabulky včetně schématu (pokud existuje) -- `$columns: array<string, Column>` - asociativní pole sloupců tabulky -- `$indexes: Index[]` - pole indexů tabulky -- `$primaryKey: ?Index` - primární klíč tabulky nebo null -- `$foreignKeys: ForeignKey[]` - pole cizích klíčů tabulky -- `$comment: ?string` - komentář tabulky - - -Sloupce -------- - -Vlastnost `columns` tabulky poskytuje asociativní pole sloupců, kde klíčem je název sloupce a hodnotou instance [Column|api:Nette\Database\Reflection\Column] s těmito vlastnostmi: - -- `$name: string` - název sloupce -- `$table: ?Table` - reference na tabulku sloupce -- `$nativeType: string` - nativní databázový typ -- `$size: ?int` - velikost/délka typu -- `$nullable: bool` - zda může sloupec obsahovat NULL -- `$default: mixed` - výchozí hodnota sloupce -- `$autoIncrement: bool` - zda je sloupec auto-increment -- `$primary: bool` - zda je součástí primárního klíče -- `$vendor: array` - dodatečná metadata specifická pro daný databázový systém -- `$comment: ?string` - komentář sloupce - -```php -foreach ($table->columns as $name => $column) { - echo "Sloupec: $name\n"; - echo "Typ: {$column->nativeType}\n"; - echo "Nullable: " . ($column->nullable ? 'Ano' : 'Ne') . "\n"; -} -``` - - -Indexy ------- - -Vlastnost `indexes` tabulky poskytuje pole indexů, kde každý index je instance [Index|api:Nette\Database\Reflection\Index] s těmito vlastnostmi: - -- `$columns: Column[]` - pole sloupců tvořících index -- `$unique: bool` - zda je index unikátní -- `$primary: bool` - zda jde o primární klíč -- `$name: ?string` - název indexu - -Primární klíč tabulky lze získat pomocí vlastnosti `primaryKey`, která vrací buď objekt `Index`, nebo `null` v případě, že tabulka nemá primární klíč. - -```php -// Výpis indexů -foreach ($table->indexes as $index) { - $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); - echo "Index" . ($index->name ? " {$index->name}" : '') . ":\n"; - echo " Sloupce: $columns\n"; - echo " Unique: " . ($index->unique ? 'Ano' : 'Ne') . "\n"; -} - -// Výpis primárního klíče -if ($primaryKey = $table->primaryKey) { - $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); - echo "Primární klíč: $columns\n"; -} -``` - - -Cizí klíče ----------- - -Vlastnost `foreignKeys` tabulky poskytuje pole cizích klíčů, kde každý cizí klíč je instance [ForeignKey|api:Nette\Database\Reflection\ForeignKey] s těmito vlastnostmi: - -- `$foreignTable: Table` - odkazovaná tabulka -- `$localColumns: Column[]` - pole lokálních sloupců -- `$foreignColumns: Column[]` - pole odkazovaných sloupců -- `$name: string` - název cizího klíče - -```php -// Výpis cizích klíčů -foreach ($table->foreignKeys as $fk) { - $localCols = implode(', ', array_map(fn($col) => $col->name, $fk->localColumns)); - $foreignCols = implode(', ', array_map(fn($col) => $col->name, $fk->foreignColumns)); - - echo "FK" . ($fk->name ? " {$fk->name}" : '') . ":\n"; - echo " $localCols -> {$fk->foreignTable->name}($foreignCols)\n"; -} -``` - -{{maintitle: Reflexe databáze v Nette: tabulky, sloupce a indexy}} -{{description: Zjistěte strukturu databáze přímo z PHP: seznam tabulek a informace o sloupcích, indexech a cizích klíčích pomocí reflexe v Nette Database.}} diff --git a/database/cs/transactions.texy b/database/cs/transactions.texy deleted file mode 100644 index ddd95386d5..0000000000 --- a/database/cs/transactions.texy +++ /dev/null @@ -1,48 +0,0 @@ -Transakce -********* - -.[perex] -Transakce zaručují, že se buď provedou všechny operace v rámci transakce, nebo se neprovede žádná. Jsou užitečné pro zajištění konzistence dat při složitějších operacích. - -Nejjednodušší způsob použití transakcí vypadá takto: - -```php -$database->beginTransaction(); -try { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); - $database->commit(); -} catch (\Exception $e) { - $database->rollBack(); - throw $e; -} -``` - -Mnohem elegantněji můžete to samé zapsat pomocí metody `transaction()`. Jako parametr přijímá callback, který vykoná v transakci. Pokud callback proběhne bez výjimky, transakce se automaticky potvrdí. Pokud dojde k výjimce, transakce se zruší (rollback) a výjimka se šíří dál. - -```php -$database->transaction(function ($database) use ($id) { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); -}); -``` - -Volání `transaction()` lze vnořovat, což usnadňuje skládání metod, z nichž každá spravuje vlastní transakci. Na databázi se jako `BEGIN`/`COMMIT` skutečně pošle jen nejvyšší (vnější) transakce; vnitřní volání pouze počítají hloubku zanoření. Volání `beginTransaction()`, `commit()` nebo `rollBack()` ručně uvnitř callbacku `transaction()` vyhodí `LogicException`. - -Metoda `transaction()` může také vracet hodnoty: - -```php -$count = $database->transaction(function ($database) { - $result = $database->query('UPDATE users SET active = ?', true); - return $result->getRowCount(); // vrátí počet aktualizovaných řádků -}); -``` - -{{maintitle: Databázové transakce v PHP a Nette Database}} -{{description: Více operací jako jeden celek: provedou se všechny, nebo žádná. Stačí beginTransaction(), commit() a rollBack(), nebo jednodušší transaction() s callbackem.}} diff --git a/database/cs/type-conversion.texy b/database/cs/type-conversion.texy deleted file mode 100644 index c1e9b1ee9b..0000000000 --- a/database/cs/type-conversion.texy +++ /dev/null @@ -1,58 +0,0 @@ -Konverze typů -************* - -.[perex] -Nette Database automaticky konvertuje hodnoty vrácené z databáze na odpovídající PHP typy. - - -Datum a čas ------------ - -Časové údaje jsou převáděny na objekty `Nette\Utils\DateTime`. Pokud chcete, aby byly časové údaje převáděny na immutable objekty `Nette\Database\DateTime`, nastavte v [konfiguraci|configuration] volbu `newDateTime` na true. - -```php -$row = $database->fetch('SELECT created_at FROM articles'); -echo $row->created_at instanceof DateTime; // true -echo $row->created_at->format('j. n. Y'); -``` - -V případě MySQL se datový typ `TIME` převádí na objekty `DateInterval`. - - -Booleovské hodnoty ------------------- - -Booleovské hodnoty jsou automaticky převedeny na `true` nebo `false`. U MySQL se `TINYINT(1)` převádí, pokud v [konfiguraci|configuration] nastavíme `convertBoolean`. - -```php -$row = $database->fetch('SELECT is_published FROM articles'); -echo gettype($row->is_published); // 'boolean' -``` - - -Číselné hodnoty ---------------- - -Číselné hodnoty jsou převedeny na `int` nebo `float` podle typu sloupce v databázi: - -```php -$row = $database->fetch('SELECT id, price FROM products'); -echo gettype($row->id); // integer -echo gettype($row->price); // float -``` - - -Vlastní normalizace -------------------- - -Pomocí metody `setRowNormalizer(?callable $normalizer)` můžete nastavit vlastní funkci pro transformaci řádků z databáze. To se hodí například pro automatický převod datových typů. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // tady proběhne konverze typů - return $row; -}); -``` - -{{maintitle: Konverze typů v Nette Database: z databáze do PHP}} -{{description: Nette Database automaticky převádí hodnoty z databáze na typy PHP: celá a desetinná čísla, boolean a objekty DateTime a DateInterval.}} diff --git a/database/cs/upgrading.texy b/database/cs/upgrading.texy deleted file mode 100644 index b38003085d..0000000000 --- a/database/cs/upgrading.texy +++ /dev/null @@ -1,39 +0,0 @@ -Upgrade -******* - - -Upgrade na verzi 3.2 -==================== - -Minimální požadovaná verze PHP je 8.1. - -Kód byl pečlivě vyladěn pro PHP 8.1. Byly doplněny všechny nové typehinty u metod a properties. Změny jsou jen drobné: - -- MySQL: nulové datum `0000-00-00` vrací jako `null` -- MySQL: decimal bez desetinných míst vrací jako int místo float -- typ `time` vrací jako objekt `DateTime` s datem nastaveným na `0001-01-01` místo aktuálního data - - -Upgrade na verzi 3.1 -==================== - -- třída `Nette\Database\Context` byla přejmenována na `Nette\Database\Explorer` kvůli konzistenci s pojmenováním [Database Explorer|explorer] -- rozhraní `Nette\Database\IRow` a `Nette\Database\IRowContainer` jsou označena jako deprecated, protože jsou nepotřebná -- driver `MySqlDriver` používá subqueries -- translator SQL příkazů lépe kontroluje, kde je možné předávat pole - - -Upgrade na verzi 3.0 -==================== - -Některé metody, jako `fetch()` nebo `fetchField()`, nyní v případě, že není další záznam, vracejí `null` místo `false`. - - -Upgrade na verzi 2.3 -==================== - -- `MySqlDriver` používá ve výchozím nastavení pro MySQL >= 5.5.3 kódování `utf8mb4` místo `utf8` -- `IReflection` bylo rozděleno na dvojici rozhraní `IStructure` a `IConventions` - -{{maintitle: Upgrade Nette Database: změny v jednotlivých verzích}} -{{description: Co se změnilo v Nette Database 3.2, 3.1, 3.0 a 2.3 a jak upravit kód při přechodu na novější verzi.}} diff --git a/database/de/@home.texy b/database/de/@home.texy deleted file mode 100644 index 464cd5e695..0000000000 --- a/database/de/@home.texy +++ /dev/null @@ -1,18 +0,0 @@ -Unterstützte Datenbanken -======================== - -Diese Datenbankserver werden unterstützt: - -|* Datenbankserver |* DSN-Name |* Core-Unterstützung |* Explorer-Unterstützung -| MySQL (>= 5.1) | mysql | JA | JA -| PostgreSQL (>= 9.0) | pgsql | JA | JA -| Sqlite 3 (>= 3.8) | sqlite | JA | JA -| Oracle | oci | JA | - -| MS SQL (PDO_SQLSRV) | sqlsrv | JA | JA -| MS SQL (PDO_DBLIB) | mssql | JA | - -| ODBC | odbc | JA | - - - - -{{maintitle: Nette Database - großartige Datenbankschicht für PHP}} -{{description: Nette Database vereinfacht das Laden von Daten aus der Datenbank erheblich, ohne dass Sie SQL-Queries schreiben müssen. Es führt effiziente Queries aus und überträgt keine unnötigen Daten.}} diff --git a/database/de/@left-menu.texy b/database/de/@left-menu.texy deleted file mode 100644 index e9dc1a243f..0000000000 --- a/database/de/@left-menu.texy +++ /dev/null @@ -1,21 +0,0 @@ -Nette Database -************** -- [Erste Schritte |guide] -- [SQL-Weg|sql-way] -- [Explorer|explorer] -- [Transaktionen|transactions] -- [Exceptions|exceptions] -- [Reflection|reflection] -- [Typkonvertierung |type-conversion] -- [Konfiguration|configuration] -- [Sicherheitsrisiken |security] -- [Upgrade|upgrading] - - -Weiterführende Lektüre -********************** -- [Nette Dokumentation |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Best Practices |best-practices:] -- [Fehlerbehebung |nette:troubleshooting] diff --git a/database/de/@meta.texy b/database/de/@meta.texy deleted file mode 100644 index b3b806b2ca..0000000000 --- a/database/de/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokumentation}} diff --git a/database/de/configuration.texy b/database/de/configuration.texy deleted file mode 100644 index 10e35eec1c..0000000000 --- a/database/de/configuration.texy +++ /dev/null @@ -1,110 +0,0 @@ -Datenbankkonfiguration -********************** - -.[perex] -Übersicht der Konfigurationsoptionen für Nette Database. - -Wenn Sie nicht das gesamte Framework, sondern nur diese Bibliothek verwenden, lesen Sie, [wie man die Konfiguration lädt |bootstrap:]. - - -Einzelne Verbindung -------------------- - -Konfiguration einer einzelnen Datenbankverbindung: - -```neon -database: - # DSN, der einzige Pflichtschlüssel - dsn: "sqlite:%appDir%/Model/demo.db" - user: ... - password: ... -``` - -Dadurch entstehen die Services `Nette\Database\Connection` und `Nette\Database\Explorer`, die üblicherweise per [Autowiring |dependency-injection:autowiring] oder über einen Verweis auf [ihren Namen |#DI-Services] übergeben werden. - -Weitere Einstellungen: - -```neon -database: - # das Datenbank-Panel in der Tracy Bar anzeigen? - debugger: ... # (bool) standardmäßig an, wenn Tracy aktiv ist - - # EXPLAIN der Queries in der Tracy Bar anzeigen? - explain: ... # (bool) Standardwert ist true - - # Autowiring für diese Verbindung aktivieren? - autowired: ... # (bool) Standardwert ist true für die erste Verbindung - - # Tabellenkonventionen: discovered, static oder ein Klassenname - conventions: discovered # (string) Standardwert ist 'discovered' - - options: - # erst dann mit der Datenbank verbinden, wenn es nötig ist? - lazy: ... # (bool) Standardwert ist false - - # PHP-Klasse des Datenbanktreibers - driverClass: # (string) - - # nur MySQL: setzt sql_mode - sqlmode: # (string) - - # nur MySQL: setzt SET NAMES - charset: # (string) Standardwert ist 'utf8mb4' - - # nur MySQL: wandelt TINYINT(1) in bool um - convertBoolean: # (bool) Standardwert ist false - - # gibt Datumsspalten als unveränderliche Objekte zurück (seit Version 3.2.1) - newDateTime: # (bool) Standardwert ist false - - # nur Oracle und SQLite: Format zum Speichern des Datums - formatDateTime: # (string) Standardwert ist 'U' -``` - -Der Schlüssel `options` kann weitere Optionen enthalten, die Sie in der [Dokumentation der PDO-Treiber |https://www.php.net/manual/en/pdo.drivers.php] finden, zum Beispiel: - -```neon -database: - options: - PDO::MYSQL_ATTR_COMPRESS: true -``` - - -Mehrere Verbindungen --------------------- - -In der Konfiguration können wir mehrere Datenbankverbindungen definieren, indem wir sie in benannte Abschnitte aufteilen: - -```neon -database: - main: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password - - another: - dsn: 'sqlite::memory:' -``` - -Das Autowiring ist nur für die Services aus dem ersten Abschnitt aktiviert. Das lässt sich mit `autowired: false` oder `autowired: true` ändern. - - -DI-Services ------------ - -Diese Services werden dem DI-Container hinzugefügt, wobei `###` für den Namen der Verbindung steht: - -| Name | Typ | Beschreibung -|---------------------------|---------------------------------|--------------------------- -| `database.###.connection` | [api:Nette\Database\Connection] | Datenbankverbindung -| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |explorer] - - -Wenn wir nur eine Verbindung definieren, lauten die Namen der Services `database.default.connection` und `database.default.explorer`. Definieren wir wie im obigen Beispiel mehrere Verbindungen, entsprechen die Namen den Abschnitten, also `database.main.connection`, `database.main.explorer` sowie `database.another.connection` und `database.another.explorer`. - -Nicht autowirete Services übergeben wir explizit über einen Verweis auf ihren Namen: - -```neon -services: - - UserFacade(@database.another.connection) -``` diff --git a/database/de/exceptions.texy b/database/de/exceptions.texy deleted file mode 100644 index 7e7a726168..0000000000 --- a/database/de/exceptions.texy +++ /dev/null @@ -1,37 +0,0 @@ -Exceptions -********** - -Nette Database verwendet eine Hierarchie von Exceptions. Die Basisklasse ist `Nette\Database\DriverException`, die `PDOException` erweitert und erweiterte Möglichkeiten für die Arbeit mit Datenbankfehlern bietet: - -- Die Methode `getDriverCode()` gibt den Fehlercode des Datenbanktreibers zurück. -- Die Methode `getSqlState()` gibt den SQLSTATE-Code zurück. -- Die Methoden `getQueryString()` und `getParameters()` erlauben es, die ursprüngliche Query und ihre Parameter abzurufen. - -Die Klasse `DriverException` wird von den folgenden spezialisierten Exceptions erweitert: - -- `ConnectionException` - zeigt an, dass die Verbindung zum Datenbankserver fehlgeschlagen ist. - - `ConnectionLostException` .{data-version:3.2.9} - die Verbindung wurde während einer Operation abgebrochen (Serverneustart, Netzwerkausfall, Idle Timeout); vor der weiteren Verwendung ist ein Reconnect nötig. -- `ConstraintViolationException` - die Basisklasse für Verletzungen von Datenbank-Constraints, von der die folgenden Exceptions erben: - - `ForeignKeyConstraintViolationException` - Verletzung eines Fremdschlüssel-Constraints. - - `NotNullConstraintViolationException` - Verletzung eines NOT-NULL-Constraints. - - `UniqueConstraintViolationException` - Verletzung eines Eindeutigkeits-Constraints. - - `CheckConstraintViolationException` .{data-version:3.2.9} - Verletzung eines CHECK-Constraints. -- `DeadlockException` .{data-version:3.2.9} - ein vom Server erkannter Deadlock oder Serialisierungskonflikt; die Transaktion wurde zurückgerollt und kann wiederholt werden. -- `LockTimeoutException` .{data-version:3.2.9} - eine Zeitüberschreitung beim Warten auf eine Sperre; die Anweisung wurde abgebrochen, die umgebende Transaktion bleibt aber in der Regel offen. - -Das folgende Beispiel zeigt, wie man eine `UniqueConstraintViolationException` abfängt, die auftritt, wenn man einen Benutzer mit einer E-Mail-Adresse einfügen will, die bereits in der Datenbank existiert (vorausgesetzt, die Spalte `email` hat einen Unique-Index): - -```php -try { - $database->query('INSERT INTO users', [ - 'email' => 'john@example.com', - 'name' => 'John Doe', - 'password' => $hashedPassword, - ]); -} catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'Ein Benutzer mit dieser E-Mail-Adresse existiert bereits.'; - -} catch (Nette\Database\DriverException $e) { - echo 'Bei der Registrierung ist ein Fehler aufgetreten: ' . $e->getMessage(); -} -``` diff --git a/database/de/explorer.texy b/database/de/explorer.texy deleted file mode 100644 index 1b9ec271c2..0000000000 --- a/database/de/explorer.texy +++ /dev/null @@ -1,912 +0,0 @@ -Database Explorer -***************** - -<div class=perex> - -Der Explorer bietet eine intuitive und effiziente Art, mit der Datenbank zu arbeiten. Er kümmert sich automatisch um die Beziehungen zwischen Tabellen und um die Optimierung der Queries, sodass Sie sich auf die Logik Ihrer Anwendung konzentrieren können. Er funktioniert sofort ohne Konfiguration. Wenn Sie volle Kontrolle über die SQL-Queries brauchen, können Sie den [SQL-Weg |SQL way] verwenden. - -- Die Arbeit mit Daten ist natürlich und leicht verständlich -- Erzeugt optimierte SQL-Queries, die nur die benötigten Daten laden -- Ermöglicht einfachen Zugriff auf verwandte Daten, ohne JOIN-Queries schreiben zu müssen -- Funktioniert sofort ohne jede Konfiguration oder Generierung von Entities - -</div> - - -Mit dem Explorer beginnen Sie, indem Sie die Methode `table()` des Objekts [api:Nette\Database\Explorer] aufrufen (Details zum Einrichten der Datenbankverbindung finden Sie unter [Verbindung und Konfiguration |guide#Verbindung und Konfiguration]): - -```php -$books = $explorer->table('book'); // 'book' ist der Tabellenname -``` - -Die Methode gibt ein Objekt [Selection |api:Nette\Database\Table\Selection] zurück, das eine SQL-Query repräsentiert. An dieses Objekt lassen sich weitere Methoden zum Filtern und Sortieren der Ergebnisse anhängen. Die Query wird erst dann zusammengesetzt und ausgeführt, wenn die Daten angefordert werden, zum Beispiel beim Durchlaufen mit `foreach`. Jede Zeile wird durch ein Objekt [ActiveRow |api:Nette\Database\Table\ActiveRow] repräsentiert: - -```php -foreach ($books as $book) { - echo $book->title; // Ausgabe der Spalte 'title' - echo $book->author_id; // Ausgabe der Spalte 'author_id' -} -``` - -Der Explorer erleichtert die Arbeit mit [Beziehungen zwischen Tabellen |#Beziehungen zwischen Tabellen] ganz erheblich. Das folgende Beispiel zeigt, wie einfach wir Daten aus verknüpften Tabellen ausgeben können (Bücher und ihre Autoren). Beachten Sie, dass wir keine JOIN-Queries schreiben müssen, Nette erzeugt sie für uns: - -```php -$books = $explorer->table('book'); - -foreach ($books as $book) { - echo 'Buch: ' . $book->title; - echo 'Autor: ' . $book->author->name; // erzeugt einen JOIN auf die Tabelle 'author' -} -``` - -Nette Database Explorer optimiert die Queries, damit sie möglichst effizient sind. Das obige Beispiel führt nur zwei SELECT-Queries aus, unabhängig davon, ob wir 10 oder 10 000 Bücher verarbeiten. - -Darüber hinaus verfolgt der Explorer, welche Spalten im Code verwendet werden, und lädt nur diese aus der Datenbank, was weitere Leistung spart. Dieses Verhalten ist vollständig automatisch und anpassungsfähig. Wenn Sie den Code später ändern und weitere Spalten verwenden, passt der Explorer die Queries automatisch an. Sie müssen nichts konfigurieren und auch nicht darüber nachdenken, welche Spalten Sie brauchen werden - überlassen Sie das Nette. - - -Filterung und Sortierung -======================== - -Die Klasse `Selection` stellt Methoden zum Filtern und Sortieren der Datenauswahl bereit. - -.[language-php] -| `where($condition, ...$params)` | Fügt eine WHERE-Bedingung hinzu. Mehrere Bedingungen werden mit AND verknüpft | -| `whereOr(array $conditions)` | Fügt eine Gruppe von WHERE-Bedingungen hinzu, die mit OR verknüpft werden | -| `wherePrimary($value)` | Fügt eine WHERE-Bedingung anhand des Primärschlüssels hinzu | -| `order($columns, ...$params)` | Legt die Sortierung mit ORDER BY fest | -| `select($columns, ...$params)` | Gibt an, welche Spalten geladen werden sollen | -| `limit($limit, $offset = null)` | Begrenzt die Anzahl der Zeilen (LIMIT) und setzt optional OFFSET | -| `page($page, $itemsPerPage, &$numOfPages = null)` | Richtet die Paginierung ein | -| `group($columns, ...$params)` | Gruppiert die Zeilen (GROUP BY) | -| `having($condition, ...$params)`| Fügt eine HAVING-Bedingung zum Filtern gruppierter Zeilen hinzu | - -Die Methoden lassen sich verketten (das sogenannte [Fluent Interface |nette:introduction-to-object-oriented-programming#Fluent Interfaces]): `$table->where(...)->order(...)->limit(...)`. - -In diesen Methoden können Sie außerdem spezielle Notationen für den Zugriff auf [Daten aus verwandten Tabellen |#Abfragen über verwandte Tabellen] verwenden. - - -Escaping und Bezeichner ------------------------ - -Die Methoden escapen Parameter automatisch und setzen Bezeichner (Tabellen- und Spaltennamen) in Anführungszeichen, wodurch SQL-Injection verhindert wird. Damit das richtig funktioniert, müssen einige Regeln eingehalten werden: - -- Schreiben Sie Schlüsselwörter, Funktions- und Prozedurnamen usw. in **Großbuchstaben**. -- Schreiben Sie Spalten- und Tabellennamen in **Kleinbuchstaben**. -- Setzen Sie Strings immer über **Parameter** ein. - -```php -where('name = ' . $name); // KRITISCHE SICHERHEITSLÜCKE: SQL-Injection -where('name LIKE "%search%"'); // FALSCH: erschwert das automatische Quoting -where('name LIKE ?', '%search%'); // RICHTIG: Wert über einen Parameter eingesetzt - -where('name like ?', $name); // FALSCH: erzeugt: `name` `like` ? -where('name LIKE ?', $name); // RICHTIG: erzeugt: `name` LIKE ? -where('LOWER(name) = ?', $value);// RICHTIG: LOWER(`name`) = ? -``` - - -where(string|array $condition, ...$parameters): static .[method] ----------------------------------------------------------------- - -Filtert die Ergebnisse mit WHERE-Bedingungen. Ihre Stärke liegt im intelligenten Umgang mit verschiedenen Wertetypen und in der automatischen Wahl der passenden SQL-Operatoren. - -Grundlegende Verwendung: - -```php -$table->where('id', $value); // WHERE `id` = 123 -$table->where('id > ?', $value); // WHERE `id` > 123 -$table->where('id = ? OR name = ?', $id, $name); // WHERE `id` = 1 OR `name` = 'Jon Snow' -``` - -Dank der automatischen Erkennung passender Operatoren müssen Sie sich nicht um verschiedene Sonderfälle kümmern - Nette löst sie für Sie: - -```php -$table->where('id', 1); // WHERE `id` = 1 -$table->where('id', null); // WHERE `id` IS NULL -$table->where('id', [1, 2, 3]); // WHERE `id` IN (1, 2, 3) -// Sie können auch das Fragezeichen ohne Operator verwenden: -$table->where('id ?', 1); // WHERE `id` = 1 -``` - -Die Methode verarbeitet auch negative Bedingungen und leere Arrays korrekt: - -```php -$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- findet nichts -$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- findet alles -$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- findet alles -// $table->where('NOT id ?', $ids); // ACHTUNG: Diese Syntax wird nicht unterstützt -``` - -Als Parameter können Sie auch das Ergebnis einer anderen Tabellenabfrage übergeben - dabei entsteht eine Unterabfrage: - -```php -// WHERE `id` IN (SELECT `id` FROM `tableName`) -$table->where('id', $explorer->table($tableName)); - -// WHERE `id` IN (SELECT `col` FROM `tableName`) -$table->where('id', $explorer->table($tableName)->select('col')); -``` - -Bedingungen lassen sich auch als Array übergeben, dessen Elemente mit AND verknüpft werden: - -```php -// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) -$table->where([ - 'price_final < price_original', - 'stock_count > min_stock', -]); -``` - -Im Array können Sie Paare aus Schlüssel => Wert verwenden, und Nette wählt wieder automatisch die richtigen Operatoren: - -```php -// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3)) -$table->where([ - 'status' => 'active', - 'id' => [1, 2, 3], -]); -``` - -Im Array können Sie SQL-Ausdrücke mit Fragezeichen und mehreren Parametern kombinieren. Das eignet sich für komplexe Bedingungen mit genau festgelegten Operatoren: - -```php -// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) -$table->where([ - 'age > ?' => 18, - 'ROUND(score, ?) > ?' => [2, 75.5], // zwei Parameter werden als Array übergeben -]); -``` - -Mehrfache Aufrufe von `where()` verknüpfen die Bedingungen automatisch mit AND. - - -whereOr(array $parameters): static .[method] --------------------------------------------- - -Fügt ähnlich wie `where()` Bedingungen hinzu, verknüpft sie aber mit OR: - -```php -// WHERE (`status` = 'active') OR (`deleted` = 1) -$table->whereOr([ - 'status' => 'active', - 'deleted' => true, -]); -``` - -Auch hier lassen sich komplexere Ausdrücke verwenden: - -```php -// WHERE (`price` > 1000) OR (`price_with_tax` > 1500) -$table->whereOr([ - 'price > ?' => 1000, - 'price_with_tax > ?' => 1500, -]); -``` - - -wherePrimary(mixed $key): static .[method] ------------------------------------------- - -Fügt eine Bedingung für den Primärschlüssel der Tabelle hinzu: - -```php -// WHERE `id` = 123 -$table->wherePrimary(123); - -// WHERE `id` IN (1, 2, 3) -$table->wherePrimary([1, 2, 3]); -``` - -Wenn die Tabelle einen zusammengesetzten Primärschlüssel hat (z. B. `foo_id`, `bar_id`), übergeben Sie ihn als Array: - -```php -// WHERE `foo_id` = 1 AND `bar_id` = 5 -$table->wherePrimary(['foo_id' => 1, 'bar_id' => 5])->fetch(); - -// WHERE (`foo_id`, `bar_id`) IN ((1, 5), (2, 3)) -$table->wherePrimary([ - ['foo_id' => 1, 'bar_id' => 5], - ['foo_id' => 2, 'bar_id' => 3], -])->fetchAll(); -``` - - -order(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Bestimmt die Reihenfolge, in der die Zeilen zurückgegeben werden. Sie können nach einer oder mehreren Spalten sortieren, aufsteigend oder absteigend, oder nach einem eigenen Ausdruck: - -```php -$table->order('created'); // ORDER BY `created` -$table->order('created DESC'); // ORDER BY `created` DESC -$table->order('priority DESC, created'); // ORDER BY `priority` DESC, `created` -$table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC -``` - - -select(string $columns, ...$parameters): static .[method] ---------------------------------------------------------- - -Gibt an, welche Spalten aus der Datenbank zurückgegeben werden sollen. Standardmäßig gibt Nette Database Explorer nur die Spalten zurück, die im Code tatsächlich verwendet werden. Die Methode `select()` verwenden Sie also dann, wenn Sie bestimmte Ausdrücke zurückgeben müssen: - -```php -// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` -$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); -``` - -Mit `AS` definierte Aliase sind dann als Properties des `ActiveRow`-Objekts verfügbar: - -```php -foreach ($table as $row) { - echo $row->formatted_date; // Zugriff auf den Alias -} -``` - - -limit(?int $limit, ?int $offset = null): static .[method] ---------------------------------------------------------- - -Begrenzt die Anzahl der zurückgegebenen Zeilen (LIMIT) und erlaubt optional das Setzen eines Offsets: - -```php -$table->limit(10); // LIMIT 10 (gibt die ersten 10 Zeilen zurück) -$table->limit(10, 20); // LIMIT 10 OFFSET 20 -``` - -Für die Paginierung ist es sinnvoller, die Methode `page()` zu verwenden. - - -page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] -------------------------------------------------------------------------- - -Erleichtert die Paginierung der Ergebnisse. Sie nimmt die Seitennummer (beginnend bei 1) und die Anzahl der Einträge pro Seite entgegen. Optional können Sie eine Referenz auf eine Variable übergeben, in der die Gesamtzahl der Seiten gespeichert wird: - -```php -$numOfPages = null; -$table->page(page: 3, itemsPerPage: 10, numOfPages: $numOfPages); -echo "Seiten insgesamt: $numOfPages"; -``` - - -group(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Gruppiert die Zeilen nach den angegebenen Spalten (GROUP BY). Üblicherweise wird das in Verbindung mit Aggregatfunktionen verwendet: - -```php -// Zählt die Anzahl der Produkte in jeder Kategorie -$table->select('category_id, COUNT(*) AS count') - ->group('category_id'); -``` - - -having(string $having, ...$parameters): static .[method] --------------------------------------------------------- - -Setzt eine Bedingung zum Filtern gruppierter Zeilen (HAVING). Sie lässt sich in Verbindung mit der Methode `group()` und Aggregatfunktionen verwenden: - -```php -// Findet Kategorien mit mehr als 100 Produkten -$table->select('category_id, COUNT(*) AS count') - ->group('category_id') - ->having('count > ?', 100); -``` - - -Daten lesen -=========== - -Zum Lesen von Daten aus der Datenbank stehen mehrere nützliche Methoden zur Verfügung: - -.[language-php] -| `foreach ($table as $key => $row)` | Iteriert über alle Zeilen, `$key` ist der Wert des Primärschlüssels, `$row` ist ein ActiveRow-Objekt | -| `$row = $table->get($key)` | Gibt eine einzelne Zeile anhand des Primärschlüssels zurück | -| `$row = $table->fetch()` | Gibt die aktuelle Zeile zurück und setzt den Zeiger auf die nächste | -| `$array = $table->fetchPairs()` | Erzeugt aus den Ergebnissen ein assoziatives Array | -| `$array = $table->fetchAll()` | Gibt alle Zeilen als Array zurück | -| `count($table)` | Gibt die Anzahl der Zeilen im Selection-Objekt zurück | - -Das Objekt [ActiveRow |api:Nette\Database\Table\ActiveRow] ist nur zum Lesen bestimmt. Das heißt, Sie können die Werte seiner Properties nicht ändern. Diese Einschränkung sichert die Konsistenz der Daten und verhindert unerwartete Nebenwirkungen. Die Daten werden aus der Datenbank geladen, und jede Änderung sollte explizit und kontrolliert erfolgen. - - -`foreach` - Iteration über alle Zeilen --------------------------------------- - -Der einfachste Weg, eine Query auszuführen und die Zeilen zu erhalten, ist die Iteration in einer `foreach`-Schleife. Sie führt die SQL-Query automatisch aus. - -```php -$books = $explorer->table('book'); -foreach ($books as $key => $book) { - // $key ist der Wert des Primärschlüssels, $book ist ActiveRow - echo "$book->title ({$book->author->name})"; -} -``` - - -get($key): ?ActiveRow .[method] -------------------------------- - -Führt die SQL-Query aus und gibt die Zeile anhand des Primärschlüssels zurück, oder `null`, wenn sie nicht existiert. - -```php -$book = $explorer->table('book')->get(123); // gibt ActiveRow mit der ID 123 oder null zurück -if ($book) { - echo $book->title; -} -``` - - -fetch(): ?ActiveRow .[method] ------------------------------ - -Gibt die aktuelle Zeile zurück und setzt den internen Zeiger auf die nächste. Wenn es keine weiteren Zeilen gibt, wird `null` zurückgegeben. - -```php -$books = $explorer->table('book'); -while ($book = $books->fetch()) { - $this->processBook($book); -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Gibt die Ergebnisse als assoziatives Array zurück. Das erste Argument bestimmt den Namen der Spalte, die als Schlüssel im Array verwendet wird, das zweite Argument den Namen der Spalte, die als Wert verwendet wird: - -```php -$authors = $explorer->table('author')->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Wenn nur der erste Parameter angegeben wird, ist der Wert die gesamte Zeile, also das Objekt `ActiveRow`: - -```php -$authors = $explorer->table('author')->fetchPairs('id'); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - -Bei doppelten Schlüsseln wird der Wert aus der letzten Zeile verwendet. Wird `null` als Schlüssel verwendet, ist das Array numerisch ab null indiziert (dann treten keine Kollisionen auf): - -```php -$authors = $explorer->table('author')->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Alternativ können Sie als Parameter einen Callback angeben, der für jede Zeile entweder einen einzelnen Wert oder ein Schlüssel-Wert-Paar zurückgibt. - -```php -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); -// ['Erstes Buch (John Novak)', ...] - -// Der Callback kann auch ein Array mit einem Schlüssel-Wert-Paar zurückgeben: -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => [$row->title, $row->author->name]); -// ['Erstes Buch' => 'John Novak', ...] -``` - - -fetchAll(): array .[method] ---------------------------- - -Gibt alle Zeilen als assoziatives Array von `ActiveRow`-Objekten zurück, wobei die Schlüssel die Werte der Primärschlüssel sind. - -```php -$allBooks = $explorer->table('book')->fetchAll(); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - - -count(): int .[method] ----------------------- - -Die Methode `count()` ohne Parameter gibt die Anzahl der Zeilen im Objekt `Selection` zurück: - -```php -$table->where('category', 1); -$count = $table->count(); -$count = count($table); // Alternative -``` - -Achtung: `count()` mit einem Parameter führt die Aggregatfunktion COUNT in der Datenbank aus, siehe unten. - - -ActiveRow::toArray(): array .[method] -------------------------------------- - -Wandelt das Objekt `ActiveRow` in ein assoziatives Array um, in dem die Schlüssel die Spaltennamen und die Werte die zugehörigen Daten sind. - -```php -$book = $explorer->table('book')->get(1); -$bookArray = $book->toArray(); -// $bookArray ist ['id' => 1, 'title' => '...', 'author_id' => ..., ...] -``` - - -Aggregation -=========== - -Die Klasse `Selection` stellt Methoden bereit, mit denen sich Aggregatfunktionen (COUNT, SUM, MIN, MAX, AVG usw.) leicht ausführen lassen. - -.[language-php] -| `count($expr)` | Zählt die Anzahl der Zeilen | -| `min($expr)` | Gibt den kleinsten Wert einer Spalte zurück | -| `max($expr)` | Gibt den größten Wert einer Spalte zurück | -| `sum($expr)` | Gibt die Summe der Werte einer Spalte zurück | -| `aggregation($function)` | Erlaubt eine beliebige Aggregatfunktion, etwa `AVG()` oder `GROUP_CONCAT()` | - - -count(string $expr): int .[method] ----------------------------------- - -Führt eine SQL-Query mit der Funktion COUNT aus und gibt das Ergebnis zurück. Die Methode wird verwendet, um zu ermitteln, wie viele Zeilen einer bestimmten Bedingung entsprechen: - -```php -$count = $table->count('*'); // SELECT COUNT(*) FROM `table` -$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` -``` - -Achtung: [#count()] ohne Parameter gibt nur die Anzahl der Zeilen im Objekt `Selection` zurück. - - -min(string $expr) and max(string $expr) .[method] -------------------------------------------------- - -Die Methoden `min()` und `max()` geben den kleinsten und den größten Wert in der angegebenen Spalte oder im angegebenen Ausdruck zurück: - -```php -// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 -$maxPrice = $products->where('active', true) - ->max('price'); -``` - - -sum(string $expr): mixed .[method] ----------------------------------- - -Gibt die Summe der Werte in der angegebenen Spalte oder im angegebenen Ausdruck zurück: - -```php -// SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 -$totalPrice = $products->where('active', true) - ->sum('price * items_in_stock'); -``` - - -aggregation(string $function, ?string $groupFunction = null): mixed .[method] ------------------------------------------------------------------------------ - -Erlaubt die Ausführung einer beliebigen Aggregatfunktion. - -```php -// Durchschnittspreis der Produkte in einer Kategorie -$avgPrice = $products->where('category_id', 1) - ->aggregation('AVG(price)'); - -// verbindet die Tags eines Produkts zu einem einzigen String -$tags = $products->where('id', 1) - ->aggregation('GROUP_CONCAT(tag.name) AS tags') - ->fetch() - ->tags; -``` - -Wenn wir Ergebnisse aggregieren müssen, die selbst schon aus einer Aggregatfunktion und einer Gruppierung stammen (z. B. `SUM(wert)` über gruppierte Zeilen), geben wir als zweites Argument die Aggregatfunktion an, die auf diese Zwischenergebnisse angewendet werden soll: - -```php -// Berechnet den Gesamtpreis der Produkte auf Lager für die einzelnen Kategorien und summiert diese Preise anschließend. -$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') - ->group('category_id') - ->aggregation('SUM(category_total)', 'SUM'); -``` - -In diesem Beispiel berechnen wir zuerst den Gesamtpreis der Produkte in jeder Kategorie (`SUM(price * stock) AS category_total`) und gruppieren die Ergebnisse nach `category_id`. Dann verwenden wir `aggregation('SUM(category_total)', 'SUM')`, um diese Zwischensummen `category_total` zu addieren. Das zweite Argument `'SUM'` sagt, dass auf die Zwischenergebnisse die Funktion SUM angewendet werden soll. - - -Insert, Update & Delete -======================= - -Nette Database Explorer vereinfacht das Einfügen, Aktualisieren und Löschen von Daten. Alle genannten Methoden werfen im Fehlerfall eine `Nette\Database\DriverException`. - - -Selection::insert(iterable $data) .[method] -------------------------------------------- - -Fügt neue Datensätze in die Tabelle ein. - -**Einfügen eines einzelnen Datensatzes:** - -Übergeben Sie den neuen Datensatz als assoziatives Array oder als iterierbares Objekt (etwa `ArrayHash`, das in [Formularen |forms:] verwendet wird), dessen Schlüssel den Spaltennamen in der Tabelle entsprechen. - -Wenn die Tabelle einen definierten Primärschlüssel hat, gibt die Methode ein `ActiveRow`-Objekt zurück, das aus der Datenbank neu geladen wird, um eventuelle Änderungen auf Datenbankebene zu berücksichtigen (Trigger, Standardwerte von Spalten, Berechnung von Auto-Increment-Spalten). Damit ist die Konsistenz der Daten sichergestellt und das Objekt enthält immer die aktuellen Daten aus der Datenbank. Hat die Tabelle keinen Primärschlüssel, gibt es keine identifizierbare Zeile und die Methode gibt `null` zurück. - -```php -$row = $explorer->table('users')->insert([ - 'name' => 'John Doe', - 'email' => 'john.doe@example.com', -]); -// $row ist eine Instanz von ActiveRow und enthält die vollständigen Daten der eingefügten Zeile, -// einschließlich der automatisch erzeugten ID und eventueller von Triggern vorgenommener Änderungen -echo $row->id; // Gibt die ID des neu eingefügten Benutzers aus -echo $row->created_at; // Gibt die Erstellungszeit aus, wenn sie von einem Trigger gesetzt wird -``` - -**Einfügen mehrerer Datensätze auf einmal:** - -Die Methode `insert()` erlaubt das Einfügen mehrerer Datensätze mit einer einzigen SQL-Query. In diesem Fall gibt sie die Anzahl der eingefügten Zeilen zurück. - -```php -$insertedRows = $explorer->table('users')->insert([ - [ - 'name' => 'John', - 'year' => 1994, - ], - [ - 'name' => 'Jack', - 'year' => 1995, - ], -]); -// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) -// $insertedRows ist 2 -``` - -Als Parameter lässt sich auch ein `Selection`-Objekt mit einer Datenauswahl übergeben. - -```php -$newUsers = $explorer->table('potential_users') - ->where('approved', 1) - ->select('name, email'); - -$insertedRows = $explorer->table('users')->insert($newUsers); -``` - -**Einfügen spezieller Werte:** - -Als Werte können wir auch Dateien, `DateTime`-Objekte oder SQL-Literale übergeben: - -```php -$explorer->table('users')->insert([ - 'name' => 'John', - 'created_at' => new DateTime, // wandelt in das Datenbankformat um - 'avatar' => fopen('image.jpg', 'rb'), // fügt den binären Inhalt der Datei ein - 'uuid' => $explorer::literal('UUID()'), // ruft die Funktion UUID() auf -]); -``` - - -Selection::update(iterable $data): int .[method] ------------------------------------------------- - -Aktualisiert die Zeilen der Tabelle nach dem angegebenen Filter. Gibt die Anzahl der tatsächlich geänderten Zeilen zurück. - -Übergeben Sie die zu ändernden Spalten als assoziatives Array oder als iterierbares Objekt (etwa `ArrayHash`, das in [Formularen |forms:] verwendet wird), dessen Schlüssel den Spaltennamen in der Tabelle entsprechen: - -```php -$affected = $explorer->table('users') - ->where('id', 10) - ->update([ - 'name' => 'John Smith', - 'year' => 1994, - ]); -// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 -``` - -Zum Ändern numerischer Werte können Sie die Operatoren `+=` und `-=` verwenden: - -```php -$explorer->table('users') - ->where('id', 10) - ->update([ - 'points+=' => 1, // erhöht den Wert der Spalte 'points' um 1 - 'coins-=' => 1, // verringert den Wert der Spalte 'coins' um 1 - ]); -// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 -``` - - -Selection::delete(): int .[method] ----------------------------------- - -Löscht Zeilen aus der Tabelle nach dem angegebenen Filter. Gibt die Anzahl der gelöschten Zeilen zurück. - -```php -$count = $explorer->table('users') - ->where('id', 10) - ->delete(); -// DELETE FROM `users` WHERE `id` = 10 -``` - -.[caution] -Vergessen Sie beim Aufruf von `update()` oder `delete()` nicht, mit `where()` die Zeilen anzugeben, die geändert bzw. gelöscht werden sollen. Wenn Sie `where()` nicht verwenden, wird die Operation auf der gesamten Tabelle ausgeführt! - - -ActiveRow::update(iterable $data): bool .[method] -------------------------------------------------- - -Aktualisiert die Daten in der Datenbankzeile, die durch das Objekt `ActiveRow` repräsentiert wird. Die Methode nimmt ein Iterable mit den zu aktualisierenden Daten entgegen (die Schlüssel sind Spaltennamen). Zum Ändern numerischer Werte können Sie die Operatoren `+=` und `-=` verwenden: - -Nach der Aktualisierung wird das `ActiveRow` automatisch aus der Datenbank neu geladen, um eventuelle Änderungen auf Datenbankebene zu berücksichtigen (z. B. durch Trigger). Die Methode gibt nur dann `true` zurück, wenn eine tatsächliche Datenänderung stattgefunden hat. - -```php -$article = $explorer->table('article')->get(1); -$article->update([ - 'views += 1', // erhöht die Anzahl der Aufrufe -]); -echo $article->views; // Gibt die aktuelle Anzahl der Aufrufe aus -``` - -Diese Methode aktualisiert nur eine bestimmte Zeile in der Datenbank. Für die Massenaktualisierung mehrerer Zeilen verwenden Sie die Methode [#Selection::update()]. - - -ActiveRow::delete(): int .[method] ----------------------------------- - -Löscht die Zeile aus der Datenbank, die durch das Objekt `ActiveRow` repräsentiert wird. Gibt die Anzahl der gelöschten Zeilen zurück, die 1 sein sollte. - -```php -$book = $explorer->table('book')->get(1); -$book->delete(); // Löscht das Buch mit der ID 1 -``` - -Diese Methode löscht nur eine bestimmte Zeile in der Datenbank. Für das Massenlöschen mehrerer Zeilen verwenden Sie die Methode [#Selection::delete()]. - - -Beziehungen zwischen Tabellen -============================= - -In relationalen Datenbanken sind die Daten auf mehrere Tabellen verteilt und über Fremdschlüssel miteinander verknüpft. Nette Database Explorer bringt eine revolutionäre Art, mit diesen Beziehungen zu arbeiten - ohne JOIN-Queries zu schreiben und ohne dass etwas konfiguriert oder generiert werden müsste. - -Zur Veranschaulichung der Arbeit mit Beziehungen verwenden wir eine Beispieldatenbank mit Büchern ([Sie finden sie auf GitHub |https://github.com/nette-examples/books]). In der Datenbank haben wir die Tabellen: - -- `author` - Schriftsteller und Übersetzer (Spalten `id`, `name`, `web`, `born`) -- `book` - Bücher (Spalten `id`, `author_id`, `translator_id`, `title`, `sequel_id`) -- `tag` - Tags (Spalten `id`, `name`) -- `book_tag` - Verknüpfungstabelle zwischen Büchern und Tags (Spalten `book_id`, `tag_id`) - -[* db-schema-1-.webp *] *** Struktur der in den Beispielen verwendeten Datenbank .<> - -In unserer Beispieldatenbank mit Büchern finden wir mehrere Arten von Beziehungen (auch wenn das Modell gegenüber der Realität vereinfacht ist): - -- **One-to-many (1:N)** - Jedes Buch **hat einen** Autor; ein Autor kann **mehrere** Bücher schreiben. -- **Zero-to-many (0:N)** - Ein Buch **kann** einen Übersetzer **haben**; ein Übersetzer kann **mehrere** Bücher übersetzen. -- **Zero-to-one (0:1)** - Ein Buch **kann** eine Fortsetzung **haben**. -- **Many-to-many (M:N)** - Ein Buch **kann mehrere** Tags haben, und ein Tag kann **mehreren** Büchern zugeordnet sein. - -In diesen Beziehungen gibt es immer eine **übergeordnete Tabelle** und eine **untergeordnete Tabelle**. Zum Beispiel ist in der Beziehung zwischen Autoren und Büchern die Tabelle `author` die übergeordnete und die Tabelle `book` die untergeordnete - Sie können sich das so vorstellen, dass ein Buch immer zu einem Autor "gehört". Das zeigt sich auch in der Struktur der Datenbank: Die untergeordnete Tabelle `book` enthält den Fremdschlüssel `author_id`, der auf die übergeordnete Tabelle `author` verweist. - -Wenn wir Bücher samt den Namen ihrer Autoren ausgeben müssen, haben wir zwei Möglichkeiten. Entweder holen wir die Daten mit einer einzigen SQL-Query per JOIN: - -```sql -SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; -``` - -Oder wir laden die Daten in zwei Schritten - zuerst die Bücher, dann ihre Autoren - und setzen sie anschließend in PHP zusammen: - -```sql -SELECT * FROM book; -SELECT * FROM author WHERE id IN (1, 2, 3); -- IDs der Autoren der ausgewählten Bücher -``` - -Der zweite Ansatz ist in Wirklichkeit **effizienter**, auch wenn das überraschen mag. Die Daten werden nur einmal geladen und lassen sich besser im Cache nutzen. Genau so arbeitet Nette Database Explorer - er löst alles unter der Oberfläche und bietet Ihnen eine elegante API: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo 'title: ' . $book->title; - echo 'written by: ' . $book->author->name; // $book->author ist ein Datensatz aus der Tabelle 'author' - echo 'translated by: ' . $book->translator?->name; -} -``` - - -Zugriff auf die übergeordnete Tabelle -------------------------------------- - -Der Zugriff auf die übergeordnete Tabelle ist geradlinig. Es geht um Beziehungen wie *ein Buch hat einen Autor* oder *ein Buch kann einen Übersetzer haben*. Den verwandten Datensatz erhalten wir über eine Property des ActiveRow-Objekts - ihr Name entspricht dem Namen der Fremdschlüsselspalte ohne das Suffix `_id`: - -```php -$book = $explorer->table('book')->get(1); -echo $book->author->name; // findet den Autor anhand der Spalte author_id -echo $book->translator?->name; // findet den Übersetzer anhand der Spalte translator_id -``` - -Wenn wir auf die Property `$book->author` zugreifen, sucht der Explorer in der Tabelle `book` nach einer Spalte, deren Name die Zeichenfolge `author` enthält (also `author_id`). Anhand des Werts in dieser Spalte lädt er den entsprechenden Datensatz aus der Tabelle `author` und gibt ihn als `ActiveRow` zurück. Ebenso funktioniert `$book->translator`, das die Spalte `translator_id` nutzt. Weil die Spalte `translator_id` den Wert `null` enthalten kann, verwenden wir im Code den Nullsafe-Operator `?->`. - -Einen alternativen Weg bietet die Methode `ref()`, die zwei Argumente entgegennimmt, den Namen der Zieltabelle und den Namen der verbindenden Spalte, und eine `ActiveRow`-Instanz oder `null` zurückgibt: - -```php -echo $book->ref('author', 'author_id')->name; // Beziehung zum Autor -echo $book->ref('author', 'translator_id')->name; // Beziehung zum Übersetzer -``` - -Die Methode `ref()` ist nützlich, wenn sich der Zugriff über eine Property nicht verwenden lässt, etwa weil die Tabelle eine Spalte mit demselben Namen enthält (also `author`). In den übrigen Fällen wird der Zugriff über Properties empfohlen, weil er besser lesbar ist. - -Der Explorer optimiert die Datenbank-Queries automatisch. Wenn wir Bücher in einer Schleife durchlaufen und auf ihre verwandten Datensätze (Autoren, Übersetzer) zugreifen, erzeugt der Explorer nicht für jedes Buch eine eigene Query. Stattdessen führt er nur **eine SELECT-Query für jede Art von Beziehung** aus, was die Last der Datenbank deutlich senkt. Zum Beispiel: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo $book->title . ': '; - echo $book->author->name; - echo $book->translator?->name; -} -``` - -Dieser Code führt nur diese drei blitzschnellen Queries an die Datenbank aus: - -```sql -SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- IDs aus der Spalte author_id der ausgewählten Bücher -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- IDs aus der Spalte translator_id der ausgewählten Bücher -``` - -.[note] -Die Logik zum Auffinden der verbindenden Spalte wird durch die Implementierung von [Conventions |api:Nette\Database\Conventions] bestimmt. Wir empfehlen die Verwendung von [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], die Fremdschlüssel analysiert und Ihnen erlaubt, einfach mit den bestehenden Beziehungen zwischen Tabellen zu arbeiten. - - -Zugriff auf die untergeordnete Tabelle --------------------------------------- - -Der Zugriff auf die untergeordnete Tabelle funktioniert in umgekehrter Richtung. Jetzt fragen wir, *welche Bücher dieser Autor geschrieben* oder *welche Bücher dieser Übersetzer übersetzt hat*. Für diese Art von Abfrage verwenden wir die Methode `related()`, die eine `Selection` mit den verwandten Datensätzen zurückgibt. Sehen wir uns ein Beispiel an: - -```php -$author = $explorer->table('author')->get(1); - -// Gibt alle Bücher des Autors aus -foreach ($author->related('book.author_id') as $book) { - echo "Geschrieben: $book->title"; -} - -// Gibt alle Bücher aus, die der Autor übersetzt hat -foreach ($author->related('book.translator_id') as $book) { - echo "Übersetzt: $book->title"; -} -``` - -Die Methode `related()` nimmt die Beschreibung der Verknüpfung als ein einziges Argument in Punktnotation oder als zwei getrennte Argumente entgegen: - -```php -$author->related('book.translator_id'); // ein Argument -$author->related('book', 'translator_id'); // zwei Argumente -``` - -Der Explorer kann die richtige verbindende Spalte automatisch anhand des Namens der übergeordneten Tabelle erkennen. In diesem Fall wird über die Spalte `book.author_id` verknüpft, weil der Name der Quelltabelle `author` lautet: - -```php -$author->related('book'); // verwendet book.author_id -``` - -Wenn mehrere mögliche Verknüpfungen existieren, wirft der Explorer eine [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -Die Methode `related()` können wir natürlich auch beim Durchlaufen mehrerer Datensätze in einer Schleife verwenden, und der Explorer optimiert die Queries auch in diesem Fall automatisch: - -```php -$authors = $explorer->table('author'); -foreach ($authors as $author) { - echo $author->name . ' schrieb:'; - foreach ($author->related('book') as $book) { - echo $book->title; - } -} -``` - -Dieser Code erzeugt nur zwei blitzschnelle SQL-Queries: - -```sql -SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- IDs der ausgewählten Autoren -``` - - -Many-to-Many-Beziehung ----------------------- - -Für eine Many-to-many-Beziehung (M:N) wird eine **Verknüpfungstabelle** benötigt (in unserem Fall `book_tag`), die zwei Fremdschlüsselspalten enthält (`book_id`, `tag_id`). Jede dieser Spalten verweist auf den Primärschlüssel einer der verknüpften Tabellen. Um die verwandten Daten zu erhalten, holen wir zuerst die Datensätze aus der Verknüpfungstabelle mit `related('book_tag')` und gehen dann weiter zu den Zieldaten: - -```php -$book = $explorer->table('book')->get(1); -// gibt die Namen der dem Buch zugeordneten Tags aus -foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name; // gibt den Namen des Tags über die Verknüpfungstabelle aus -} - -$tag = $explorer->table('tag')->get(1); -// oder umgekehrt: gibt die Namen der mit diesem Tag markierten Bücher aus -foreach ($tag->related('book_tag') as $bookTag) { - echo $bookTag->book->title; // gibt den Titel des Buches aus -} -``` - -Der Explorer optimiert die SQL-Queries wieder in eine effiziente Form: - -```sql -SELECT * FROM `book`; -SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- IDs der ausgewählten Bücher -SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- IDs der in book_tag gefundenen Tags -``` - - -Abfragen über verwandte Tabellen --------------------------------- - -In den Methoden `where()`, `select()`, `order()` und `group()` können Sie spezielle Notationen verwenden, um auf Spalten aus anderen Tabellen zuzugreifen. Der Explorer erzeugt die nötigen JOINs automatisch. - -**Punktnotation** (`übergeordnete_tabelle.spalte`) wird für 1:N-Beziehungen aus Sicht der untergeordneten Tabelle verwendet: - -```php -$books = $explorer->table('book'); - -// Findet Bücher, deren Autorenname mit 'Jon' beginnt -$books->where('author.name LIKE ?', 'Jon%'); - -// Sortiert die Bücher absteigend nach dem Autorennamen -$books->order('author.name DESC'); - -// Gibt den Buchtitel und den Autorennamen aus -$books->select('book.title, author.name'); -``` - -**Doppelpunktnotation** (`:untergeordnete_tabelle.spalte`) wird für 1:N-Beziehungen aus Sicht der übergeordneten Tabelle verwendet: - -```php -$authors = $explorer->table('author'); - -// Findet Autoren, die ein Buch mit 'PHP' im Titel geschrieben haben -$authors->where(':book.title LIKE ?', '%PHP%'); - -// Zählt die Anzahl der Bücher je Autor -$authors->select('*, COUNT(:book.id) AS book_count') - ->group('author.id'); -``` - -Im obigen Beispiel mit der Doppelpunktnotation (`:book.title`) ist die Fremdschlüsselspalte nicht angegeben. Der Explorer erkennt die richtige Spalte automatisch anhand des Namens der übergeordneten Tabelle. In diesem Fall wird über die Spalte `book.author_id` verknüpft, weil der Name der Quelltabelle `author` lautet. Wenn mehrere mögliche Verknüpfungen existieren, wirft der Explorer eine [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -Die verbindende Spalte lässt sich explizit in Klammern angeben: - -```php -// Findet Autoren, die ein Buch mit 'PHP' im Titel übersetzt haben -$authors->where(':book(translator_id).title LIKE ?', '%PHP%'); -``` - -Die Notationen lassen sich verketten, um über mehrere Tabellen hinweg auf Daten zuzugreifen: - -```php -// Findet die Autoren von Büchern, die mit dem Tag 'PHP' markiert sind -$authors->where(':book:book_tag.tag.name', 'PHP') - ->group('author.id'); -``` - - -Erweiterung der Bedingungen für JOIN ------------------------------------- - -Die Methode `joinWhere()` erweitert die Bedingungen, die beim Verknüpfen von Tabellen in SQL hinter dem Schlüsselwort `ON` angegeben werden. - -Nehmen wir an, wir wollen Bücher finden, die von einem bestimmten Übersetzer übersetzt wurden: - -```php -// Findet Bücher, die von einem Übersetzer namens 'David' übersetzt wurden -$books = $explorer->table('book') - ->joinWhere('translator', 'translator.name', 'David'); -// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') -``` - -In der Bedingung von `joinWhere()` können Sie dieselben Konstrukte verwenden wie in der Methode `where()` - Operatoren, Fragezeichen, Arrays von Werten oder SQL-Ausdrücke. - -Für komplexere Queries mit mehreren JOINs können Sie Tabellenaliase definieren: - -```php -$tags = $explorer->table('tag') - ->joinWhere(':book_tag.book.author', 'book_author.born < ?', 1950) - ->alias(':book_tag.book.author', 'book_author'); -// LEFT JOIN `book_tag` ON `tag`.`id` = `book_tag`.`tag_id` -// LEFT JOIN `book` ON `book_tag`.`book_id` = `book`.`id` -// LEFT JOIN `author` `book_author` ON `book`.`author_id` = `book_author`.`id` -// AND (`book_author`.`born` < 1950) -``` - -Beachten Sie, dass die Methode `where()` Bedingungen zur `WHERE`-Klausel hinzufügt, während die Methode `joinWhere()` die Bedingungen in der `ON`-Klausel beim Verknüpfen der Tabellen erweitert. diff --git a/database/de/guide.texy b/database/de/guide.texy deleted file mode 100644 index c0a27e0f8d..0000000000 --- a/database/de/guide.texy +++ /dev/null @@ -1,218 +0,0 @@ -Nette Database -************** - -.[perex] -Nette Database ist eine leistungsfähige und elegante Datenbankschicht für PHP mit dem Fokus auf Einfachheit und intelligente Funktionen. Sie bietet zwei Arten, mit der Datenbank zu arbeiten: den [Explorer |explorer] für die schnelle Anwendungsentwicklung oder den [SQL-Weg |SQL way] für die direkte Arbeit mit Queries. - -<div class="grid gap-3"> -<div> - - -[SQL-Weg |sql-way] -================== -- Sichere, parametrisierte Queries -- Genaue Kontrolle über den Aufbau der SQL-Queries -- Wenn Sie komplexe Queries mit fortgeschrittenen Funktionen schreiben -- Optimierung der Leistung mit spezifischen SQL-Funktionen - -</div> - -<div> - - -[Explorer |explorer] -==================== -- Schnell entwickeln, ohne SQL zu schreiben -- Intuitive Arbeit mit Beziehungen zwischen Tabellen -- Automatische Optimierung der Queries nutzen -- Geeignet für schnelles und bequemes Arbeiten mit der Datenbank - -</div> - -</div> - - -Installation -============ - -Die Bibliothek laden und installieren Sie mit [Composer |best-practices:composer]: - -```shell -composer require nette/database -``` - - -Unterstützte Datenbanken -======================== - -Nette Database unterstützt die folgenden Datenbanken: - -|* Datenbankserver |* DSN-Name |* Explorer-Unterstützung -|-----------------------|--------------|-----------------------| -| MySQL (>= 5.1) | mysql | JA | -| PostgreSQL (>= 9.0) | pgsql | JA | -| SQLite 3 (>= 3.8) | sqlite | JA | -| Oracle | oci | NEIN | -| MS SQL (PDO_SQLSRV) | sqlsrv | JA | -| MS SQL (PDO_DBLIB) | mssql | NEIN | -| ODBC | odbc | NEIN | - - -Zwei Zugänge zur Datenbank -========================== - -Nette Database lässt Ihnen die Wahl: Sie können SQL-Queries entweder direkt schreiben (SQL-Weg) oder sie automatisch generieren lassen (Explorer). Sehen wir uns an, wie beide Zugänge dieselben Aufgaben lösen: - -[SQL-Weg |sql-way] - SQL-Queries - -```php -// Einfügen eines Datensatzes -$database->query('INSERT INTO books', [ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// Laden von Datensätzen: Autoren der Bücher -$result = $database->query(' - SELECT authors.*, COUNT(books.id) AS books_count - FROM authors - LEFT JOIN books ON authors.id = books.author_id - WHERE authors.active = 1 - GROUP BY authors.id -'); - -// Ausgabe (nicht optimal, erzeugt N weitere Queries) -foreach ($result as $author) { - $books = $database->query(' - SELECT * FROM books - WHERE author_id = ? - ORDER BY published_at DESC - ', $author->id); - - echo "Autor $author->name hat $author->books_count Bücher geschrieben:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -[Explorer-Weg |explorer] - automatische SQL-Generierung - -```php -// Einfügen eines Datensatzes -$database->table('books')->insert([ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// Laden von Datensätzen: Autoren der Bücher -$authors = $database->table('authors') - ->where('active', 1); - -// Ausgabe (erzeugt automatisch nur 2 optimierte Queries) -foreach ($authors as $author) { - $books = $author->related('books') - ->order('published_at DESC'); - - echo "Autor $author->name hat {$books->count()} Bücher geschrieben:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -Der Explorer-Zugang erzeugt und optimiert die SQL-Queries automatisch. Im obigen Beispiel erzeugt der SQL-Weg N+1 Queries (eine für die Autoren und dann eine für die Bücher jedes Autors), während der Explorer die Queries automatisch optimiert und nur zwei ausführt - eine für die Autoren und eine für alle ihre Bücher. - -Beide Zugänge lassen sich in der Anwendung nach Bedarf beliebig kombinieren. - - -Verbindung und Konfiguration -============================ - -Um sich mit der Datenbank zu verbinden, genügt es, eine Instanz der Klasse [api:Nette\Database\Connection] zu erzeugen: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password); -``` - -Der Parameter `$dsn` (Data Source Name) ist derselbe, [den PDO verwendet |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], also z. B. `host=127.0.0.1;dbname=test`. Im Fehlerfall wird eine `Nette\Database\ConnectionException` geworfen. - -Bequemer ist jedoch der Weg über die [Anwendungskonfiguration |configuration], in der Sie nur den Abschnitt `database` ergänzen müssen. Dadurch entstehen die nötigen Objekte und außerdem ein Datenbank-Panel in der [Tracy |tracy:] Bar. - -```neon -database: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password -``` - -Das Verbindungsobjekt lässt sich danach [als Service aus dem DI-Container beziehen |dependency-injection:passing-dependencies], z. B.: - -```php -class Model -{ - public function __construct( - // oder Nette\Database\Explorer - private Nette\Database\Connection $database, - ) { - } -} -``` - -Mehr Informationen zur [Datenbankkonfiguration |configuration]. - - -Manuelle Erstellung des Explorers ---------------------------------- - -Wenn Sie den Nette DI-Container nicht verwenden, können Sie eine Instanz von `Nette\Database\Explorer` manuell erzeugen: - -```php -// Verbindung zur Datenbank -$connection = new Nette\Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); -// Speicher für den Cache, implementiert Nette\Caching\Storage, z. B.: -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); -// kümmert sich um die Reflexion der Datenbankstruktur -$structure = new Nette\Database\Structure($connection, $storage); -// definiert die Regeln für das Mapping von Tabellen-, Spalten- und Fremdschlüsselnamen -$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); -$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage); -``` - - -Verbindungsverwaltung -===================== - -Beim Erzeugen eines `Connection`-Objekts wird die Verbindung automatisch aufgebaut. Wenn Sie den Verbindungsaufbau verzögern wollen, verwenden Sie den Lazy-Modus - Sie schalten ihn in der [Konfiguration |configuration] mit `lazy` ein oder so: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => true]); -``` - -Zur Verwaltung der Verbindung dienen die Methoden `connect()`, `disconnect()` und `reconnect()`. -- `connect()` baut die Verbindung auf, falls sie noch nicht existiert, und kann dabei eine `Nette\Database\ConnectionException` werfen. -- `disconnect()` trennt die aktuelle Verbindung zur Datenbank. -- `reconnect()` trennt die Verbindung und baut sie anschließend neu auf. Auch diese Methode kann eine `Nette\Database\ConnectionException` werfen. - -Außerdem können Sie Ereignisse rund um die Verbindung mit dem Event `onConnect` verfolgen, das ein Array von Callbacks ist, die nach dem Aufbau der Verbindung zur Datenbank aufgerufen werden. - -```php -// läuft nach dem Verbinden mit der Datenbank -$database->onConnect[] = function($database) { - echo "Mit der Datenbank verbunden"; -}; -``` - -Das Event `onQuery` funktioniert ähnlich - es ist ein Array von Callbacks, die nach jeder ausgeführten Query aufgerufen werden (auch wenn sie fehlschlägt), was sich für Logging oder Profiling eignet. - - -Tracy Debug Bar -=============== - -Wenn Sie [Tracy |tracy:] verwenden, wird das Panel Database in der Debug Bar automatisch aktiviert. Es zeigt alle ausgeführten Queries, ihre Parameter, die Ausführungszeit und die Stelle im Code, an der sie aufgerufen wurden. - -[* db-panel.webp *] diff --git a/database/de/reflection.texy b/database/de/reflection.texy deleted file mode 100644 index d8b6a0a727..0000000000 --- a/database/de/reflection.texy +++ /dev/null @@ -1,127 +0,0 @@ -Strukturreflexion -***************** - -.{data-version:3.2.1} -Nette Database stellt mit der Klasse [api:Nette\Database\Reflection] Werkzeuge zur Introspektion der Datenbankstruktur bereit. Damit lassen sich Informationen über Tabellen, Spalten, Indizes und Fremdschlüssel abrufen. Sie können die Reflexion nutzen, um Schemata zu generieren, flexible Anwendungen zu bauen, die mit der Datenbank arbeiten, oder allgemeine Datenbankwerkzeuge zu schreiben. - -Das Reflexionsobjekt erhalten Sie von der Instanz der Datenbankverbindung: - -```php -$reflection = $database->getReflection(); -``` - - -Abrufen von Tabellen --------------------- - -Die Readonly-Property `$reflection->tables` enthält ein assoziatives Array aller Tabellen der Datenbank: - -```php -// Ausgabe der Namen aller Tabellen -foreach ($reflection->tables as $name => $table) { - echo $name . "\n"; -} -``` - -Zwei weitere Methoden stehen zur Verfügung: - -```php -// Prüfung, ob eine Tabelle existiert -if ($reflection->hasTable('users')) { - echo "Die Tabelle users existiert"; -} - -// Gibt das Tabellenobjekt zurück; wirft eine Exception, wenn es nicht existiert -$table = $reflection->getTable('users'); -``` - - -Informationen über eine Tabelle -------------------------------- - -Eine Tabelle wird durch das Objekt [Table |api:Nette\Database\Reflection\Table] repräsentiert, das die folgenden Readonly-Properties bietet: - -- `$name: string` - Name der Tabelle -- `$view: bool` - ob es sich um einen View handelt -- `$fullName: ?string` - vollständiger Name der Tabelle einschließlich Schema (falls vorhanden) -- `$columns: array<string, Column>` - assoziatives Array der Tabellenspalten -- `$indexes: Index[]` - Array der Tabellenindizes -- `$primaryKey: ?Index` - Primärschlüssel der Tabelle oder null -- `$foreignKeys: ForeignKey[]` - Array der Fremdschlüssel der Tabelle -- `$comment: ?string` - Kommentar zur Tabelle - - -Spalten -------- - -Die Property `columns` der Tabelle liefert ein assoziatives Array der Spalten, in dem der Schlüssel der Spaltenname und der Wert eine Instanz von [Column |api:Nette\Database\Reflection\Column] mit diesen Properties ist: - -- `$name: string` - Name der Spalte -- `$table: ?Table` - Referenz auf die Tabelle der Spalte -- `$nativeType: string` - nativer Datenbanktyp -- `$size: ?int` - Größe/Länge des Typs -- `$nullable: bool` - ob die Spalte NULL enthalten darf -- `$default: mixed` - Standardwert der Spalte -- `$autoIncrement: bool` - ob die Spalte auto-increment ist -- `$primary: bool` - ob sie Teil des Primärschlüssels ist -- `$vendor: array` - zusätzliche Metadaten, spezifisch für das jeweilige Datenbanksystem -- `$comment: ?string` - Kommentar zur Spalte - -```php -foreach ($table->columns as $name => $column) { - echo "Spalte: $name\n"; - echo "Typ: {$column->nativeType}\n"; - echo "Nullable: " . ($column->nullable ? 'Ja' : 'Nein') . "\n"; -} -``` - - -Indizes -------- - -Die Property `indexes` der Tabelle liefert ein Array von Indizes, wobei jeder Index eine Instanz von [Index |api:Nette\Database\Reflection\Index] mit diesen Properties ist: - -- `$columns: Column[]` - Array der Spalten, die den Index bilden -- `$unique: bool` - ob der Index eindeutig ist -- `$primary: bool` - ob es sich um einen Primärschlüssel handelt -- `$name: ?string` - Name des Index - -Den Primärschlüssel der Tabelle erhalten Sie über die Property `primaryKey`, die entweder ein `Index`-Objekt oder `null` zurückgibt, wenn die Tabelle keinen Primärschlüssel hat. - -```php -// Ausgabe der Indizes -foreach ($table->indexes as $index) { - $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); - echo "Index" . ($index->name ? " {$index->name}" : '') . ":\n"; - echo " Spalten: $columns\n"; - echo " Eindeutig: " . ($index->unique ? 'Ja' : 'Nein') . "\n"; -} - -// Ausgabe des Primärschlüssels -if ($primaryKey = $table->primaryKey) { - $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); - echo "Primärschlüssel: $columns\n"; -} -``` - - -Fremdschlüssel --------------- - -Die Property `foreignKeys` der Tabelle liefert ein Array von Fremdschlüsseln, wobei jeder Fremdschlüssel eine Instanz von [ForeignKey |api:Nette\Database\Reflection\ForeignKey] mit diesen Properties ist: - -- `$foreignTable: Table` - die referenzierte Tabelle -- `$localColumns: Column[]` - Array der lokalen Spalten -- `$foreignColumns: Column[]` - Array der referenzierten Spalten -- `$name: string` - Name des Fremdschlüssels - -```php -// Ausgabe der Fremdschlüssel -foreach ($table->foreignKeys as $fk) { - $localCols = implode(', ', array_map(fn($col) => $col->name, $fk->localColumns)); - $foreignCols = implode(', ', array_map(fn($col) => $col->name, $fk->foreignColumns)); - - echo "FK" . ($fk->name ? " {$fk->name}" : '') . ":\n"; - echo " $localCols -> {$fk->foreignTable->name}($foreignCols)\n"; -} -``` diff --git a/database/de/security.texy b/database/de/security.texy deleted file mode 100644 index 2f07e36e34..0000000000 --- a/database/de/security.texy +++ /dev/null @@ -1,185 +0,0 @@ -Sicherheitsrisiken -****************** - -<div class=perex> - -Datenbanken enthalten oft sensible Daten und erlauben gefährliche Operationen. Für die sichere Arbeit mit Nette Database ist es entscheidend: - -- den Unterschied zwischen sicheren und unsicheren APIs zu verstehen -- parametrisierte Queries zu verwenden -- Eingabedaten richtig zu validieren - -</div> - - -Was ist SQL-Injection? -====================== - -SQL-Injection ist das schwerwiegendste Sicherheitsrisiko bei der Arbeit mit Datenbanken. Sie entsteht, wenn ungeprüfte Benutzereingaben Teil einer SQL-Query werden. Ein Angreifer kann eigene SQL-Befehle einschleusen und dadurch: -- unbefugten Zugriff auf Daten erlangen -- Daten in der Datenbank ändern oder löschen -- die Authentifizierung umgehen - -```php -// ❌ GEFÄHRLICHER CODE - anfällig für SQL-Injection -$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); - -// Ein Angreifer könnte einen Wert wie diesen eingeben: ' OR '1'='1 -// Die entstehende Query wäre: SELECT * FROM users WHERE name = '' OR '1'='1' -// Das gibt alle Benutzer zurück -``` - -Dasselbe gilt für den Database Explorer: - -```php -// ❌ GEFÄHRLICHER CODE - anfällig für SQL-Injection -$table->where('name = ' . $_GET['name']); -$table->where("name = '$_GET[name]'"); -``` - - -Parametrisierte Queries -======================= - -Die grundlegende Abwehr gegen SQL-Injection sind parametrisierte Queries. Nette Database bietet mehrere Wege, sie zu verwenden. - -Am einfachsten sind **Fragezeichen-Platzhalter**: - -```php -// ✅ Sichere parametrisierte Query -$database->query('SELECT * FROM users WHERE name = ?', $name); - -// ✅ Sichere Bedingung im Explorer -$table->where('name = ?', $name); -``` - -Das gilt für alle weiteren Methoden im [Database Explorer |explorer], die das Einsetzen von Ausdrücken mit Fragezeichen-Platzhaltern und Parametern erlauben. - -Für INSERT- und UPDATE-Befehle oder die WHERE-Klausel können wir die Werte in einem Array übergeben: - -```php -// ✅ Sicheres INSERT -$database->query('INSERT INTO users', [ - 'name' => $name, - 'email' => $email, -]); - -// ✅ Sicheres INSERT im Explorer -$table->insert([ - 'name' => $name, - 'email' => $email, -]); -``` - - -Validierung von Parameterwerten -=============================== - -Parametrisierte Queries sind der Grundstein sicherer Datenbankarbeit. Die Werte, die wir in sie einsetzen, müssen jedoch mehrere Prüfebenen durchlaufen: - - -Typkontrolle ------------- - -**Das Wichtigste ist, den korrekten Datentyp der Parameter sicherzustellen** - das ist eine notwendige Bedingung für die sichere Verwendung von Nette Database. Die Datenbank geht davon aus, dass alle Eingabedaten den korrekten, zur jeweiligen Spalte passenden Datentyp haben. - -Wäre `$name` in den vorigen Beispielen unerwartet ein Array statt eines Strings, würde Nette Database versuchen, alle seine Elemente in die SQL-Query einzusetzen, was zu einem Fehler führt. Verwenden Sie deshalb **niemals** unvalidierte Daten aus `$_GET`, `$_POST` oder `$_COOKIE` direkt in Datenbankabfragen. - - -Formale Kontrolle ------------------ - -Auf der zweiten Ebene prüfen wir das Format der Daten - zum Beispiel, ob Strings in der Kodierung UTF-8 vorliegen und ihre Länge der Spaltendefinition entspricht oder ob numerische Werte im zulässigen Bereich des Datentyps der Spalte liegen. - -Auf dieser Ebene können wir uns teilweise auf die Datenbank selbst verlassen - viele Datenbanken weisen ungültige Daten zurück. Das Verhalten kann jedoch unterschiedlich sein; manche kürzen lange Strings stillschweigend oder beschneiden Zahlen außerhalb des Bereichs. - - -Domänenspezifische Kontrolle ----------------------------- - -Die dritte Ebene umfasst logische Prüfungen, die für Ihre Anwendung spezifisch sind. Zum Beispiel die Prüfung, ob Werte aus Auswahlfeldern den angebotenen Optionen entsprechen, ob Zahlen im erwarteten Bereich liegen (z. B. Alter 0-150 Jahre) oder ob die gegenseitigen Abhängigkeiten zwischen Werten sinnvoll sind. - - -Empfohlene Validierungsmethoden -------------------------------- - -- Verwenden Sie [Nette Forms |forms:], die automatisch für die richtige Validierung aller Eingaben sorgen. -- Verwenden Sie [Presenter |application:] und geben Sie die Datentypen der Parameter in den Methoden `action*()` und `render*()` an. -- Oder implementieren Sie eine eigene Validierungsschicht mit den Standardwerkzeugen von PHP wie `filter_var()`. - - -Sichere Arbeit mit Spalten -========================== - -Im vorigen Abschnitt haben wir gezeigt, wie man Parameterwerte richtig validiert. Bei der Verwendung von Arrays in SQL-Queries müssen wir jedoch ihren Schlüsseln dieselbe Aufmerksamkeit widmen. - -```php -// ❌ GEFÄHRLICHER CODE - die Schlüssel im Array sind nicht geprüft -$database->query('INSERT INTO users', $_POST); -``` - -Bei INSERT- und UPDATE-Befehlen ist das ein kritischer Sicherheitsmangel - ein Angreifer kann beliebige Spalten der Datenbank einfügen oder ändern. Er könnte zum Beispiel `is_admin = 1` setzen oder beliebige Daten in sensible Spalten schreiben (die sogenannte Mass Assignment Vulnerability). - -In WHERE-Bedingungen ist es noch gefährlicher, weil sie Operatoren enthalten können: - -```php -// ❌ GEFÄHRLICHER CODE - die Schlüssel im Array sind nicht geprüft -$_POST['salary >'] = 100000; -$database->query('SELECT * FROM users WHERE', $_POST); -// führt die Query WHERE (`salary` > 100000) aus -``` - -Ein Angreifer kann auf diesem Weg systematisch die Gehälter der Mitarbeiter herausfinden. Er könnte zum Beispiel mit einer Abfrage nach Gehältern über 100 000 beginnen, dann unter 50 000, und indem er den Bereich schrittweise eingrenzt, die ungefähren Gehälter aller Mitarbeiter aufdecken. Diese Art von Angriff nennt man SQL-Enumeration. - -Die Methoden `where()` und `whereOr()` sind sogar [noch viel flexibler |explorer#where()] und unterstützen in Schlüsseln und Werten SQL-Ausdrücke einschließlich Operatoren und Funktionen. Das gibt einem Angreifer die Möglichkeit zur SQL-Injection: - -```php -// ❌ GEFÄHRLICHER CODE - der Angreifer kann eigenes SQL einschleusen -$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1']; -$table->where($_POST); -// führt die Query WHERE (0) UNION SELECT name, salary FROM users WHERE (1) aus -``` - -Dieser Angriff beendet die ursprüngliche Bedingung mit `0)`, hängt mit `UNION` ein eigenes `SELECT` an, um sensible Daten aus der Tabelle `users` zu erhalten, und schließt die syntaktisch korrekte Query mit `WHERE (1)` ab. - - -Whitelist für Spalten ---------------------- - -Für die sichere Arbeit mit Spaltennamen brauchen wir einen Mechanismus, der sicherstellt, dass der Benutzer nur mit erlaubten Spalten arbeiten kann und keine eigenen ergänzen darf. Wir könnten versuchen, gefährliche Spaltennamen zu erkennen und zu blockieren (Blacklist), aber dieser Ansatz ist unzuverlässig - ein Angreifer kann sich immer eine neue Schreibweise für einen gefährlichen Spaltennamen ausdenken, an die wir nicht gedacht haben. - -Deshalb ist es viel sicherer, die Logik umzudrehen und eine explizite Liste erlaubter Spalten zu definieren (Whitelist): - -```php -// Spalten, die der Benutzer ändern darf -$allowedColumns = ['name', 'email', 'active']; - -// Alle unerlaubten Spalten aus der Eingabe entfernen -$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); - -// ✅ Jetzt sicher in Queries verwendbar, etwa: -$database->query('INSERT INTO users', $filteredData); -$table->update($filteredData); -$table->where($filteredData); -``` - - -Dynamische Bezeichner -===================== - -Für dynamische Tabellen- und Spaltennamen verwenden Sie den Platzhalter `?name`. Er sorgt für das korrekte Escaping der Bezeichner nach der Syntax der jeweiligen Datenbank (in MySQL etwa mit Backticks): - -```php -// ✅ Sichere Verwendung vertrauenswürdiger Bezeichner -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name', $column, $table); -// Ergebnis in MySQL: SELECT `name` FROM `users` -``` - -Wichtig: Verwenden Sie das Symbol `?name` nur für vertrauenswürdige Werte, die im Code der Anwendung definiert sind. Für Werte vom Benutzer verwenden Sie wieder eine [Whitelist |#Whitelist für Spalten]. Sonst setzen Sie sich Sicherheitsrisiken aus: - -```php -// ❌ GEFÄHRLICH - verwenden Sie niemals Benutzereingaben -$database->query('SELECT ?name FROM users', $_GET['column']); -``` diff --git a/database/de/sql-way.texy b/database/de/sql-way.texy deleted file mode 100644 index 7caf3c74eb..0000000000 --- a/database/de/sql-way.texy +++ /dev/null @@ -1,513 +0,0 @@ -SQL-Weg -******* - -.[perex] -Nette Database bietet zwei Wege: Sie können SQL-Queries selbst schreiben (SQL-Weg) oder sie automatisch generieren lassen (siehe [Explorer |explorer]). Der SQL-Weg gibt Ihnen volle Kontrolle über die Queries und sorgt zugleich dafür, dass sie sicher zusammengesetzt werden. - -.[note] -Details zur Datenbankverbindung und -konfiguration finden Sie im Kapitel [Verbindung und Konfiguration |guide#Verbindung und Konfiguration]. - - -Grundlegende Abfragen -===================== - -Für Datenbankabfragen wird die Methode `query()` verwendet. Sie gibt ein Objekt [ResultSet |api:Nette\Database\ResultSet] zurück, das das Ergebnis der Query repräsentiert. Schlägt die Query fehl, [wirft die Methode eine Exception |exceptions]. Das Ergebnis der Query können Sie mit einer `foreach`-Schleife durchlaufen oder eine der [Hilfsmethoden |#Daten abrufen] verwenden. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; -} -``` - -Um Werte sicher in SQL-Queries einzusetzen, verwenden Sie parametrisierte Queries. Nette Database macht das denkbar einfach: Fügen Sie hinter der SQL-Query einfach ein Komma und den Wert an: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Bei mehreren Parametern haben Sie zwei Möglichkeiten: Entweder verschränken Sie die SQL-Query mit den Parametern: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age); -``` - -Oder Sie schreiben zuerst die gesamte SQL-Query und hängen dann alle Parameter an: - -```php -$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); -``` - - -Schutz vor SQL-Injection -======================== - -Warum ist es wichtig, parametrisierte Queries zu verwenden? Weil sie Sie vor einem Angriff namens SQL-Injection schützen, bei dem ein Angreifer eigene SQL-Befehle einschleusen und dadurch Zugriff auf Daten in der Datenbank erlangen oder diese beschädigen könnte. - -.[warning] -**Setzen Sie Variablen niemals direkt in eine SQL-Query ein!** Verwenden Sie immer parametrisierte Queries, die Sie vor SQL-Injection schützen. - -```php -// ❌ GEFÄHRLICHER CODE - anfällig für SQL-Injection -$database->query("SELECT * FROM users WHERE name = '$name'"); - -// ✅ Sichere parametrisierte Query -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Machen Sie sich mit den [möglichen Sicherheitsrisiken |security] vertraut. - - -Abfragetechniken -================ - - -WHERE-Bedingungen ------------------ - -`WHERE`-Bedingungen können Sie als assoziatives Array schreiben, dessen Schlüssel Spaltennamen und dessen Werte die Vergleichsdaten sind. Nette Database wählt anhand des Wertetyps automatisch den passendsten SQL-Operator. - -```php -$database->query('SELECT * FROM users WHERE', [ - 'name' => 'John', - 'active' => true, -]); -// WHERE `name` = 'John' AND `active` = 1 -``` - -Sie können den Vergleichsoperator auch explizit im Schlüssel angeben: - -```php -$database->query('SELECT * FROM users WHERE', [ - 'age >' => 25, // verwendet den Operator > - 'name LIKE' => '%John%', // verwendet den Operator LIKE - 'email NOT LIKE' => '%example.com%', // verwendet den Operator NOT LIKE -]); -// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' -``` - -Nette behandelt Sonderfälle wie `null`-Werte oder Arrays automatisch. - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name' => 'Laptop', // verwendet den Operator = - 'category_id' => [1, 2, 3], // verwendet IN - 'description' => null, // verwendet IS NULL -]); -// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL -``` - -Für negative Bedingungen verwenden Sie den Operator `NOT`: - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // verwendet den Operator != - 'category_id NOT' => [1, 2, 3], // verwendet NOT IN - 'description NOT' => null, // verwendet IS NOT NULL - 'id NOT' => [], // übersprungen -]); -// WHERE `name` != 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL -``` - -Standardmäßig werden die Bedingungen mit dem Operator `AND` verknüpft. Das lässt sich mit dem [Platzhalter ?or |#Hinweise zur SQL-Konstruktion] ändern. - - -ORDER BY-Regeln ---------------- - -Die `ORDER BY`-Klausel lässt sich mit einem Array schreiben. Geben Sie die Spalten in den Schlüsseln an und verwenden Sie einen booleschen Wert für aufsteigende (`true`) oder absteigende (`false`) Sortierung: - -```php -$database->query('SELECT id FROM author ORDER BY', [ - 'id' => true, // aufsteigend - 'name' => false, // absteigend -]); -// SELECT id FROM author ORDER BY `id`, `name` DESC -``` - - -Einfügen von Daten (INSERT) ---------------------------- - -Zum Einfügen von Datensätzen wird der SQL-Befehl `INSERT` verwendet. - -```php -$values = [ - 'name' => 'John Doe', - 'email' => 'john@example.com', -]; -$database->query('INSERT INTO users ?', $values); -$userId = $database->getInsertId(); -``` - -Die Methode `getInsertId()` gibt die ID der zuletzt eingefügten Zeile zurück. Bei manchen Datenbanken (z. B. PostgreSQL) muss als Parameter der Name der Sequenz angegeben werden, aus der die ID erzeugt werden soll: `$database->getInsertId($sequenceId)`. - -Als Parameter können Sie auch [#Spezielle Werte] wie Dateien, DateTime-Objekte oder Enum-Typen übergeben. - -Einfügen mehrerer Datensätze auf einmal: - -```php -$database->query('INSERT INTO users ?', [ - ['name' => 'User 1', 'email' => 'user1@mail.com'], - ['name' => 'User 2', 'email' => 'user2@mail.com'], -]); -``` - -Ein mehrfaches INSERT ist deutlich schneller, weil nur eine einzige Datenbankabfrage ausgeführt wird statt vieler einzelner. - -**Sicherheitshinweis:** Verwenden Sie als `$values` niemals unvalidierte Daten. Machen Sie sich mit den [möglichen Risiken |security#Sichere Arbeit mit Spalten] vertraut. - - -Aktualisieren von Daten (UPDATE) --------------------------------- - -Zum Aktualisieren von Datensätzen wird der SQL-Befehl `UPDATE` verwendet. - -```php -// Aktualisierung eines einzelnen Datensatzes -$values = [ - 'name' => 'John Smith', -]; -$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); -``` - -Die Anzahl der betroffenen Zeilen gibt `$result->getRowCount()` zurück. - -Bei `UPDATE` können wir die Operatoren `+=` und `-=` verwenden: - -```php -$database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // erhöht login_count -], 1); -``` - -Beispiel für das Einfügen oder Aktualisieren eines Datensatzes, wenn er bereits existiert. Wir verwenden die Technik `ON DUPLICATE KEY UPDATE`: - -```php -$values = [ - 'name' => $name, - 'year' => $year, -]; -$database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', - $values + ['id' => $id], - $values, -); -// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978) -// ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 -``` - -Beachten Sie, dass Nette Database den Kontext erkennt, in dem ein Array-Parameter im SQL-Befehl verwendet wird, und den SQL-Code entsprechend zusammensetzt. Aus dem ersten Array hat es also `(id, name, year) VALUES (123, 'Jim', 1978)` gebildet, während es das zweite in die Form `name = 'Jim', year = 1978` gebracht hat. Ausführlicher behandeln wir das im Abschnitt [#Hinweise zur SQL-Konstruktion]. - - -Löschen von Daten (DELETE) --------------------------- - -Zum Löschen von Datensätzen wird der SQL-Befehl `DELETE` verwendet. Beispiel für das Ermitteln der Anzahl gelöschter Zeilen: - -```php -$count = $database->query('DELETE FROM users WHERE id = ?', 1) - ->getRowCount(); -``` - - -Hinweise zur SQL-Konstruktion ------------------------------ - -Ein Hinweis ist ein spezieller Platzhalter in einer SQL-Query, der angibt, wie der Wert des Parameters in einen SQL-Ausdruck umgewandelt werden soll: - -| Hinweis | Beschreibung | Automatisch verwendet bei -|-----------|-------------------------------------------------|----------------------------- -| `?name` | Dient zum Einsetzen von Tabellen- oder Spaltennamen | - -| `?values` | Erzeugt `(key, ...) VALUES (value, ...)` | `INSERT ... ?`, `REPLACE ... ?` -| `?set` | Erzeugt Zuweisungen `key = value, ...` | `SET ?`, `KEY UPDATE ?` -| `?and` | Verknüpft Bedingungen in einem Array mit `AND` | `WHERE ?`, `HAVING ?` -| `?or` | Verknüpft Bedingungen in einem Array mit `OR` | - -| `?order` | Erzeugt die `ORDER BY`-Klausel | `ORDER BY ?`, `GROUP BY ?` - -Der Platzhalter `?name` dient dazu, Tabellen- und Spaltennamen dynamisch in die Query einzusetzen. Nette Database kümmert sich um das korrekte Quoting der Bezeichner gemäß den Konventionen der Datenbank (in MySQL etwa das Einschließen in Backticks). - -```php -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); -// SELECT `name` FROM `users` WHERE id = 1 (in MySQL) -``` - -**Achtung:** Verwenden Sie den Platzhalter `?name` nur für validierte Tabellen- und Spaltennamen. Sonst riskieren Sie [Sicherheitslücken |security#Dynamische Bezeichner]. - -Die übrigen Hinweise müssen normalerweise nicht angegeben werden, denn Nette verwendet beim Zusammensetzen der SQL-Query eine intelligente Autodetektion (siehe die dritte Spalte der Tabelle). Sie können sie aber zum Beispiel dann einsetzen, wenn Sie Bedingungen mit `OR` statt mit `AND` verknüpfen wollen: - -```php -$database->query('SELECT * FROM users WHERE ?or', [ - 'name' => 'John', - 'email' => 'john@example.com', -]); -// SELECT * FROM users WHERE `name` = 'John' OR `email` = 'john@example.com' -``` - - -Spezielle Werte ---------------- - -Außer den üblichen skalaren Typen (string, int, bool) können Sie als Parameter auch spezielle Werte übergeben: - -- Dateien: `fopen('image.gif', 'r')` fügt den binären Inhalt der Datei ein -- Datum und Zeit: Objekte vom Typ `DateTimeInterface` werden in das Datenbankformat umgewandelt -- Enum-Typen: Instanzen von `enum` werden in ihren Wert umgewandelt -- SQL-Literale: mit `Connection::literal('NOW()')` erzeugt, werden direkt in die Query eingefügt - -```php -$database->query('INSERT INTO articles ?', [ - 'title' => 'My Article', - 'published_at' => new DateTimeImmutable, // oder new DateTime - 'content' => fopen('image.png', 'r'), - 'state' => Status::Draft, -]); -``` - -Bei Datenbanken, die den Datentyp `datetime` nicht nativ unterstützen (wie SQLite und Oracle), werden `DateTime`- und `DateTimeImmutable`-Objekte in einen Wert umgewandelt, der in der [Datenbankkonfiguration |configuration] durch den Eintrag `formatDateTime` festgelegt ist (Standardwert ist `U` - Unix-Timestamp). - - -SQL-Literale ------------- - -In manchen Fällen müssen Sie rohen SQL-Code als Wert übergeben, der nicht als String behandelt und escapt werden soll. Dazu dienen Objekte der Klasse `Nette\Database\SqlLiteral`. Sie werden mit der Methode `Connection::literal()` erzeugt. - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - 'year >' => $database::literal('YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) -``` - -Alternativ: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) -``` - -SQL-Literale können Parameter enthalten: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > ? AND year < ?', $min, $max), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) -``` - -Das erlaubt interessante Kombinationen: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('?or', [ - 'active' => true, - 'role' => $role, - ]), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (`active` = 1 OR `role` = 'admin') -``` - - -Daten abrufen -============= - - -Abkürzungen für SELECT-Abfragen -------------------------------- - -Um das Laden von Daten zu vereinfachen, bietet `Connection` mehrere Abkürzungen an, die einen Aufruf von `query()` mit einem anschließenden `fetch*()` kombinieren. Diese Methoden nehmen dieselben Parameter entgegen wie `query()`, also eine SQL-Query und optionale Parameter. Eine vollständige Beschreibung der `fetch*()`-Methoden finden Sie [weiter unten |#fetch()]. - -| `fetch($sql, ...$params): ?Row` | Führt die Query aus und gibt die erste Zeile als `Row`-Objekt oder `null` zurück. -| `fetchAll($sql, ...$params): array` | Führt die Query aus und gibt alle Zeilen als Array von `Row`-Objekten zurück. -| `fetchPairs($sql, ...$params): array` | Führt die Query aus und gibt ein assoziatives Array zurück (Schlüssel-Wert-Paare). -| `fetchField($sql, ...$params): mixed` | Führt die Query aus und gibt den Wert der ersten Spalte der ersten Zeile zurück. -| `fetchList($sql, ...$params): ?array` | Führt die Query aus und gibt die erste Zeile als indiziertes Array oder `null` zurück. - -Beispiel: - -```php -// fetchField() - gibt den Wert der ersten Zelle zurück -$count = $database->query('SELECT COUNT(*) FROM articles') - ->fetchField(); -``` - - -`foreach` - Iteration über Zeilen ---------------------------------- - -Nach dem Ausführen einer Query wird ein Objekt [ResultSet |api:Nette\Database\ResultSet] zurückgegeben, das mehrere Wege bietet, die Ergebnisse zu durchlaufen. Der einfachste Weg, eine Query auszuführen und die Zeilen zu erhalten, ist die Iteration in einer `foreach`-Schleife. Diese Methode ist am speicherschonendsten, denn sie holt die Daten Zeile für Zeile und lädt nicht das gesamte Ergebnis auf einmal in den Speicher. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; - // ... -} -``` - -.[note] -Das `ResultSet` lässt sich nur einmal durchlaufen. Wenn Sie es mehrfach durchlaufen müssen, müssen Sie die Daten zuerst in ein Array laden, zum Beispiel mit der Methode `fetchAll()`. - - -fetch(): ?Row .[method] ------------------------ - -Gibt eine Zeile als `Row`-Objekt zurück. Wenn es keine weiteren Zeilen gibt, wird `null` zurückgegeben. Der interne Zeiger rückt auf die nächste Zeile vor. - -```php -$result = $database->query('SELECT * FROM users'); -$row = $result->fetch(); // lädt die erste Zeile -if ($row) { - echo $row->name; -} -``` - - -fetchAll(): array .[method] ---------------------------- - -Gibt alle verbleibenden Zeilen des `ResultSet` als Array von `Row`-Objekten zurück. - -```php -$result = $database->query('SELECT * FROM users'); -$rows = $result->fetchAll(); // lädt alle Zeilen -foreach ($rows as $row) { - echo $row->name; -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Gibt das Ergebnis als assoziatives Array zurück. Das erste Argument bestimmt die Spalte, die als Schlüssel verwendet wird, das zweite Argument die Spalte, die als Wert verwendet wird: - -```php -$result = $database->query('SELECT id, name FROM users'); -$names = $result->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Wenn nur der erste Parameter (`$key`) angegeben wird, ist der Wert die gesamte Zeile (das `Row`-Objekt): - -```php -$rows = $result->fetchPairs('id'); -// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...] -``` - -Bei doppelten Schlüsseln wird der Wert aus der letzten Zeile verwendet. Wird `null` als Schlüssel verwendet, entsteht ein numerisch (ab null) indiziertes Array, sodass keine Schlüsselkollisionen auftreten: - -```php -$names = $result->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Alternativ können Sie einen Callback angeben, der jede Zeile verarbeitet. Der Callback kann einen einzelnen Wert oder ein Schlüssel-Wert-Paar zurückgeben. - -```php -$result = $database->query('SELECT * FROM users'); -$items = $result->fetchPairs(fn($row) => "$row->id - $row->name"); -// ['1 - John', '2 - Jane', ...] - -// Der Callback kann auch ein Array mit einem Schlüssel-Wert-Paar zurückgeben: -$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); -// ['John' => 46, 'Jane' => 21, ...] -``` - - -fetchField(): mixed .[method] ------------------------------ - -Gibt den Wert der ersten Spalte der aktuellen Zeile zurück. Wenn es keine weiteren Zeilen gibt, wird `null` zurückgegeben. Der interne Zeiger rückt auf die nächste Zeile vor. - -```php -$result = $database->query('SELECT name FROM users'); -$name = $result->fetchField(); // lädt den Namen aus der ersten Zeile -``` - - -fetchList(): ?array .[method] ------------------------------ - -Gibt die Zeile als indiziertes Array zurück. Wenn es keine weiteren Zeilen gibt, wird `null` zurückgegeben. Der interne Zeiger rückt auf die nächste Zeile vor. - -```php -$result = $database->query('SELECT name, email FROM users'); -$row = $result->fetchList(); // ['John', 'john@example.com'] -``` - - -getRowCount(): ?int .[method] ------------------------------ - -Gibt die Anzahl der betroffenen Zeilen der letzten `UPDATE`- oder `DELETE`-Query zurück. Bei `SELECT`-Queries gibt sie die Anzahl der Zeilen im Ergebnis zurück. Diese muss allerdings nicht immer bekannt sein; in diesem Fall gibt die Methode `null` zurück. - - -getColumnCount(): ?int .[method] --------------------------------- - -Gibt die Anzahl der Spalten im `ResultSet` zurück. - - -Informationen zu Abfragen -========================= - -Zu Debugging-Zwecken können wir Informationen über die zuletzt ausgeführte Query erhalten: - -```php -echo $database->getLastQueryString(); // gibt die SQL-Query aus - -$result = $database->query('SELECT * FROM articles'); -echo $result->getQueryString(); // gibt die SQL-Query aus -echo $result->getTime(); // gibt die Ausführungszeit in Sekunden aus -``` - -Um das Ergebnis als HTML-Tabelle anzuzeigen, können Sie verwenden: - -```php -$result = $database->query('SELECT * FROM articles'); -$result->dump(); -``` - -`ResultSet` bietet Informationen über die Spaltentypen: - -```php -$result = $database->query('SELECT * FROM articles'); -$types = $result->getColumnTypes(); - -foreach ($types as $column => $type) { - echo "$column ist vom Typ $type"; // z. B. 'id ist vom Typ int' -} -``` - - -Abfrage-Protokollierung ------------------------ - -Wir können eine eigene Protokollierung der Queries implementieren. Das Event `onQuery` ist ein Array von Callbacks, die nach jeder ausgeführten Query aufgerufen werden: - -```php -$database->onQuery[] = function ($database, $result) use ($logger) { - $logger->info('Query: ' . $result->getQueryString()); - $logger->info('Time: ' . $result->getTime()); - - if ($result->getRowCount() > 1000) { - $logger->warning('Large result set: ' . $result->getRowCount() . ' rows'); - } -}; -``` diff --git a/database/de/transactions.texy b/database/de/transactions.texy deleted file mode 100644 index dcf52d2e21..0000000000 --- a/database/de/transactions.texy +++ /dev/null @@ -1,45 +0,0 @@ -Transaktionen -************* - -.[perex] -Transaktionen garantieren, dass entweder alle Operationen innerhalb der Transaktion ausgeführt werden oder keine. Sie sind nützlich, um bei komplexen Operationen die Konsistenz der Daten sicherzustellen. - -Am einfachsten sieht die Verwendung von Transaktionen so aus: - -```php -$database->beginTransaction(); -try { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); - $database->commit(); -} catch (\Exception $e) { - $database->rollBack(); - throw $e; -} -``` - -Dasselbe Ergebnis erreichen Sie viel eleganter mit der Methode `transaction()`. Sie nimmt einen Callback entgegen, der innerhalb der Transaktion ausgeführt wird. Läuft der Callback ohne Exception durch, wird die Transaktion automatisch committet. Tritt eine Exception auf, wird die Transaktion zurückgerollt und die Exception weiter nach oben gereicht. - -```php -$database->transaction(function ($database) use ($id) { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); -}); -``` - -Aufrufe von `transaction()` lassen sich verschachteln, wodurch sich Methoden, die jeweils ihre eigene Transaktion verwalten, leicht zusammensetzen lassen. Nur die äußere Transaktion wird tatsächlich als `BEGIN`/`COMMIT` an die Datenbank geschickt; die inneren Aufrufe verfolgen lediglich die Verschachtelungstiefe. Ein manueller Aufruf von `beginTransaction()`, `commit()` oder `rollBack()` innerhalb eines `transaction()`-Callbacks wirft eine `LogicException`. - -Die Methode `transaction()` kann auch Werte zurückgeben: - -```php -$count = $database->transaction(function ($database) { - $result = $database->query('UPDATE users SET active = ?', true); - return $result->getRowCount(); // gibt die Anzahl der aktualisierten Zeilen zurück -}); -``` diff --git a/database/de/type-conversion.texy b/database/de/type-conversion.texy deleted file mode 100644 index b32109dae0..0000000000 --- a/database/de/type-conversion.texy +++ /dev/null @@ -1,55 +0,0 @@ -Typkonvertierung -**************** - -.[perex] -Nette Database wandelt die aus der Datenbank zurückgegebenen Werte automatisch in die entsprechenden PHP-Typen um. - - -Datum und Uhrzeit ------------------ - -Zeitwerte werden in Objekte vom Typ `Nette\Utils\DateTime` umgewandelt. Wenn Sie möchten, dass Zeitwerte in unveränderliche Objekte vom Typ `Nette\Database\DateTime` umgewandelt werden, setzen Sie in der [Konfiguration |configuration] die Option `newDateTime` auf true. - -```php -$row = $database->fetch('SELECT created_at FROM articles'); -echo $row->created_at instanceof DateTime; // true -echo $row->created_at->format('j. n. Y'); -``` - -Bei MySQL wird der Datentyp `TIME` in Objekte vom Typ `DateInterval` umgewandelt. - - -Boolesche Werte ---------------- - -Boolesche Werte werden automatisch in `true` oder `false` umgewandelt. Bei MySQL wird `TINYINT(1)` umgewandelt, wenn wir in der [Konfiguration |configuration] `convertBoolean` setzen. - -```php -$row = $database->fetch('SELECT is_published FROM articles'); -echo gettype($row->is_published); // 'boolean' -``` - - -Numerische Werte ----------------- - -Numerische Werte werden je nach Spaltentyp in der Datenbank in `int` oder `float` umgewandelt: - -```php -$row = $database->fetch('SELECT id, price FROM products'); -echo gettype($row->id); // integer -echo gettype($row->price); // float -``` - - -Eigene Normalisierung ---------------------- - -Mit der Methode `setRowNormalizer(?callable $normalizer)` können Sie eine eigene Funktion zur Umwandlung der Zeilen aus der Datenbank setzen. Das ist zum Beispiel für die automatische Konvertierung von Datentypen nützlich. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // hier findet die Typkonvertierung statt - return $row; -}); -``` diff --git a/database/de/upgrading.texy b/database/de/upgrading.texy deleted file mode 100644 index 0460eb483a..0000000000 --- a/database/de/upgrading.texy +++ /dev/null @@ -1,36 +0,0 @@ -Upgrade -******* - - -Upgrade auf Version 3.2 -======================= - -Die mindestens erforderliche PHP-Version ist 8.1. - -Der Code wurde sorgfältig auf PHP 8.1 abgestimmt. Alle neuen Typdeklarationen für Methoden und Properties wurden ergänzt. Die Änderungen sind gering: - -- MySQL: das Nulldatum `0000-00-00` wird als `null` zurückgegeben -- MySQL: ein decimal ohne Nachkommastellen wird als int statt als float zurückgegeben -- der Typ `time` wird als `DateTime`-Objekt mit dem Datum `0001-01-01` statt mit dem aktuellen Datum zurückgegeben - - -Upgrade auf Version 3.1 -======================= - -- die Klasse `Nette\Database\Context` wurde aus Gründen der Konsistenz mit dem Namen [Database Explorer |explorer] in `Nette\Database\Explorer` umbenannt -- die Interfaces `Nette\Database\IRow` und `Nette\Database\IRowContainer` sind als überflüssig und veraltet markiert -- der Treiber `MySqlDriver` verwendet Unterabfragen -- der Übersetzer der SQL-Anweisungen kontrolliert besser, wo Arrays übergeben werden dürfen - - -Upgrade auf Version 3.0 -======================= - -Einige Methoden wie `fetch()` oder `fetchField()` geben nun `null` statt `false` zurück, wenn es keine weitere Zeile gibt. - - -Upgrade auf Version 2.3 -======================= - -- der `MySqlDriver` verwendet für MySQL >= 5.5.3 standardmäßig die Kodierung `utf8mb4` statt `utf8` -- `IReflection` wurde in die Zwillingsinterfaces `IStructure` und `IConventions` aufgeteilt diff --git a/database/en/@home.texy b/database/en/@home.texy deleted file mode 100644 index ffb5a172d1..0000000000 --- a/database/en/@home.texy +++ /dev/null @@ -1,16 +0,0 @@ -Supported Databases -=================== - -These database servers are supported: - -|* Database server |* DSN name |* Core support |* Explorer support -| MySQL (>= 5.1) | mysql | YES | YES -| PostgreSQL (>= 9.0) | pgsql | YES | YES -| Sqlite 3 (>= 3.8) | sqlite | YES | YES -| Oracle | oci | YES | - -| MS SQL (PDO_SQLSRV) | sqlsrv | YES | YES -| MS SQL (PDO_DBLIB) | mssql | YES | - -| ODBC | odbc | YES | - - -{{maintitle: Nette Database: Simple and Smart Database Layer for PHP}} -{{description: Nette Database simplifies retrieving data without writing SQL queries. It executes efficient queries and does not transfer unnecessary data.}} diff --git a/database/en/@left-menu.texy b/database/en/@left-menu.texy deleted file mode 100644 index 1ccf8834ff..0000000000 --- a/database/en/@left-menu.texy +++ /dev/null @@ -1,21 +0,0 @@ -Nette Database -************** -- [Getting Started |guide] -- [SQL Way] -- [Explorer] -- [Transactions] -- [Exceptions] -- [Reflection] -- [Type Conversion |type-conversion] -- [Configuration] -- [Security Risks |security] -- [Upgrading] - - -Further Reading -*************** -- [Nette Documentation |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Best practices |best-practices:] -- [Troubleshooting |nette:troubleshooting] diff --git a/database/en/@meta.texy b/database/en/@meta.texy deleted file mode 100644 index 42471908b0..0000000000 --- a/database/en/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Documentation}} diff --git a/database/en/configuration.texy b/database/en/configuration.texy deleted file mode 100644 index f31f551525..0000000000 --- a/database/en/configuration.texy +++ /dev/null @@ -1,113 +0,0 @@ -Database Configuration -********************** - -.[perex] -Overview of configuration options for Nette Database. - -If you are not using the entire framework, but only this library, read [how to load the configuration|bootstrap:]. - - -Single Connection ------------------ - -Configure a single database connection: - -```neon -database: - # DSN, the only mandatory key - dsn: "sqlite:%appDir%/Model/demo.db" - user: ... - password: ... -``` - -This creates the `Nette\Database\Connection` and `Nette\Database\Explorer` services, which are usually passed via [autowiring |dependency-injection:autowiring] or by referring to [their name |#DI Services]. - -Other settings: - -```neon -database: - # show the database panel in Tracy Bar? - debugger: ... # (bool) defaults to on if Tracy is active - - # show query EXPLAIN in Tracy Bar? - explain: ... # (bool) defaults to true - - # enable autowiring for this connection? - autowired: ... # (bool) defaults to true for the first connection - - # table conventions: discovered, static, or class name - conventions: discovered # (string) defaults to 'discovered' - - options: - # connect to the database only when needed? - lazy: ... # (bool) defaults to false - - # PHP database driver class - driverClass: # (string) - - # MySQL only: sets sql_mode - sqlmode: # (string) - - # MySQL only: sets SET NAMES - charset: # (string) defaults to 'utf8mb4' - - # MySQL only: converts TINYINT(1) to bool - convertBoolean: # (bool) defaults to false - - # returns date columns as immutable objects (since version 3.2.1) - newDateTime: # (bool) defaults to false - - # only Oracle and SQLite: format for storing date - formatDateTime: # (string) defaults to 'U' -``` - -The `options` key can contain other options found in the [PDO driver documentation |https://www.php.net/manual/en/pdo.drivers.php], such as: - -```neon -database: - options: - PDO::MYSQL_ATTR_COMPRESS: true -``` - - -Multiple Connections --------------------- - -In the configuration, we can define multiple database connections by dividing them into named sections: - -```neon -database: - main: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password - - another: - dsn: 'sqlite::memory:' -``` - -Autowiring is enabled only for services from the first section. This can be changed using `autowired: false` or `autowired: true`. - - -DI Services ------------ - -These services are added to the DI container, where `###` represents the connection name: - -| Name | Type | Description -|---------------------------|---------------------------------|--------------------------- -| `database.###.connection` | [api:Nette\Database\Connection] | database connection -| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |explorer] - - -If we define only one connection, the service names will be `database.default.connection` and `database.default.explorer`. If we define multiple connections as in the example above, the names will correspond to the sections, i.e., `database.main.connection`, `database.main.explorer`, and also `database.another.connection` and `database.another.explorer`. - -We pass non-autowired services explicitly by referencing their name: - -```neon -services: - - UserFacade(@database.another.connection) -``` - -{{maintitle: Nette Database Configuration: Connections in NEON}} -{{description: Set up one or more database connections in NEON configuration: DSN, credentials, debugging options and the DI services Nette Database registers.}} diff --git a/database/en/exceptions.texy b/database/en/exceptions.texy deleted file mode 100644 index fbd770c917..0000000000 --- a/database/en/exceptions.texy +++ /dev/null @@ -1,40 +0,0 @@ -Exceptions -********** - -Nette Database uses an exception hierarchy. The base class is `Nette\Database\DriverException`, which extends `PDOException` and provides enhanced functionality for working with database errors: - -- The `getDriverCode()` method returns the error code from the database driver. -- The `getSqlState()` method returns the SQLSTATE code. -- The `getQueryString()` and `getParameters()` methods allow retrieving the original query and its parameters. - -The `DriverException` class is extended by the following specialized exceptions: - -- `ConnectionException` - indicates a failure to connect to the database server. - - `ConnectionLostException` .{data-version:3.2.9} - the connection was dropped during an operation (server restart, network failure, idle timeout); a reconnect is required before further use. -- `ConstraintViolationException` - the base class for database constraint violations, from which the following exceptions inherit: - - `ForeignKeyConstraintViolationException` - violation of a foreign key constraint. - - `NotNullConstraintViolationException` - violation of a NOT NULL constraint. - - `UniqueConstraintViolationException` - violation of a uniqueness constraint. - - `CheckConstraintViolationException` .{data-version:3.2.9} - violation of a CHECK constraint. -- `DeadlockException` .{data-version:3.2.9} - a deadlock or serialization failure detected by the server; the transaction was rolled back and may be retried. -- `LockTimeoutException` .{data-version:3.2.9} - a lock-wait timeout was exceeded; the statement was aborted, but the surrounding transaction typically remains open. - -The following example demonstrates how to catch a `UniqueConstraintViolationException`, which occurs when trying to insert a user with an email that already exists in the database (assuming the `email` column has a unique index): - -```php -try { - $database->query('INSERT INTO users', [ - 'email' => 'john@example.com', - 'name' => 'John Doe', - 'password' => $hashedPassword, - ]); -} catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'A user with this email already exists.'; - -} catch (Nette\Database\DriverException $e) { - echo 'An error occurred during registration: ' . $e->getMessage(); -} -``` - -{{maintitle: Nette Database Exceptions: Error Hierarchy and Handling}} -{{description: The exception hierarchy of Nette Database: how to tell a connection failure, constraint violation or deadlock, and read the driver error code and SQLSTATE.}} diff --git a/database/en/explorer.texy b/database/en/explorer.texy deleted file mode 100644 index c0c931257b..0000000000 --- a/database/en/explorer.texy +++ /dev/null @@ -1,915 +0,0 @@ -Database Explorer -***************** - -<div class=perex> - -Explorer offers an intuitive and efficient way to work with your database. It automatically handles table relationships and optimizes queries, allowing you to focus on your application logic. It works immediately without configuration. If you need full control over SQL queries, you can use the [SQL way |SQL way]. - -- Working with data is natural and easy to understand -- Generates optimized SQL queries that fetch only the necessary data -- Provides easy access to related data without the need to write JOIN queries -- Works immediately without any configuration or entity generation - -</div> - - -Working with the Explorer begins by calling the `table()` method on the [api:Nette\Database\Explorer] object (see [Connection and Configuration |guide#Connection and Configuration] for details on setting up the database connection): - -```php -$books = $explorer->table('book'); // 'book' is the table name -``` - -The method returns a [Selection |api:Nette\Database\Table\Selection] object, which represents an SQL query. Additional methods can be chained to this object for filtering and sorting results. The query is assembled and executed only when the data is requested, for example, by iterating with `foreach`. Each row is represented by an [ActiveRow |api:Nette\Database\Table\ActiveRow] object: - -```php -foreach ($books as $book) { - echo $book->title; // outputs the 'title' column - echo $book->author_id; // outputs the 'author_id' column -} -``` - -Explorer greatly simplifies working with [table relationships |#Relationships Between Tables]. The following example shows how easily we can output data from related tables (books and their authors). Notice that no JOIN queries need to be written; Nette generates them for us: - -```php -$books = $explorer->table('book'); - -foreach ($books as $book) { - echo 'Book: ' . $book->title; - echo 'Author: ' . $book->author->name; // creates a JOIN to the 'author' table -} -``` - -Nette Database Explorer optimizes queries for maximum efficiency. The above example performs only two SELECT queries, regardless of whether we process 10 or 10,000 books. - -Additionally, Explorer tracks which columns are used in the code and fetches only those from the database, saving further performance. This behavior is fully automatic and adaptive. If you later modify the code to use additional columns, Explorer automatically adjusts the queries. You don't need to configure anything or think about which columns will be needed - leave that to Nette. - - -Filtering and Sorting -===================== - -The `Selection` class provides methods for filtering and sorting data selections. - -.[language-php] -| `where($condition, ...$params)` | Adds a WHERE condition. Multiple conditions are combined using AND | -| `whereOr(array $conditions)` | Adds a group of WHERE conditions combined using OR | -| `wherePrimary($value)` | Adds a WHERE condition based on the primary key | -| `order($columns, ...$params)` | Sets sorting with ORDER BY | -| `select($columns, ...$params)` | Specifies which columns to fetch | -| `limit($limit, $offset = null)` | Limits the number of rows (LIMIT) and optionally sets OFFSET | -| `page($page, $itemsPerPage, &$numOfPages = null)` | Sets pagination | -| `group($columns, ...$params)` | Groups rows (GROUP BY) | -| `having($condition, ...$params)`| Adds a HAVING condition for filtering grouped rows | - -Methods can be chained (the so-called [fluent interface |nette:introduction-to-object-oriented-programming#Fluent Interfaces]): `$table->where(...)->order(...)->limit(...)`. - -In these methods, you can also use special notations for accessing [data from related tables |#Querying Through Related Tables]. - - -Escaping and Identifiers ------------------------- - -The methods automatically escape parameters and quote identifiers (table and column names), preventing SQL injection. To ensure proper operation, a few rules must be followed: - -- Write keywords, function names, procedures, etc., in **uppercase**. -- Write column and table names in **lowercase**. -- Always pass strings using **parameters**. - -```php -where('name = ' . $name); // CRITICAL VULNERABILITY: SQL injection -where('name LIKE "%search%"'); // WRONG: complicates automatic quoting -where('name LIKE ?', '%search%'); // CORRECT: value passed as a parameter - -where('name like ?', $name); // WRONG: generates: `name` `like` ? -where('name LIKE ?', $name); // CORRECT: generates: `name` LIKE ? -where('LOWER(name) = ?', $value);// CORRECT: LOWER(`name`) = ? -``` - - -where(string|array $condition, ...$parameters): static .[method] ----------------------------------------------------------------- - -Filters results using WHERE conditions. Its strength lies in intelligently handling various value types and automatically selecting appropriate SQL operators. - -Basic usage: - -```php -$table->where('id', $value); // WHERE `id` = 123 -$table->where('id > ?', $value); // WHERE `id` > 123 -$table->where('id = ? OR name = ?', $id, $name); // WHERE `id` = 1 OR `name` = 'Jon Snow' -``` - -Thanks to automatic detection of suitable operators, you don't need to handle various special cases - Nette resolves them for you: - -```php -$table->where('id', 1); // WHERE `id` = 1 -$table->where('id', null); // WHERE `id` IS NULL -$table->where('id', [1, 2, 3]); // WHERE `id` IN (1, 2, 3) -// You can also use the placeholder ? without an operator: -$table->where('id ?', 1); // WHERE `id` = 1 -``` - -The method correctly handles negative conditions and empty arrays: - -```php -$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- finds nothing -$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- finds everything -$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- finds everything -// $table->where('NOT id ?', $ids); // WARNING: This syntax is not supported -``` - -You can also pass the result of another table query as a parameter, creating a subquery: - -```php -// WHERE `id` IN (SELECT `id` FROM `tableName`) -$table->where('id', $explorer->table($tableName)); - -// WHERE `id` IN (SELECT `col` FROM `tableName`) -$table->where('id', $explorer->table($tableName)->select('col')); -``` - -Conditions can also be passed as an array, whose items are combined using AND: - -```php -// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) -$table->where([ - 'price_final < price_original', - 'stock_count > min_stock', -]); -``` - -In the array, you can use key => value pairs, and Nette will again automatically choose the correct operators: - -```php -// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3)) -$table->where([ - 'status' => 'active', - 'id' => [1, 2, 3], -]); -``` - -In the array, you can combine SQL expressions with placeholders and multiple parameters. This is suitable for complex conditions with precisely defined operators: - -```php -// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) -$table->where([ - 'age > ?' => 18, - 'ROUND(score, ?) > ?' => [2, 75.5], // two parameters are passed as an array -]); -``` - -Multiple calls to `where()` automatically combine conditions using AND. - - -whereOr(array $parameters): static .[method] --------------------------------------------- - -Similar to `where()`, this adds conditions, but combines them using OR: - -```php -// WHERE (`status` = 'active') OR (`deleted` = 1) -$table->whereOr([ - 'status' => 'active', - 'deleted' => true, -]); -``` - -More complex expressions can also be used here: - -```php -// WHERE (`price` > 1000) OR (`price_with_tax` > 1500) -$table->whereOr([ - 'price > ?' => 1000, - 'price_with_tax > ?' => 1500, -]); -``` - - -wherePrimary(mixed $key): static .[method] ------------------------------------------- - -Adds a condition for the table's primary key: - -```php -// WHERE `id` = 123 -$table->wherePrimary(123); - -// WHERE `id` IN (1, 2, 3) -$table->wherePrimary([1, 2, 3]); -``` - -If the table has a composite primary key (e.g., `foo_id`, `bar_id`), pass it as an array: - -```php -// WHERE `foo_id` = 1 AND `bar_id` = 5 -$table->wherePrimary(['foo_id' => 1, 'bar_id' => 5])->fetch(); - -// WHERE (`foo_id`, `bar_id`) IN ((1, 5), (2, 3)) -$table->wherePrimary([ - ['foo_id' => 1, 'bar_id' => 5], - ['foo_id' => 2, 'bar_id' => 3], -])->fetchAll(); -``` - - -order(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Specifies the order in which rows are returned. You can sort by one or more columns, in ascending or descending order, or according to a custom expression: - -```php -$table->order('created'); // ORDER BY `created` -$table->order('created DESC'); // ORDER BY `created` DESC -$table->order('priority DESC, created'); // ORDER BY `priority` DESC, `created` -$table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC -``` - - -select(string $columns, ...$parameters): static .[method] ---------------------------------------------------------- - -Specifies the columns to be returned from the database. By default, Nette Database Explorer returns only the columns that are actually used in the code. Use the `select()` method when you need to retrieve specific expressions: - -```php -// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` -$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); -``` - -Aliases defined using `AS` are then accessible as properties of the `ActiveRow` object: - -```php -foreach ($table as $row) { - echo $row->formatted_date; // access the alias -} -``` - - -limit(?int $limit, ?int $offset = null): static .[method] ---------------------------------------------------------- - -Limits the number of returned rows (LIMIT) and optionally allows setting an offset: - -```php -$table->limit(10); // LIMIT 10 (returns the first 10 rows) -$table->limit(10, 20); // LIMIT 10 OFFSET 20 -``` - -For pagination, it is more appropriate to use the `page()` method. - - -page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] -------------------------------------------------------------------------- - -Facilitates pagination of results. It accepts the page number (starting from 1) and the number of items per page. Optionally, you can pass a reference to a variable where the total number of pages will be stored: - -```php -$numOfPages = null; -$table->page(page: 3, itemsPerPage: 10, numOfPages: $numOfPages); -echo "Total pages: $numOfPages"; -``` - - -group(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Groups rows according to the specified columns (GROUP BY). It is typically used in conjunction with aggregate functions: - -```php -// Counts the number of products in each category -$table->select('category_id, COUNT(*) AS count') - ->group('category_id'); -``` - - -having(string $having, ...$parameters): static .[method] --------------------------------------------------------- - -Sets a condition for filtering grouped rows (HAVING). It can be used in conjunction with the `group()` method and aggregate functions: - -```php -// Finds categories that have more than 100 products -$table->select('category_id, COUNT(*) AS count') - ->group('category_id') - ->having('count > ?', 100); -``` - - -Reading Data -============ - -For reading data from the database, several useful methods are available: - -.[language-php] -| `foreach ($table as $key => $row)` | Iterates through all rows, `$key` is the primary key value, `$row` is an ActiveRow object | -| `$row = $table->get($key)` | Returns a single row by primary key | -| `$row = $table->fetch()` | Returns the current row and advances the pointer to the next one | -| `$array = $table->fetchPairs()` | Creates an associative array from the results | -| `$array = $table->fetchAll()` | Returns all rows as an array | -| `count($table)` | Returns the number of rows in the Selection object | - -The [ActiveRow |api:Nette\Database\Table\ActiveRow] object is read-only. This means you cannot change the values of its properties. This restriction ensures data consistency and prevents unexpected side effects. Data is loaded from the database, and any changes should be made explicitly and in a controlled manner. - - -`foreach` - Iterating Through All Rows --------------------------------------- - -The easiest way to execute a query and retrieve rows is by iterating using a `foreach` loop. It automatically executes the SQL query. - -```php -$books = $explorer->table('book'); -foreach ($books as $key => $book) { - // $key is the primary key value, $book is ActiveRow - echo "$book->title ({$book->author->name})"; -} -``` - - -get($key): ?ActiveRow .[method] -------------------------------- - -Executes the SQL query and returns the row by primary key, or `null` if it doesn't exist. - -```php -$book = $explorer->table('book')->get(123); // returns ActiveRow with ID 123 or null -if ($book) { - echo $book->title; -} -``` - - -fetch(): ?ActiveRow .[method] ------------------------------ - -Returns the current row and advances the internal pointer to the next one. If there are no more rows, it returns `null`. - -```php -$books = $explorer->table('book'); -while ($book = $books->fetch()) { - $this->processBook($book); -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Returns results as an associative array. The first argument specifies the column name to be used as the key in the array, the second argument specifies the column name to be used as the value: - -```php -$authors = $explorer->table('author')->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -If only the first parameter is provided, the value will be the entire row, i.e., the `ActiveRow` object: - -```php -$authors = $explorer->table('author')->fetchPairs('id'); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - -In case of duplicate keys, the value from the last row is used. When using `null` as the key, the array will be indexed numerically starting from zero (then no collisions occur): - -```php -$authors = $explorer->table('author')->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Alternatively, you can pass a callback as a parameter, which will return either a single value or a key-value pair for each row. - -```php -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); -// ['First Book (John Novak)', ...] - -// The callback can also return an array with a key & value pair: -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => [$row->title, $row->author->name]); -// ['First Book' => 'John Novak', ...] -``` - - -fetchAll(): array .[method] ---------------------------- - -Returns all rows as an associative array of `ActiveRow` objects, where the keys are the primary key values. - -```php -$allBooks = $explorer->table('book')->fetchAll(); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - - -count(): int .[method] ----------------------- - -The `count()` method without a parameter returns the number of rows in the `Selection` object: - -```php -$table->where('category', 1); -$count = $table->count(); -$count = count($table); // alternative -``` - -Note: `count()` with a parameter performs the COUNT aggregate function in the database, see below. - - -ActiveRow::toArray(): array .[method] -------------------------------------- - -Converts the `ActiveRow` object to an associative array, where keys are column names and values are the corresponding data. - -```php -$book = $explorer->table('book')->get(1); -$bookArray = $book->toArray(); -// $bookArray will be ['id' => 1, 'title' => '...', 'author_id' => ..., ...] -``` - - -Aggregation -=========== - -The `Selection` class provides methods for easily performing aggregate functions (COUNT, SUM, MIN, MAX, AVG, etc.). - -.[language-php] -| `count($expr)` | Counts the number of rows | -| `min($expr)` | Returns the minimum value in a column | -| `max($expr)` | Returns the maximum value in a column | -| `sum($expr)` | Returns the sum of values in a column | -| `aggregation($function)` | Allows any aggregation function, such as `AVG()` or `GROUP_CONCAT()` | - - -count(string $expr): int .[method] ----------------------------------- - -Executes an SQL query with the COUNT function and returns the result. The method is used to determine how many rows match a certain condition: - -```php -$count = $table->count('*'); // SELECT COUNT(*) FROM `table` -$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` -``` - -Note: [#count()] without a parameter only returns the number of rows in the `Selection` object. - - -min(string $expr) and max(string $expr) .[method] -------------------------------------------------- - -The `min()` and `max()` methods return the minimum and maximum values in the specified column or expression: - -```php -// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 -$maxPrice = $products->where('active', true) - ->max('price'); -``` - - -sum(string $expr): mixed .[method] ----------------------------------- - -Returns the sum of values in the specified column or expression: - -```php -// SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 -$totalPrice = $products->where('active', true) - ->sum('price * items_in_stock'); -``` - - -aggregation(string $function, ?string $groupFunction = null): mixed .[method] ------------------------------------------------------------------------------ - -Allows performing any aggregate function. - -```php -// average price of products in a category -$avgPrice = $products->where('category_id', 1) - ->aggregation('AVG(price)'); - -// joins product tags into a single string -$tags = $products->where('id', 1) - ->aggregation('GROUP_CONCAT(tag.name) AS tags') - ->fetch() - ->tags; -``` - -If we need to aggregate results that already result from some aggregate function and grouping (e.g., `SUM(value)` over grouped rows), we specify the aggregate function to be applied to these intermediate results as the second argument: - -```php -// Calculates the total price of products in stock for individual categories and then sums these prices together. -$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') - ->group('category_id') - ->aggregation('SUM(category_total)', 'SUM'); -``` - -In this example, we first calculate the total price of products in each category (`SUM(price * stock) AS category_total`) and group the results by `category_id`. Then, we use `aggregation('SUM(category_total)', 'SUM')` to sum these intermediate totals `category_total`. The second argument `'SUM'` specifies that the SUM function should be applied to the intermediate results. - - -Insert, Update & Delete -======================= - -Nette Database Explorer simplifies inserting, updating, and deleting data. All the methods mentioned throw a `Nette\Database\DriverException` in case of an error. - - -Selection::insert(iterable $data) .[method] -------------------------------------------- - -Inserts new records into the table. - -**Inserting a single record:** - -Pass the new record as an associative array or iterable object (like `ArrayHash` used in [forms |forms:]), where keys correspond to column names in the table. - -If the table has a defined primary key, the method returns an `ActiveRow` object, which is reloaded from the database to reflect any changes made at the database level (triggers, default column values, auto-increment column calculations). This ensures data consistency, and the object always contains the current data from the database. If the table has no primary key, there is no identifiable row and the method returns `null`. - -```php -$row = $explorer->table('users')->insert([ - 'name' => 'John Doe', - 'email' => 'john.doe@example.com', -]); -// $row is an instance of ActiveRow and contains the complete data of the inserted row, -// including the automatically generated ID and any changes made by triggers -echo $row->id; // Outputs the ID of the newly inserted user -echo $row->created_at; // Outputs the creation time if set by a trigger -``` - -**Inserting multiple records at once:** - -The `insert()` method allows inserting multiple records using a single SQL query. In this case, it returns the number of inserted rows. - -```php -$insertedRows = $explorer->table('users')->insert([ - [ - 'name' => 'John', - 'year' => 1994, - ], - [ - 'name' => 'Jack', - 'year' => 1995, - ], -]); -// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) -// $insertedRows will be 2 -``` - -A `Selection` object with a data selection can also be passed as a parameter. - -```php -$newUsers = $explorer->table('potential_users') - ->where('approved', 1) - ->select('name, email'); - -$insertedRows = $explorer->table('users')->insert($newUsers); -``` - -**Inserting special values:** - -We can also pass files, `DateTime` objects, or SQL literals as values: - -```php -$explorer->table('users')->insert([ - 'name' => 'John', - 'created_at' => new DateTime, // converts to database format - 'avatar' => fopen('image.jpg', 'rb'), // inserts binary content of the file - 'uuid' => $explorer::literal('UUID()'), // calls the UUID() function -]); -``` - - -Selection::update(iterable $data): int .[method] ------------------------------------------------- - -Updates rows in the table according to the specified filter. Returns the number of actually modified rows. - -Pass the columns to be changed as an associative array or iterable object (like `ArrayHash` used in [forms |forms:]), where keys correspond to column names in the table: - -```php -$affected = $explorer->table('users') - ->where('id', 10) - ->update([ - 'name' => 'John Smith', - 'year' => 1994, - ]); -// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 -``` - -To change numeric values, you can use the `+=` and `-=` operators: - -```php -$explorer->table('users') - ->where('id', 10) - ->update([ - 'points+=' => 1, // increases the value of the 'points' column by 1 - 'coins-=' => 1, // decreases the value of the 'coins' column by 1 - ]); -// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 -``` - - -Selection::delete(): int .[method] ----------------------------------- - -Deletes rows from the table according to the specified filter. Returns the number of deleted rows. - -```php -$count = $explorer->table('users') - ->where('id', 10) - ->delete(); -// DELETE FROM `users` WHERE `id` = 10 -``` - -.[caution] -When calling `update()` or `delete()`, don't forget to use `where()` to specify the rows to be modified/deleted. If `where()` is not used, the operation will be performed on the entire table! - - -ActiveRow::update(iterable $data): bool .[method] -------------------------------------------------- - -Updates data in the database row represented by the `ActiveRow` object. It accepts an iterable with data to be updated (keys are column names). To change numeric values, you can use the `+=` and `-=` operators: - -After performing the update, the `ActiveRow` is automatically reloaded from the database to reflect any changes made at the database level (e.g., triggers). The method returns `true` only if a real data change occurred. - -```php -$article = $explorer->table('article')->get(1); -$article->update([ - 'views += 1', // increments the view count -]); -echo $article->views; // Outputs the current view count -``` - -This method updates only one specific row in the database. For bulk updates of multiple rows, use the [#Selection::update()] method. - - -ActiveRow::delete(): int .[method] ----------------------------------- - -Deletes the row from the database represented by the `ActiveRow` object. Returns the number of deleted rows, which should be 1. - -```php -$book = $explorer->table('book')->get(1); -$book->delete(); // Deletes the book with ID 1 -``` - -This method deletes only one specific row in the database. For bulk deletion of multiple rows, use the [#Selection::delete()] method. - - -Relationships Between Tables -============================ - -In relational databases, data is divided into multiple tables and linked together using foreign keys. Nette Database Explorer provides a revolutionary way to work with these relationships - without writing JOIN queries and without needing to configure or generate anything. - -To illustrate working with relationships, we'll use an example book database ([find it on GitHub |https://github.com/nette-examples/books]). In the database, we have tables: - -- `author` - writers and translators (columns `id`, `name`, `web`, `born`) -- `book` - books (columns `id`, `author_id`, `translator_id`, `title`, `sequel_id`) -- `tag` - tags (columns `id`, `name`) -- `book_tag` - junction table between books and tags (columns `book_id`, `tag_id`) - -[* db-schema-1-.webp *] *** Database structure used in examples .<> - -In our example book database, we find several types of relationships (although the model is simplified compared to reality): - -- **One-to-many (1:N)** - Each book **has one** author; an author can write **multiple** books. -- **Zero-to-many (0:N)** - A book **can have** a translator; a translator can translate **multiple** books. -- **Zero-to-one (0:1)** - A book **can have** a sequel. -- **Many-to-many (M:N)** - A book **can have several** tags, and a tag can be assigned to **several** books. - -In these relationships, there is always a **parent table** and a **child table**. For example, in the relationship between authors and books, the `author` table is the parent, and the `book` table is the child - you can think of it as a book always "belonging" to an author. This is also reflected in the database structure: the child table `book` contains the foreign key `author_id`, which references the parent table `author`. - -If we need to list books including their authors' names, we have two options. Either retrieve the data with a single SQL query using JOIN: - -```sql -SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; -``` - -Or retrieve the data in two steps - first the books, then their authors - and then assemble them in PHP: - -```sql -SELECT * FROM book; -SELECT * FROM author WHERE id IN (1, 2, 3); -- IDs of authors from the selected books -``` - -The second approach is actually **more efficient**, although it might be surprising. Data is fetched only once and can be better utilized in the cache. This is precisely how Nette Database Explorer works - it handles everything under the hood and offers you an elegant API: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo 'title: ' . $book->title; - echo 'written by: ' . $book->author->name; // $book->author is a record from the 'author' table - echo 'translated by: ' . $book->translator?->name; -} -``` - - -Accessing the Parent Table --------------------------- - -Accessing the parent table is straightforward. These are relationships like *a book has an author* or *a book may have a translator*. The related record is obtained via a property of the ActiveRow object - its name corresponds to the name of the foreign key column without the `_id` suffix: - -```php -$book = $explorer->table('book')->get(1); -echo $book->author->name; // finds the author based on the author_id column -echo $book->translator?->name; // finds the translator based on the translator_id column -``` - -When accessing the `$book->author` property, Explorer looks in the `book` table for a column whose name contains the string `author` (i.e., `author_id`). Based on the value in this column, it loads the corresponding record from the `author` table and returns it as an `ActiveRow`. Similarly, `$book->translator` uses the `translator_id` column. Since the `translator_id` column can contain `null`, we use the nullsafe operator `?->` in the code. - -An alternative approach is offered by the `ref()` method, which accepts two arguments: the name of the target table and the name of the joining column, and returns an `ActiveRow` instance or `null`: - -```php -echo $book->ref('author', 'author_id')->name; // relationship to the author -echo $book->ref('author', 'translator_id')->name; // relationship to the translator -``` - -The `ref()` method is useful if property-based access cannot be used, for example, because the table contains a column with the same name (i.e., `author`). In other cases, using property-based access is recommended for better readability. - -Explorer automatically optimizes database queries. When iterating through books in a loop and accessing their related records (authors, translators), Explorer does not generate a query for each book separately. Instead, it performs only **one SELECT query for each type of relationship**, significantly reducing the database load. For example: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo $book->title . ': '; - echo $book->author->name; - echo $book->translator?->name; -} -``` - -This code executes only these three lightning-fast queries to the database: - -```sql -SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- IDs from the author_id column of selected books -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- IDs from the translator_id column of selected books -``` - -.[note] -The logic for finding the joining column is determined by the implementation of [Conventions |api:Nette\Database\Conventions]. We recommend using [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], which analyzes foreign keys and allows you to easily work with existing relationships between tables. - - -Accessing the Child Table -------------------------- - -Accessing the child table works in the opposite direction. Now we ask *which books did this author write* or *which books did this translator translate*. For this type of query, we use the `related()` method, which returns a `Selection` with related records. Let's look at an example: - -```php -$author = $explorer->table('author')->get(1); - -// Outputs all books by the author -foreach ($author->related('book.author_id') as $book) { - echo "Wrote: $book->title"; -} - -// Outputs all books translated by the author -foreach ($author->related('book.translator_id') as $book) { - echo "Translated: $book->title"; -} -``` - -The `related()` method accepts the join description as a single argument with dot notation or as two separate arguments: - -```php -$author->related('book.translator_id'); // single argument -$author->related('book', 'translator_id'); // two arguments -``` - -Explorer can automatically detect the correct joining column based on the name of the parent table. In this case, it joins via the `book.author_id` column because the name of the source table is `author`: - -```php -$author->related('book'); // uses book.author_id -``` - -If multiple possible connections exist, Explorer will throw an [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -We can, of course, use the `related()` method when iterating through multiple records in a loop, and Explorer will automatically optimize the queries in this case as well: - -```php -$authors = $explorer->table('author'); -foreach ($authors as $author) { - echo $author->name . ' wrote:'; - foreach ($author->related('book') as $book) { - echo $book->title; - } -} -``` - -This code generates only two lightning-fast SQL queries: - -```sql -SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- IDs of the selected authors -``` - - -Many-to-Many Relationship -------------------------- - -For a many-to-many (M:N) relationship, a **junction table** (in our case, `book_tag`) is needed, containing two foreign key columns (`book_id`, `tag_id`). Each of these columns refers to the primary key of one of the linked tables. To retrieve related data, we first get the records from the junction table using `related('book_tag')` and then proceed to the target data: - -```php -$book = $explorer->table('book')->get(1); -// outputs the names of tags assigned to the book -foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name; // outputs the tag name via the junction table -} - -$tag = $explorer->table('tag')->get(1); -// or the opposite way: outputs the names of books marked with this tag -foreach ($tag->related('book_tag') as $bookTag) { - echo $bookTag->book->title; // outputs the book title -} -``` - -Explorer again optimizes the SQL queries into an efficient form: - -```sql -SELECT * FROM `book`; -SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- IDs of the selected books -SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- IDs of the tags found in book_tag -``` - - -Querying Through Related Tables -------------------------------- - -In the `where()`, `select()`, `order()`, and `group()` methods, you can use special notations to access columns from other tables. Explorer automatically creates the necessary JOINs. - -**Dot notation** (`parent_table.column`) is used for 1:N relationships from the perspective of the child table: - -```php -$books = $explorer->table('book'); - -// Finds books whose author's name starts with 'Jon' -$books->where('author.name LIKE ?', 'Jon%'); - -// Sorts books by author name descending -$books->order('author.name DESC'); - -// Outputs the book title and author name -$books->select('book.title, author.name'); -``` - -**Colon notation** (`:child_table.column`) is used for 1:N relationships from the perspective of the parent table: - -```php -$authors = $explorer->table('author'); - -// Finds authors who wrote a book with 'PHP' in the title -$authors->where(':book.title LIKE ?', '%PHP%'); - -// Counts the number of books for each author -$authors->select('*, COUNT(:book.id) AS book_count') - ->group('author.id'); -``` - -In the example above with colon notation (`:book.title`), the foreign key column is not specified. Explorer automatically detects the correct column based on the name of the parent table. In this case, it joins via the `book.author_id` column because the name of the source table is `author`. If multiple possible connections exist, Explorer will throw an [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -The joining column can be explicitly specified in parentheses: - -```php -// Finds authors who translated a book with 'PHP' in the title -$authors->where(':book(translator_id).title LIKE ?', '%PHP%'); -``` - -Notations can be chained to access data across multiple tables: - -```php -// Finds authors of books tagged with 'PHP' -$authors->where(':book:book_tag.tag.name', 'PHP') - ->group('author.id'); -``` - - -Extending Conditions for JOIN ------------------------------ - -The `joinWhere()` method extends the conditions specified when joining tables in SQL after the `ON` keyword. - -Let's say we want to find books translated by a specific translator: - -```php -// Finds books translated by a translator named 'David' -$books = $explorer->table('book') - ->joinWhere('translator', 'translator.name', 'David'); -// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') -``` - -In the `joinWhere()` condition, you can use the same constructs as in the `where()` method - operators, placeholders, arrays of values, or SQL expressions. - -For more complex queries with multiple JOINs, you can define table aliases: - -```php -$tags = $explorer->table('tag') - ->joinWhere(':book_tag.book.author', 'book_author.born < ?', 1950) - ->alias(':book_tag.book.author', 'book_author'); -// LEFT JOIN `book_tag` ON `tag`.`id` = `book_tag`.`tag_id` -// LEFT JOIN `book` ON `book_tag`.`book_id` = `book`.`id` -// LEFT JOIN `author` `book_author` ON `book`.`author_id` = `book_author`.`id` -// AND (`book_author`.`born` < 1950) -``` - -Note that while the `where()` method adds conditions to the `WHERE` clause, the `joinWhere()` method extends the conditions in the `ON` clause when joining tables. - -{{maintitle: Nette Database Explorer: Query Tables Without Writing SQL}} -{{description: Work with your database without writing SQL: filter, sort and aggregate data, insert and update rows, and let Explorer handle relationships between tables.}} diff --git a/database/en/guide.texy b/database/en/guide.texy deleted file mode 100644 index f58541cd0a..0000000000 --- a/database/en/guide.texy +++ /dev/null @@ -1,221 +0,0 @@ -Nette Database -************** - -.[perex] -Nette Database is a powerful and elegant database layer for PHP with a focus on simplicity and smart features. It offers two ways to work with your database: the [Explorer |explorer] for rapid application development, or the [SQL way |SQL way] for direct query manipulation. - -<div class="grid gap-3"> -<div> - - -[SQL way] -========= -- Safe, parameterized queries -- Precise control over SQL query structure -- When writing complex queries with advanced functions -- Optimize performance using specific SQL functions - -</div> - -<div> - - -[Explorer |explorer] -==================== -- Develop quickly without writing SQL -- Intuitive handling of relationships between tables -- Benefit from automatic query optimization -- Suitable for fast and convenient database work - -</div> - -</div> - - -Installation -============ - -Download and install the library using [Composer|best-practices:composer]: - -```shell -composer require nette/database -``` - - -Supported Databases -=================== - -Nette Database supports the following databases: - -|* Database Server |* DSN Name |* Explorer Support -|-----------------------|--------------|-----------------------| -| MySQL (>= 5.1) | mysql | YES | -| PostgreSQL (>= 9.0) | pgsql | YES | -| SQLite 3 (>= 3.8) | sqlite | YES | -| Oracle | oci | NO | -| MS SQL (PDO_SQLSRV) | sqlsrv | YES | -| MS SQL (PDO_DBLIB) | mssql | NO | -| ODBC | odbc | NO | - - -Two Approaches to Database Work -=============================== - -Nette Database gives you a choice: you can either write SQL queries directly (SQL way), or let them be generated automatically (Explorer). Let's see how both approaches handle the same tasks: - -[SQL way|sql-way] - SQL Queries - -```php -// Insert a record -$database->query('INSERT INTO books', [ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// Retrieve records: book authors -$result = $database->query(' - SELECT authors.*, COUNT(books.id) AS books_count - FROM authors - LEFT JOIN books ON authors.id = books.author_id - WHERE authors.active = 1 - GROUP BY authors.id -'); - -// Display (not optimal, generates N additional queries) -foreach ($result as $author) { - $books = $database->query(' - SELECT * FROM books - WHERE author_id = ? - ORDER BY published_at DESC - ', $author->id); - - echo "Author $author->name has written $author->books_count books:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -[Explorer way|explorer] - Automatic SQL Generation - -```php -// Insert a record -$database->table('books')->insert([ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// Retrieve records: book authors -$authors = $database->table('authors') - ->where('active', 1); - -// Display (automatically generates only 2 optimized queries) -foreach ($authors as $author) { - $books = $author->related('books') - ->order('published_at DESC'); - - echo "Author $author->name has written {$books->count()} books:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -The Explorer approach generates and optimizes SQL queries automatically. In the example above, the SQL way generates N+1 queries (one for authors and then one for the books of each author), while Explorer automatically optimizes queries and executes only two - one for authors and one for all their books. - -Both approaches can be freely combined in your application as needed. - - -Connection and Configuration -============================ - -To connect to the database, simply create an instance of the [api:Nette\Database\Connection] class: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password); -``` - -The `$dsn` (Data Source Name) parameter is the same as [used by PDO |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], e.g., `host=127.0.0.1;dbname=test`. In case of failure, it throws a `Nette\Database\ConnectionException`. - -However, a more convenient method is offered by [application configuration |configuration], where you just need to add a `database` section. This creates the necessary objects and also a database panel in the [Tracy |tracy:] bar. - -```neon -database: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password -``` - -Then, the connection object can be [obtained as a service from the DI container |dependency-injection:passing-dependencies], e.g.: - -```php -class Model -{ - public function __construct( - // or Nette\Database\Explorer - private Nette\Database\Connection $database, - ) { - } -} -``` - -More information about [database configuration|configuration]. - - -Manual Creation of Explorer ---------------------------- - -If you are not using the Nette DI container, you can create an instance of `Nette\Database\Explorer` manually: - -```php -// database connection -$connection = new Nette\Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); -// cache storage, implements Nette\Caching\Storage, e.g.: -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); -// handles database structure reflection -$structure = new Nette\Database\Structure($connection, $storage); -// defines rules for mapping table names, columns, and foreign keys -$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); -$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage); -``` - - -Connection Management -===================== - -When a `Connection` object is created, the connection is automatically established. If you want to delay the connection, use lazy mode - enable it in the [configuration|configuration] by setting `lazy`, or like this: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => true]); -``` - -To manage the connection, use the `connect()`, `disconnect()`, and `reconnect()` methods. -- `connect()` creates a connection if one does not already exist, and may throw a `Nette\Database\ConnectionException`. -- `disconnect()` disconnects the current database connection. -- `reconnect()` performs a disconnection and subsequent reconnection to the database. This method may also throw a `Nette\Database\ConnectionException`. - -Additionally, you can monitor events related to the connection using the `onConnect` event, which is an array of callbacks called after the database connection is established. - -```php -// runs after connecting to the database -$database->onConnect[] = function($database) { - echo "Connected to the database"; -}; -``` - -The `onQuery` event works similarly - it is an array of callbacks invoked after each executed query (and when a query fails), useful for logging or profiling. - - -Tracy Debug Bar -=============== - -If you use [Tracy |tracy:], the Database panel in the Debug Bar is automatically activated. It displays all executed queries, their parameters, execution time, and the location in the code where they were called. - -[* db-panel.webp *] - -{{maintitle: Getting Started with Nette Database: Explorer or SQL}} -{{description: Install Nette Database, connect to MySQL, PostgreSQL, SQLite or MS SQL and choose your approach: the Explorer for rapid development or SQL for full control.}} diff --git a/database/en/reflection.texy b/database/en/reflection.texy deleted file mode 100644 index 86a687812f..0000000000 --- a/database/en/reflection.texy +++ /dev/null @@ -1,130 +0,0 @@ -Structure Reflection -******************** - -.{data-version:3.2.1} -Nette Database provides tools for database structure introspection using the [api:Nette\Database\Reflection] class. It allows obtaining information about tables, columns, indexes, and foreign keys. You can use reflection to generate schemas, create flexible applications working with the database, or general database tools. - -The reflection object is obtained from the database connection instance: - -```php -$reflection = $database->getReflection(); -``` - - -Retrieving Tables ------------------ - -The readonly property `$reflection->tables` contains an associative array of all tables in the database: - -```php -// Listing the names of all tables -foreach ($reflection->tables as $name => $table) { - echo $name . "\n"; -} -``` - -Two additional methods are available: - -```php -// Checking the existence of a table -if ($reflection->hasTable('users')) { - echo "Table users exists"; -} - -// Returns the table object; throws an exception if it does not exist -$table = $reflection->getTable('users'); -``` - - -Table Information ------------------ - -A table is represented by the [Table|api:Nette\Database\Reflection\Table] object, which provides the following readonly properties: - -- `$name: string` - name of the table -- `$view: bool` - whether it is a view -- `$fullName: ?string` - full name of the table including schema (if it exists) -- `$columns: array<string, Column>` - associative array of table columns -- `$indexes: Index[]` - array of table indexes -- `$primaryKey: ?Index` - primary key of the table or null -- `$foreignKeys: ForeignKey[]` - array of table foreign keys -- `$comment: ?string` - comment of the table - - -Columns -------- - -The `columns` property of the table provides an associative array of columns, where the key is the column name and the value is an instance of [Column|api:Nette\Database\Reflection\Column] with these properties: - -- `$name: string` - name of the column -- `$table: ?Table` - reference to the column's table -- `$nativeType: string` - native database type -- `$size: ?int` - size/length of the type -- `$nullable: bool` - whether the column can contain NULL -- `$default: mixed` - default value of the column -- `$autoIncrement: bool` - whether the column is auto-increment -- `$primary: bool` - whether it is part of the primary key -- `$vendor: array` - additional metadata specific to the given database system -- `$comment: ?string` - comment of the column - -```php -foreach ($table->columns as $name => $column) { - echo "Column: $name\n"; - echo "Type: {$column->nativeType}\n"; - echo "Nullable: " . ($column->nullable ? 'Yes' : 'No') . "\n"; -} -``` - - -Indexes -------- - -The `indexes` property of the table provides an array of indexes, where each index is an instance of [Index|api:Nette\Database\Reflection\Index] with these properties: - -- `$columns: Column[]` - array of columns forming the index -- `$unique: bool` - whether the index is unique -- `$primary: bool` - whether it is a primary key -- `$name: ?string` - name of the index - -The primary key of the table can be obtained using the `primaryKey` property, which returns either an `Index` object or `null` if the table does not have a primary key. - -```php -// Listing indexes -foreach ($table->indexes as $index) { - $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); - echo "Index" . ($index->name ? " {$index->name}" : '') . ":\n"; - echo " Columns: $columns\n"; - echo " Unique: " . ($index->unique ? 'Yes' : 'No') . "\n"; -} - -// Listing the primary key -if ($primaryKey = $table->primaryKey) { - $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); - echo "Primary Key: $columns\n"; -} -``` - - -Foreign Keys ------------- - -The `foreignKeys` property of the table provides an array of foreign keys, where each foreign key is an instance of [ForeignKey|api:Nette\Database\Reflection\ForeignKey] with these properties: - -- `$foreignTable: Table` - the referenced table -- `$localColumns: Column[]` - array of local columns -- `$foreignColumns: Column[]` - array of referenced columns -- `$name: string` - name of the foreign key - -```php -// Listing foreign keys -foreach ($table->foreignKeys as $fk) { - $localCols = implode(', ', array_map(fn($col) => $col->name, $fk->localColumns)); - $foreignCols = implode(', ', array_map(fn($col) => $col->name, $fk->foreignColumns)); - - echo "FK" . ($fk->name ? " {$fk->name}" : '') . ":\n"; - echo " $localCols -> {$fk->foreignTable->name}($foreignCols)\n"; -} -``` - -{{maintitle: Nette Database Reflection: Tables, Columns and Indexes}} -{{description: Inspect your database structure from PHP: list tables and read information about their columns, indexes and foreign keys with Nette Database reflection.}} diff --git a/database/en/security.texy b/database/en/security.texy deleted file mode 100644 index 2c1b6b48d7..0000000000 --- a/database/en/security.texy +++ /dev/null @@ -1,188 +0,0 @@ -Security Risks -************** - -<div class=perex> - -Databases often contain sensitive data and allow performing dangerous operations. For secure work with Nette Database, it is crucial to: - -- Understand the difference between secure and insecure APIs -- Use parameterized queries -- Properly validate input data - -</div> - - -What is SQL Injection? -====================== - -SQL injection is the most serious security risk when working with databases. It occurs when unsanitized user input becomes part of an SQL query. An attacker can insert their own SQL commands and thereby: -- Gain unauthorized access to data -- Modify or delete data in the database -- Bypass authentication - -```php -// ❌ DANGEROUS CODE - vulnerable to SQL injection -$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); - -// An attacker might enter a value like: ' OR '1'='1 -// The resulting query would be: SELECT * FROM users WHERE name = '' OR '1'='1' -// Which returns all users -``` - -The same applies to Database Explorer: - -```php -// ❌ DANGEROUS CODE - vulnerable to SQL injection -$table->where('name = ' . $_GET['name']); -$table->where("name = '$_GET[name]'"); -``` - - -Parameterized Queries -===================== - -The fundamental defense against SQL injection is parameterized queries. Nette Database offers several ways to use them. - -The simplest way is to use **question mark placeholders**: - -```php -// ✅ Secure parameterized query -$database->query('SELECT * FROM users WHERE name = ?', $name); - -// ✅ Secure condition in Explorer -$table->where('name = ?', $name); -``` - -This applies to all other methods in [Database Explorer|explorer] that allow inserting expressions with question mark placeholders and parameters. - -For INSERT, UPDATE commands, or the WHERE clause, we can pass values in an array: - -```php -// ✅ Secure INSERT -$database->query('INSERT INTO users', [ - 'name' => $name, - 'email' => $email, -]); - -// ✅ Secure INSERT in Explorer -$table->insert([ - 'name' => $name, - 'email' => $email, -]); -``` - - -Parameter Value Validation -========================== - -Parameterized queries are the cornerstone of secure database work. However, the values we insert into them must undergo several levels of checks: - - -Type Checking -------------- - -**The most important thing is to ensure the correct data type of parameters** - this is a necessary condition for the safe use of Nette Database. The database assumes that all input data has the correct data type corresponding to the given column. - -For example, if `$name` in the previous examples were unexpectedly an array instead of a string, Nette Database would try to insert all its elements into the SQL query, leading to an error. Therefore, **never use** unvalidated data from `$_GET`, `$_POST`, or `$_COOKIE` directly in database queries. - - -Format Validation ------------------ - -At the second level, we check the format of the data - for example, whether strings are in UTF-8 encoding and their length corresponds to the column definition, or whether numerical values are within the allowed range for the given column data type. - -For this level of validation, we can partially rely on the database itself - many databases will reject invalid data. However, behavior can vary; some might silently truncate long strings or clip numbers outside the range. - - -Domain-Specific Validation --------------------------- - -The third level involves logical checks specific to your application. For example, verifying that values from select boxes match the offered options, that numbers are within the expected range (e.g., age 0-150 years), or that mutual dependencies between values make sense. - - -Recommended Validation Methods ------------------------------- - -- Use [Nette Forms|forms:], which automatically ensure proper validation of all inputs. -- Use [Presenters|application:] and specify data types for parameters in `action*()` and `render*()` methods. -- Or implement your own validation layer using standard PHP tools like `filter_var()`. - - -Safe Work with Columns -====================== - -In the previous section, we showed how to properly validate parameter values. However, when using arrays in SQL queries, we must pay equal attention to their keys. - -```php -// ❌ DANGEROUS CODE - keys in the array are not sanitized -$database->query('INSERT INTO users', $_POST); -``` - -For INSERT and UPDATE commands, this is a critical security flaw - an attacker can insert or modify any column in the database. They could, for example, set `is_admin = 1` or insert arbitrary data into sensitive columns (the so-called Mass Assignment Vulnerability). - -In WHERE conditions, it is even more dangerous because they can contain operators: - -```php -// ❌ DANGEROUS CODE - keys in the array are not sanitized -$_POST['salary >'] = 100000; -$database->query('SELECT * FROM users WHERE', $_POST); -// executes the query WHERE (`salary` > 100000) -``` - -An attacker can use this approach to systematically discover employee salaries. They might start, for example, with a query for salaries above 100,000, then below 50,000, and by gradually narrowing the range, they can reveal the approximate salaries of all employees. This type of attack is called SQL enumeration. - -The `where()` and `whereOr()` methods are even [much more flexible |explorer#where] and support SQL expressions, including operators and functions, in keys and values. This gives an attacker the ability to perform SQL injection: - -```php -// ❌ DANGEROUS CODE - attacker can insert their own SQL -$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1']; -$table->where($_POST); -// executes the query WHERE (0) UNION SELECT name, salary FROM users WHERE (1) -``` - -This attack terminates the original condition using `0)`, appends its own `SELECT` using `UNION` to obtain sensitive data from the `users` table, and closes the syntactically correct query using `WHERE (1)`. - - -Column Whitelist ----------------- - -For safe work with column names, we need a mechanism that ensures the user can only work with allowed columns and cannot add their own. We could try to detect and block dangerous column names (blacklist), but this approach is unreliable - an attacker can always come up with a new way to write a dangerous column name that we didn't anticipate. - -Therefore, it is much safer to reverse the logic and define an explicit list of allowed columns (whitelist): - -```php -// Columns the user is allowed to modify -$allowedColumns = ['name', 'email', 'active']; - -// Remove all unauthorized columns from the input -$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); - -// ✅ Now safe to use in queries, such as: -$database->query('INSERT INTO users', $filteredData); -$table->update($filteredData); -$table->where($filteredData); -``` - - -Dynamic Identifiers -=================== - -For dynamic table and column names, use the `?name` placeholder. This ensures proper escaping of identifiers according to the syntax of the given database (e.g., using backticks in MySQL): - -```php -// ✅ Safe use of trusted identifiers -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name', $column, $table); -// Result in MySQL: SELECT `name` FROM `users` -``` - -Important: Use the `?name` symbol only for trusted values defined in the application code. For values from the user, use a [whitelist |#Column Whitelist] again. Otherwise, you expose yourself to security risks: - -```php -// ❌ DANGEROUS - never use user input -$database->query('SELECT ?name FROM users', $_GET['column']); -``` - -{{maintitle: PHP SQL Injection Protection in Nette Database}} -{{description: What SQL injection is and how to prevent it in Nette Database: parameterized queries, input validation, safe work with columns and dynamic identifiers.}} diff --git a/database/en/sql-way.texy b/database/en/sql-way.texy deleted file mode 100644 index 62a72cfdf3..0000000000 --- a/database/en/sql-way.texy +++ /dev/null @@ -1,516 +0,0 @@ -SQL Way -******* - -.[perex] -Nette Database offers two ways of working: you can write SQL queries yourself (SQL way), or have them generated automatically (see [Explorer |explorer]). The SQL way gives you full control over the queries while ensuring they are constructed securely. - -.[note] -Details on database connection and configuration can be found in the [Connection and Configuration |guide#Connection and Configuration] chapter. - - -Basic Querying -============== - -The `query()` method is used for database querying. It returns a [ResultSet |api:Nette\Database\ResultSet] object, which represents the query result. If the query fails, the method [throws an exception|exceptions]. You can iterate through the query result using a `foreach` loop, or use one of the [helper methods |#Fetching Data]. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; -} -``` - -To securely insert values into SQL queries, use parameterized queries. Nette Database makes this extremely simple: just add a comma and the value after the SQL query: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -With multiple parameters, you have two options: You can either interleave the SQL query with parameters: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age); -``` - -Or write the entire SQL query first and then append all the parameters: - -```php -$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); -``` - - -Protection Against SQL Injection -================================ - -Why is it important to use parameterized queries? Because they protect you from an attack called SQL injection, where an attacker could inject their own SQL commands and thereby gain access to or damage data in the database. - -.[warning] -**Never insert variables directly into an SQL query!** Always use parameterized queries, which protect you from SQL injection. - -```php -// ❌ DANGEROUS CODE - vulnerable to SQL injection -$database->query("SELECT * FROM users WHERE name = '$name'"); - -// ✅ Secure parameterized query -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Familiarize yourself with [potential security risks |security]. - - -Querying Techniques -=================== - - -WHERE Conditions ----------------- - -You can write `WHERE` conditions as an associative array, where keys are column names and values are the data for comparison. Nette Database automatically selects the most suitable SQL operator based on the value type. - -```php -$database->query('SELECT * FROM users WHERE', [ - 'name' => 'John', - 'active' => true, -]); -// WHERE `name` = 'John' AND `active` = 1 -``` - -You can also explicitly specify the comparison operator in the key: - -```php -$database->query('SELECT * FROM users WHERE', [ - 'age >' => 25, // uses the > operator - 'name LIKE' => '%John%', // uses the LIKE operator - 'email NOT LIKE' => '%example.com%', // uses the NOT LIKE operator -]); -// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' -``` - -Nette automatically handles special cases like `null` values or arrays. - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name' => 'Laptop', // uses the = operator - 'category_id' => [1, 2, 3], // uses IN - 'description' => null, // uses IS NULL -]); -// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL -``` - -For negative conditions, use the `NOT` operator: - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // uses the != operator - 'category_id NOT' => [1, 2, 3], // uses NOT IN - 'description NOT' => null, // uses IS NOT NULL - 'id NOT' => [], // skipped -]); -// WHERE `name` != 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL -``` - -By default, conditions are joined using the `AND` operator. This can be changed using the [?or placeholder |#SQL Construction Hints]. - - -ORDER BY Rules --------------- - -The `ORDER BY` clause can be written using an array. Specify columns in the keys, and use a boolean value to indicate ascending (`true`) or descending (`false`) order: - -```php -$database->query('SELECT id FROM author ORDER BY', [ - 'id' => true, // ascending - 'name' => false, // descending -]); -// SELECT id FROM author ORDER BY `id`, `name` DESC -``` - - -Inserting Data (INSERT) ------------------------ - -The SQL `INSERT` command is used for inserting records. - -```php -$values = [ - 'name' => 'John Doe', - 'email' => 'john@example.com', -]; -$database->query('INSERT INTO users ?', $values); -$userId = $database->getInsertId(); -``` - -The `getInsertId()` method returns the ID of the last inserted row. For some databases (e.g., PostgreSQL), it is necessary to specify the name of the sequence from which the ID should be generated as a parameter, using `$database->getInsertId($sequenceId)`. - -You can also pass [#special values], such as files, DateTime objects, or enum types, as parameters. - -Inserting multiple records at once: - -```php -$database->query('INSERT INTO users ?', [ - ['name' => 'User 1', 'email' => 'user1@mail.com'], - ['name' => 'User 2', 'email' => 'user2@mail.com'], -]); -``` - -A multi-record INSERT is much faster because only a single database query is executed, instead of many individual ones. - -**Security Note:** Never use unvalidated data as `$values`. Familiarize yourself with [possible risks |security#Safe Work with Columns]. - - -Updating Data (UPDATE) ----------------------- - -The SQL `UPDATE` command is used for updating records. - -```php -// Update a single record -$values = [ - 'name' => 'John Smith', -]; -$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); -``` - -The number of affected rows is returned by `$result->getRowCount()`. - -For `UPDATE`, we can use the `+=` and `-=` operators: - -```php -$database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // increment login_count -], 1); -``` - -Example of inserting or updating a record if it already exists. We use the `ON DUPLICATE KEY UPDATE` technique: - -```php -$values = [ - 'name' => $name, - 'year' => $year, -]; -$database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', - $values + ['id' => $id], - $values, -); -// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978) -// ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 -``` - -Notice that Nette Database recognizes the context in which an array parameter is used within the SQL command and constructs the SQL code accordingly. So, from the first array, it constructed `(id, name, year) VALUES (123, 'Jim', 1978)`, while it converted the second into the form `name = 'Jim', year = 1978`. We discuss this in more detail in the section [#SQL Construction Hints]. - - -Deleting Data (DELETE) ----------------------- - -The SQL `DELETE` command is used for deleting records. Example of obtaining the number of deleted rows: - -```php -$count = $database->query('DELETE FROM users WHERE id = ?', 1) - ->getRowCount(); -``` - - -SQL Construction Hints ----------------------- - -A hint is a special placeholder in an SQL query that specifies how the parameter value should be converted into an SQL expression: - -| Hint | Description | Automatically Used For -|-----------|-------------------------------------------------|----------------------------- -| `?name` | Used for inserting table or column names | - -| `?values` | Generates `(key, ...) VALUES (value, ...)` | `INSERT ... ?`, `REPLACE ... ?` -| `?set` | Generates assignments `key = value, ...` | `SET ?`, `KEY UPDATE ?` -| `?and` | Joins conditions in an array with `AND` | `WHERE ?`, `HAVING ?` -| `?or` | Joins conditions in an array with `OR` | - -| `?order` | Generates the `ORDER BY` clause | `ORDER BY ?`, `GROUP BY ?` - -The `?name` placeholder is used for dynamically inserting table and column names into the query. Nette Database handles the correct quoting of identifiers according to the database conventions (e.g., enclosing in backticks in MySQL). - -```php -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); -// SELECT `name` FROM `users` WHERE id = 1 (in MySQL) -``` - -**Warning:** Only use the `?name` placeholder for validated table and column names. Otherwise, you risk [security vulnerabilities |security#Dynamic Identifiers]. - -Other hints usually do not need to be specified, as Nette uses smart autodetection when constructing the SQL query (see the third column of the table). But you can use them, for example, in a situation where you want to join conditions using `OR` instead of `AND`: - -```php -$database->query('SELECT * FROM users WHERE ?or', [ - 'name' => 'John', - 'email' => 'john@example.com', -]); -// SELECT * FROM users WHERE `name` = 'John' OR `email` = 'john@example.com' -``` - - -Special Values --------------- - -In addition to common scalar types (string, int, bool), you can also pass special values as parameters: - -- files: `fopen('image.gif', 'r')` inserts the binary content of the file -- date and time: `DateTimeInterface` objects are converted to the database format -- enum types: instances of `enum` are converted to their value -- SQL literals: those created using `Connection::literal('NOW()')` are inserted directly into the query - -```php -$database->query('INSERT INTO articles ?', [ - 'title' => 'My Article', - 'published_at' => new DateTimeImmutable, // or new DateTime - 'content' => fopen('image.png', 'r'), - 'state' => Status::Draft, -]); -``` - -For databases that do not have native support for the `datetime` data type (like SQLite and Oracle), `DateTime` and `DateTimeImmutable` objects are converted to a value specified in the [database configuration|configuration] by the `formatDateTime` item (default value is `U` - Unix timestamp). - - -SQL Literals ------------- - -In some cases, you need to pass raw SQL code as a value, which should not be treated as a string and escaped. Objects of the `Nette\Database\SqlLiteral` class are used for this purpose. They are created by the `Connection::literal()` method. - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - 'year >' => $database::literal('YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) -``` - -Alternatively: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) -``` - -SQL literals can contain parameters: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > ? AND year < ?', $min, $max), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) -``` - -This allows for interesting combinations: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('?or', [ - 'active' => true, - 'role' => $role, - ]), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (`active` = 1 OR `role` = 'admin') -``` - - -Fetching Data -============= - - -Shortcuts for SELECT Queries ----------------------------- - -To simplify data retrieval, `Connection` offers several shortcuts that combine a `query()` call with a subsequent `fetch*()` call. These methods accept the same parameters as `query()`, i.e., an SQL query and optional parameters. A full description of the `fetch*()` methods can be found [below |#fetch]. - -| `fetch($sql, ...$params): ?Row` | Executes the query and returns the first row as a `Row` object or `null`. -| `fetchAll($sql, ...$params): array` | Executes the query and returns all rows as an array of `Row` objects. -| `fetchPairs($sql, ...$params): array` | Executes the query and returns an associative array (key => value pairs). -| `fetchField($sql, ...$params): mixed` | Executes the query and returns the value of the first column in the first row. -| `fetchList($sql, ...$params): ?array` | Executes the query and returns the first row as an indexed array or `null`. - -Example: - -```php -// fetchField() - returns the value of the first cell -$count = $database->query('SELECT COUNT(*) FROM articles') - ->fetchField(); -``` - - -`foreach` - Iterating Over Rows -------------------------------- - -After executing a query, a [ResultSet|api:Nette\Database\ResultSet] object is returned, which allows iterating through the results in several ways. The easiest way to execute a query and retrieve rows is by iterating in a `foreach` loop. This method is the most memory-efficient, as it fetches data row by row and does not load the entire result set into memory at once. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; - // ... -} -``` - -.[note] -The `ResultSet` can only be iterated once. If you need to iterate repeatedly, you must first load the data into an array, for example, using the `fetchAll()` method. - - -fetch(): ?Row .[method] ------------------------ - -Returns a row as a `Row` object. If no more rows exist, it returns `null`. Advances the internal pointer to the next row. - -```php -$result = $database->query('SELECT * FROM users'); -$row = $result->fetch(); // loads the first row -if ($row) { - echo $row->name; -} -``` - - -fetchAll(): array .[method] ---------------------------- - -Returns all remaining rows from the `ResultSet` as an array of `Row` objects. - -```php -$result = $database->query('SELECT * FROM users'); -$rows = $result->fetchAll(); // loads all rows -foreach ($rows as $row) { - echo $row->name; -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Returns the result set as an associative array. The first argument specifies the column to use as keys, and the second argument specifies the column to use as values: - -```php -$result = $database->query('SELECT id, name FROM users'); -$names = $result->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -If only the first parameter (`$key`) is provided, the entire row (`Row` object) will be used as the value: - -```php -$rows = $result->fetchPairs('id'); -// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...] -``` - -In case of duplicate keys, the value from the last row is used. Using `null` as the key results in a numerically indexed array (starting from zero), preventing key collisions: - -```php -$names = $result->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Alternatively, you can provide a callback that processes each row. The callback can return a single value or a key-value pair. - -```php -$result = $database->query('SELECT * FROM users'); -$items = $result->fetchPairs(fn($row) => "$row->id - $row->name"); -// ['1 - John', '2 - Jane', ...] - -// The callback can also return an array with a key & value pair: -$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); -// ['John' => 46, 'Jane' => 21, ...] -``` - - -fetchField(): mixed .[method] ------------------------------ - -Returns the value of the first column from the current row. If no more rows exist, it returns `null`. Advances the internal pointer to the next row. - -```php -$result = $database->query('SELECT name FROM users'); -$name = $result->fetchField(); // loads the name from the first row -``` - - -fetchList(): ?array .[method] ------------------------------ - -Returns the row as an indexed array. If no more rows exist, it returns `null`. Advances the internal pointer to the next row. - -```php -$result = $database->query('SELECT name, email FROM users'); -$row = $result->fetchList(); // ['John', 'john@example.com'] -``` - - -getRowCount(): ?int .[method] ------------------------------ - -Returns the number of affected rows from the last `UPDATE` or `DELETE` query. For `SELECT` queries, it returns the number of rows in the result set. However, this might not always be known, in which case the method returns `null`. - - -getColumnCount(): ?int .[method] --------------------------------- - -Returns the number of columns in the `ResultSet`. - - -Query Information -================= - -For debugging purposes, we can obtain information about the last executed query: - -```php -echo $database->getLastQueryString(); // prints the SQL query - -$result = $database->query('SELECT * FROM articles'); -echo $result->getQueryString(); // prints the SQL query -echo $result->getTime(); // prints the execution time in seconds -``` - -To display the result as an HTML table, you can use: - -```php -$result = $database->query('SELECT * FROM articles'); -$result->dump(); -``` - -`ResultSet` provides information about column types: - -```php -$result = $database->query('SELECT * FROM articles'); -$types = $result->getColumnTypes(); - -foreach ($types as $column => $type) { - echo "$column is of type $type"; // e.g., 'id is of type int' -} -``` - - -Query Logging -------------- - -We can implement custom query logging. The `onQuery` event is an array of callbacks that are called after each executed query: - -```php -$database->onQuery[] = function ($database, $result) use ($logger) { - $logger->info('Query: ' . $result->getQueryString()); - $logger->info('Time: ' . $result->getTime()); - - if ($result->getRowCount() > 1000) { - $logger->warning('Large result set: ' . $result->getRowCount() . ' rows'); - } -}; -``` - -{{maintitle: Nette Database SQL Way: Safe Queries with Full Control}} -{{description: Write SQL queries yourself while Nette Database keeps them safe: parameters against SQL injection, query techniques, fetching data and query information.}} diff --git a/database/en/transactions.texy b/database/en/transactions.texy deleted file mode 100644 index 6103213ba9..0000000000 --- a/database/en/transactions.texy +++ /dev/null @@ -1,48 +0,0 @@ -Transactions -************ - -.[perex] -Transactions guarantee that either all operations within the transaction are executed, or none are. They are useful for ensuring data consistency during complex operations. - -The simplest way to use transactions looks like this: - -```php -$database->beginTransaction(); -try { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); - $database->commit(); -} catch (\Exception $e) { - $database->rollBack(); - throw $e; -} -``` - -You can achieve the same result much more elegantly using the `transaction()` method. It accepts a callback which is executed within the transaction. If the callback runs without an exception, the transaction is automatically committed. If an exception occurs, the transaction is rolled back, and the exception is propagated further. - -```php -$database->transaction(function ($database) use ($id) { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); -}); -``` - -Calls to `transaction()` can be nested, which makes it easy to compose methods that each manage their own transaction. Only the outermost transaction is actually sent to the database as `BEGIN`/`COMMIT`; inner calls merely track the nesting depth. Calling `beginTransaction()`, `commit()`, or `rollBack()` manually inside a `transaction()` callback throws a `LogicException`. - -The `transaction()` method can also return values: - -```php -$count = $database->transaction(function ($database) { - $result = $database->query('UPDATE users SET active = ?', true); - return $result->getRowCount(); // returns the number of updated rows -}); -``` - -{{maintitle: PHP Database Transactions in Nette Database}} -{{description: Run several operations as one: either all succeed or none do. Use beginTransaction(), commit() and rollBack(), or the simpler transaction() with a callback.}} diff --git a/database/en/type-conversion.texy b/database/en/type-conversion.texy deleted file mode 100644 index 6afa03eb77..0000000000 --- a/database/en/type-conversion.texy +++ /dev/null @@ -1,58 +0,0 @@ -Type Conversion -*************** - -.[perex] -Nette Database automatically converts values returned from the database to the corresponding PHP types. - - -Date and Time -------------- - -Time values are converted to `Nette\Utils\DateTime` objects. If you want time values to be converted to immutable `Nette\Database\DateTime` objects, set the `newDateTime` option to true in the [configuration |configuration]. - -```php -$row = $database->fetch('SELECT created_at FROM articles'); -echo $row->created_at instanceof DateTime; // true -echo $row->created_at->format('j. n. Y'); -``` - -In the case of MySQL, the `TIME` data type is converted to `DateInterval` objects. - - -Boolean Values --------------- - -Boolean values are automatically converted to `true` or `false`. For MySQL, `TINYINT(1)` is converted if we set `convertBoolean` in the [configuration |configuration]. - -```php -$row = $database->fetch('SELECT is_published FROM articles'); -echo gettype($row->is_published); // 'boolean' -``` - - -Numeric Values --------------- - -Numeric values are converted to `int` or `float` according to the column type in the database: - -```php -$row = $database->fetch('SELECT id, price FROM products'); -echo gettype($row->id); // integer -echo gettype($row->price); // float -``` - - -Custom Normalization --------------------- - -Using the `setRowNormalizer(?callable $normalizer)` method, you can set a custom function for transforming rows from the database. This is useful, for example, for automatic data type conversion. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // type conversion happens here - return $row; -}); -``` - -{{maintitle: Nette Database Type Conversion: From SQL to PHP Types}} -{{description: Nette Database automatically converts database values to PHP types: integers, floats, booleans, and DateTime and DateInterval objects.}} diff --git a/database/en/upgrading.texy b/database/en/upgrading.texy deleted file mode 100644 index 2c12506410..0000000000 --- a/database/en/upgrading.texy +++ /dev/null @@ -1,39 +0,0 @@ -Upgrading -********* - - -Upgrading to Version 3.2 -======================== - -The minimum required PHP version is 8.1. - -The code has been carefully tuned for PHP 8.1. All new type hints for methods and properties have been added. The changes are minor: - -- MySQL: zero date `0000-00-00` is returned as `null` -- MySQL: decimal without decimal places is returned as int instead of float -- The `time` type is returned as a `DateTime` object with the date set to `0001-01-01` instead of the current date - - -Upgrading to Version 3.1 -======================== - -- the class `Nette\Database\Context` was renamed to `Nette\Database\Explorer` for consistency with the name [Database Explorer|explorer] -- the interfaces `Nette\Database\IRow` and `Nette\Database\IRowContainer` are marked as deprecated as unnecessary -- the `MySqlDriver` driver uses subqueries -- the SQL statement translator better controls where arrays can be passed - - -Upgrading to Version 3.0 -======================== - -Some methods, such as `fetch()` or `fetchField()`, now return `null` instead of `false` when there is no next row. - - -Upgrading to Version 2.3 -======================== - -- the `MySqlDriver` uses `utf8mb4` encoding by default for MySQL >= 5.5.3 instead of `utf8` -- `IReflection` was split into the twin interfaces `IStructure` and `IConventions` - -{{maintitle: Upgrading Nette Database: What Changed in Each Version}} -{{description: What changed in Nette Database 3.2, 3.1, 3.0 and 2.3, and how to update your code when moving to a newer version.}} diff --git a/database/es/@home.texy b/database/es/@home.texy deleted file mode 100644 index 4b02660296..0000000000 --- a/database/es/@home.texy +++ /dev/null @@ -1,18 +0,0 @@ -Bases de datos compatibles -========================== - -Están soportados estos servidores de bases de datos: - -|* Servidor de base de datos |* Nombre DSN |* Soporte en Core |* Soporte en Explorer -| MySQL (>= 5.1) | mysql | SÍ | SÍ -| PostgreSQL (>= 9.0) | pgsql | SÍ | SÍ -| Sqlite 3 (>= 3.8) | sqlite | SÍ | SÍ -| Oracle | oci | SÍ | - -| MS SQL (PDO_SQLSRV) | sqlsrv | SÍ | SÍ -| MS SQL (PDO_DBLIB) | mssql | SÍ | - -| ODBC | odbc | SÍ | - - - - -{{maintitle: Nette Database - awesome database layer for PHP}} -{{description: Nette Database simplifica notablemente la obtención de datos de la base de datos sin escribir consultas SQL. Ejecuta consultas eficientes y no transfiere datos innecesarios.}} diff --git a/database/es/@left-menu.texy b/database/es/@left-menu.texy deleted file mode 100644 index cb91994186..0000000000 --- a/database/es/@left-menu.texy +++ /dev/null @@ -1,21 +0,0 @@ -Nette Database -************** -- [Primeros pasos |guide] -- [Enfoque SQL|sql-way] -- [Explorer|explorer] -- [Transacciones|transactions] -- [Excepciones|exceptions] -- [Reflexión|reflection] -- [Conversión de tipos |type-conversion] -- [Configuración|configuration] -- [Riesgos de seguridad |security] -- [Actualización|upgrading] - - -Lecturas adicionales -******************** -- [Documentación de Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Buenas prácticas |best-practices:] -- [Solución de problemas |nette:troubleshooting] diff --git a/database/es/@meta.texy b/database/es/@meta.texy deleted file mode 100644 index 3798d9cda4..0000000000 --- a/database/es/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentación de Nette}} diff --git a/database/es/configuration.texy b/database/es/configuration.texy deleted file mode 100644 index ab1b60004b..0000000000 --- a/database/es/configuration.texy +++ /dev/null @@ -1,110 +0,0 @@ -Configuración de la base de datos -********************************* - -.[perex] -Resumen de las opciones de configuración de Nette Database. - -Si no usa el framework entero, sino solo esta biblioteca, lea [cómo cargar la configuración|bootstrap:]. - - -Una sola conexión ------------------ - -Configuración de una única conexión a la base de datos: - -```neon -database: - # DSN, la única clave obligatoria - dsn: "sqlite:%appDir%/Model/demo.db" - user: ... - password: ... -``` - -Esto crea los servicios `Nette\Database\Connection` y `Nette\Database\Explorer`, que normalmente se pasan mediante [autowiring |dependency-injection:autowiring] o refiriéndose a [su nombre |#Servicios DI]. - -Otros ajustes: - -```neon -database: - # ¿mostrar el panel de la base de datos en la Tracy Bar? - debugger: ... # (bool) activado de forma predeterminada si Tracy está activa - - # ¿mostrar el EXPLAIN de las consultas en la Tracy Bar? - explain: ... # (bool) el valor predeterminado es true - - # ¿activar el autowiring para esta conexión? - autowired: ... # (bool) true de forma predeterminada para la primera conexión - - # convenciones de las tablas: discovered, static o el nombre de una clase - conventions: discovered # (string) el valor predeterminado es 'discovered' - - options: - # ¿conectarse a la base de datos solo cuando haga falta? - lazy: ... # (bool) el valor predeterminado es false - - # clase del driver de base de datos de PHP - driverClass: # (string) - - # solo MySQL: establece sql_mode - sqlmode: # (string) - - # solo MySQL: establece SET NAMES - charset: # (string) el valor predeterminado es 'utf8mb4' - - # solo MySQL: convierte TINYINT(1) a bool - convertBoolean: # (bool) el valor predeterminado es false - - # devuelve las columnas de fecha como objetos inmutables (desde la versión 3.2.1) - newDateTime: # (bool) el valor predeterminado es false - - # solo Oracle y SQLite: formato para guardar la fecha - formatDateTime: # (string) el valor predeterminado es 'U' -``` - -La clave `options` puede contener otras opciones que encontrará en la [documentación del driver PDO |https://www.php.net/manual/en/pdo.drivers.php], por ejemplo: - -```neon -database: - options: - PDO::MYSQL_ATTR_COMPRESS: true -``` - - -Varias conexiones ------------------ - -En la configuración podemos definir varias conexiones a bases de datos repartiéndolas en secciones con nombre: - -```neon -database: - main: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password - - another: - dsn: 'sqlite::memory:' -``` - -El autowiring está activado solo para los servicios de la primera sección. Se puede cambiar con `autowired: false` o `autowired: true`. - - -Servicios DI ------------- - -Estos servicios se añaden al contenedor DI, donde `###` representa el nombre de la conexión: - -| Nombre | Tipo | Descripción -|---------------------------|---------------------------------|--------------------------- -| `database.###.connection` | [api:Nette\Database\Connection] | conexión a la base de datos -| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |explorer] - - -Si definimos una sola conexión, los nombres de los servicios serán `database.default.connection` y `database.default.explorer`. Si definimos varias conexiones como en el ejemplo anterior, los nombres corresponderán a las secciones, es decir, `database.main.connection`, `database.main.explorer`, y también `database.another.connection` y `database.another.explorer`. - -Los servicios que no están en el autowiring se pasan explícitamente refiriéndose a su nombre: - -```neon -services: - - UserFacade(@database.another.connection) -``` diff --git a/database/es/exceptions.texy b/database/es/exceptions.texy deleted file mode 100644 index 78b8f4ae28..0000000000 --- a/database/es/exceptions.texy +++ /dev/null @@ -1,37 +0,0 @@ -Excepciones -*********** - -Nette Database usa una jerarquía de excepciones. La clase base es `Nette\Database\DriverException`, que extiende `PDOException` y ofrece funcionalidad ampliada para trabajar con los errores de la base de datos: - -- El método `getDriverCode()` devuelve el código de error del driver de la base de datos. -- El método `getSqlState()` devuelve el código SQLSTATE. -- Los métodos `getQueryString()` y `getParameters()` permiten obtener la consulta original y sus parámetros. - -La clase `DriverException` la extienden las siguientes excepciones especializadas: - -- `ConnectionException`: indica un fallo al conectar con el servidor de la base de datos. - - `ConnectionLostException` .{data-version:3.2.9}: la conexión se cayó durante una operación (reinicio del servidor, fallo de red, idle timeout); antes de seguir usándola hay que reconectar. -- `ConstraintViolationException`: la clase base para las violaciones de restricciones de la base de datos, de la que heredan las siguientes excepciones: - - `ForeignKeyConstraintViolationException`: violación de una restricción de clave foránea. - - `NotNullConstraintViolationException`: violación de una restricción NOT NULL. - - `UniqueConstraintViolationException`: violación de una restricción de unicidad. - - `CheckConstraintViolationException` .{data-version:3.2.9}: violación de una restricción CHECK. -- `DeadlockException` .{data-version:3.2.9}: un deadlock o un conflicto de serialización detectado por el servidor; la transacción se ha revertido y se puede reintentar. -- `LockTimeoutException` .{data-version:3.2.9}: se ha superado el tiempo de espera de un bloqueo; la sentencia se ha abortado, pero la transacción que la envuelve suele seguir abierta. - -El siguiente ejemplo muestra cómo capturar una `UniqueConstraintViolationException`, que se produce al intentar insertar un usuario con un correo que ya existe en la base de datos (suponiendo que la columna `email` tenga un índice único): - -```php -try { - $database->query('INSERT INTO users', [ - 'email' => 'john@example.com', - 'name' => 'John Doe', - 'password' => $hashedPassword, - ]); -} catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'A user with this email already exists.'; - -} catch (Nette\Database\DriverException $e) { - echo 'An error occurred during registration: ' . $e->getMessage(); -} -``` diff --git a/database/es/explorer.texy b/database/es/explorer.texy deleted file mode 100644 index d9f3e7a984..0000000000 --- a/database/es/explorer.texy +++ /dev/null @@ -1,912 +0,0 @@ -Database Explorer -***************** - -<div class=perex> - -Explorer ofrece una forma intuitiva y eficiente de trabajar con la base de datos. Gestiona automáticamente las relaciones entre tablas y optimiza las consultas, lo que le permite concentrarse en la lógica de su aplicación. Funciona de inmediato, sin configuración. Si necesita control total sobre las consultas SQL, puede usar el [enfoque SQL |SQL way]. - -- trabajar con los datos es natural y fácil de entender -- genera consultas SQL optimizadas que obtienen solo los datos necesarios -- da acceso sencillo a los datos relacionados sin necesidad de escribir consultas JOIN -- funciona de inmediato, sin ninguna configuración ni generación de entidades - -</div> - - -El trabajo con Explorer empieza llamando al método `table()` sobre el objeto [api:Nette\Database\Explorer] (véase [Conexión y configuración |guide#Conexión y configuración] para los detalles sobre cómo configurar la conexión a la base de datos): - -```php -$books = $explorer->table('book'); // 'book' es el nombre de la tabla -``` - -El método devuelve un objeto [Selection |api:Nette\Database\Table\Selection], que representa una consulta SQL. A este objeto se le pueden encadenar más métodos para filtrar y ordenar los resultados. La consulta se monta y se ejecuta solo cuando se piden los datos, por ejemplo al recorrerla con `foreach`. Cada fila está representada por un objeto [ActiveRow |api:Nette\Database\Table\ActiveRow]: - -```php -foreach ($books as $book) { - echo $book->title; // imprime la columna 'title' - echo $book->author_id; // imprime la columna 'author_id' -} -``` - -Explorer simplifica enormemente el trabajo con las [relaciones entre tablas |#Relaciones entre tablas]. El siguiente ejemplo muestra con qué facilidad podemos mostrar datos de tablas relacionadas (libros y sus autores). Fíjese en que no hace falta escribir ninguna consulta JOIN; Nette las genera por nosotros: - -```php -$books = $explorer->table('book'); - -foreach ($books as $book) { - echo 'Book: ' . $book->title; - echo 'Author: ' . $book->author->name; // crea un JOIN con la tabla 'author' -} -``` - -Nette Database Explorer optimiza las consultas para lograr la máxima eficiencia. El ejemplo anterior ejecuta solo dos consultas SELECT, independientemente de si procesamos 10 o 10 000 libros. - -Además, Explorer lleva la cuenta de qué columnas se usan en el código y obtiene de la base de datos solo esas, lo que ahorra aún más rendimiento. Este comportamiento es completamente automático y adaptativo. Si más tarde modifica el código para usar más columnas, Explorer ajusta las consultas automáticamente. No tiene que configurar nada ni pensar en qué columnas hará falta: déjeselo a Nette. - - -Filtrado y ordenación -===================== - -La clase `Selection` ofrece métodos para filtrar y ordenar la selección de datos. - -.[language-php] -| `where($condition, ...$params)` | Añade una condición WHERE. Varias condiciones se combinan con AND | -| `whereOr(array $conditions)` | Añade un grupo de condiciones WHERE combinadas con OR | -| `wherePrimary($value)` | Añade una condición WHERE sobre la clave primaria | -| `order($columns, ...$params)` | Establece la ordenación con ORDER BY | -| `select($columns, ...$params)` | Indica qué columnas obtener | -| `limit($limit, $offset = null)` | Limita el número de filas (LIMIT) y opcionalmente fija el OFFSET | -| `page($page, $itemsPerPage, &$numOfPages = null)` | Establece la paginación | -| `group($columns, ...$params)` | Agrupa las filas (GROUP BY) | -| `having($condition, ...$params)`| Añade una condición HAVING para filtrar las filas agrupadas | - -Los métodos se pueden encadenar (la llamada [interfaz fluida |nette:introduction-to-object-oriented-programming#Interfaces fluidas]): `$table->where(...)->order(...)->limit(...)`. - -En estos métodos también puede usar las notaciones especiales para acceder a los [datos de tablas relacionadas |#Consultar a través de tablas relacionadas]. - - -Escapado e identificadores --------------------------- - -Los métodos escapan automáticamente los parámetros y entrecomillan los identificadores (nombres de tablas y columnas), lo que evita la inyección SQL. Para que funcione correctamente hay que seguir unas pocas reglas: - -- Escriba las palabras clave, los nombres de funciones, de procedimientos, etc. en **mayúsculas**. -- Escriba los nombres de columnas y tablas en **minúsculas**. -- Pase siempre las cadenas mediante **parámetros**. - -```php -where('name = ' . $name); // VULNERABILIDAD CRÍTICA: inyección SQL -where('name LIKE "%search%"'); // MAL: complica el entrecomillado automático -where('name LIKE ?', '%search%'); // BIEN: el valor se pasa como parámetro - -where('name like ?', $name); // MAL: genera: `name` `like` ? -where('name LIKE ?', $name); // BIEN: genera: `name` LIKE ? -where('LOWER(name) = ?', $value);// CORRECT: LOWER(`name`) = ? -``` - - -where(string|array $condition, ...$parameters): static .[method] ----------------------------------------------------------------- - -Filtra los resultados con condiciones WHERE. Su fuerza está en tratar de forma inteligente los distintos tipos de valores y elegir automáticamente los operadores SQL adecuados. - -Uso básico: - -```php -$table->where('id', $value); // WHERE `id` = 123 -$table->where('id > ?', $value); // WHERE `id` > 123 -$table->where('id = ? OR name = ?', $id, $name); // WHERE `id` = 1 OR `name` = 'Jon Snow' -``` - -Gracias a la detección automática del operador adecuado no tiene que ocuparse de los distintos casos especiales: Nette los resuelve por usted: - -```php -$table->where('id', 1); // WHERE `id` = 1 -$table->where('id', null); // WHERE `id` IS NULL -$table->where('id', [1, 2, 3]); // WHERE `id` IN (1, 2, 3) -// También puede usar el marcador ? sin operador: -$table->where('id ?', 1); // WHERE `id` = 1 -``` - -El método trata correctamente las condiciones negativas y los arrays vacíos: - -```php -$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- no encuentra nada -$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- lo encuentra todo -$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- lo encuentra todo -// $table->where('NOT id ?', $ids); // ATENCIÓN: esta sintaxis no está soportada -``` - -Como parámetro también puede pasar el resultado de otra consulta a una tabla, con lo que se crea una subconsulta: - -```php -// WHERE `id` IN (SELECT `id` FROM `tableName`) -$table->where('id', $explorer->table($tableName)); - -// WHERE `id` IN (SELECT `col` FROM `tableName`) -$table->where('id', $explorer->table($tableName)->select('col')); -``` - -Las condiciones también se pueden pasar como array, cuyos elementos se combinan con AND: - -```php -// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) -$table->where([ - 'price_final < price_original', - 'stock_count > min_stock', -]); -``` - -En el array puede usar pares clave => valor y Nette elegirá de nuevo automáticamente los operadores correctos: - -```php -// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3)) -$table->where([ - 'status' => 'active', - 'id' => [1, 2, 3], -]); -``` - -En el array puede combinar expresiones SQL con marcadores y varios parámetros. Esto es adecuado para condiciones complejas con operadores definidos con precisión: - -```php -// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) -$table->where([ - 'age > ?' => 18, - 'ROUND(score, ?) > ?' => [2, 75.5], // los dos parámetros se pasan como array -]); -``` - -Varias llamadas a `where()` combinan las condiciones automáticamente con AND. - - -whereOr(array $parameters): static .[method] --------------------------------------------- - -Parecido a `where()`, añade condiciones, pero las combina con OR: - -```php -// WHERE (`status` = 'active') OR (`deleted` = 1) -$table->whereOr([ - 'status' => 'active', - 'deleted' => true, -]); -``` - -Aquí también se pueden usar expresiones más complejas: - -```php -// WHERE (`price` > 1000) OR (`price_with_tax` > 1500) -$table->whereOr([ - 'price > ?' => 1000, - 'price_with_tax > ?' => 1500, -]); -``` - - -wherePrimary(mixed $key): static .[method] ------------------------------------------- - -Añade una condición sobre la clave primaria de la tabla: - -```php -// WHERE `id` = 123 -$table->wherePrimary(123); - -// WHERE `id` IN (1, 2, 3) -$table->wherePrimary([1, 2, 3]); -``` - -Si la tabla tiene una clave primaria compuesta (p. ej. `foo_id`, `bar_id`), pásela como array: - -```php -// WHERE `foo_id` = 1 AND `bar_id` = 5 -$table->wherePrimary(['foo_id' => 1, 'bar_id' => 5])->fetch(); - -// WHERE (`foo_id`, `bar_id`) IN ((1, 5), (2, 3)) -$table->wherePrimary([ - ['foo_id' => 1, 'bar_id' => 5], - ['foo_id' => 2, 'bar_id' => 3], -])->fetchAll(); -``` - - -order(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Indica el orden en el que se devuelven las filas. Puede ordenar por una o varias columnas, de forma ascendente o descendente, o según una expresión propia: - -```php -$table->order('created'); // ORDER BY `created` -$table->order('created DESC'); // ORDER BY `created` DESC -$table->order('priority DESC, created'); // ORDER BY `priority` DESC, `created` -$table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC -``` - - -select(string $columns, ...$parameters): static .[method] ---------------------------------------------------------- - -Indica las columnas que se devolverán de la base de datos. De forma predeterminada, Nette Database Explorer devuelve solo las columnas que realmente se usan en el código. Use el método `select()` cuando necesite obtener expresiones concretas: - -```php -// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` -$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); -``` - -Los alias definidos con `AS` son después accesibles como propiedades del objeto `ActiveRow`: - -```php -foreach ($table as $row) { - echo $row->formatted_date; // acceso al alias -} -``` - - -limit(?int $limit, ?int $offset = null): static .[method] ---------------------------------------------------------- - -Limita el número de filas devueltas (LIMIT) y opcionalmente permite fijar un desplazamiento: - -```php -$table->limit(10); // LIMIT 10 (devuelve las 10 primeras filas) -$table->limit(10, 20); // LIMIT 10 OFFSET 20 -``` - -Para la paginación es más adecuado usar el método `page()`. - - -page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] -------------------------------------------------------------------------- - -Facilita la paginación de los resultados. Acepta el número de página (empezando por 1) y el número de elementos por página. Opcionalmente puede pasar una referencia a una variable en la que se guardará el número total de páginas: - -```php -$numOfPages = null; -$table->page(page: 3, itemsPerPage: 10, numOfPages: $numOfPages); -echo "Total pages: $numOfPages"; -``` - - -group(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Agrupa las filas según las columnas indicadas (GROUP BY). Se usa normalmente junto con funciones de agregación: - -```php -// Cuenta el número de productos de cada categoría -$table->select('category_id, COUNT(*) AS count') - ->group('category_id'); -``` - - -having(string $having, ...$parameters): static .[method] --------------------------------------------------------- - -Establece una condición para filtrar las filas agrupadas (HAVING). Se puede usar junto con el método `group()` y funciones de agregación: - -```php -// Encuentra las categorías que tienen más de 100 productos -$table->select('category_id, COUNT(*) AS count') - ->group('category_id') - ->having('count > ?', 100); -``` - - -Leer los datos -============== - -Para leer datos de la base de datos hay disponibles varios métodos útiles: - -.[language-php] -| `foreach ($table as $key => $row)` | Recorre todas las filas; `$key` es el valor de la clave primaria, `$row` es un objeto ActiveRow | -| `$row = $table->get($key)` | Devuelve una sola fila por su clave primaria | -| `$row = $table->fetch()` | Devuelve la fila actual y avanza el puntero a la siguiente | -| `$array = $table->fetchPairs()` | Crea un array asociativo a partir de los resultados | -| `$array = $table->fetchAll()` | Devuelve todas las filas como array | -| `count($table)` | Devuelve el número de filas del objeto Selection | - -El objeto [ActiveRow |api:Nette\Database\Table\ActiveRow] es de solo lectura. Eso significa que no puede cambiar los valores de sus propiedades. Esta restricción asegura la consistencia de los datos y evita efectos secundarios inesperados. Los datos se cargan de la base de datos y cualquier cambio debe hacerse de forma explícita y controlada. - - -`foreach`: recorrer todas las filas ------------------------------------ - -La forma más fácil de ejecutar una consulta y obtener las filas es recorrerla con un bucle `foreach`. Ejecuta automáticamente la consulta SQL. - -```php -$books = $explorer->table('book'); -foreach ($books as $key => $book) { - // $key es el valor de la clave primaria, $book es ActiveRow - echo "$book->title ({$book->author->name})"; -} -``` - - -get($key): ?ActiveRow .[method] -------------------------------- - -Ejecuta la consulta SQL y devuelve la fila con la clave primaria dada, o `null` si no existe. - -```php -$book = $explorer->table('book')->get(123); // devuelve ActiveRow con ID 123 o null -if ($book) { - echo $book->title; -} -``` - - -fetch(): ?ActiveRow .[method] ------------------------------ - -Devuelve la fila actual y avanza el puntero interno a la siguiente. Si ya no hay más filas, devuelve `null`. - -```php -$books = $explorer->table('book'); -while ($book = $books->fetch()) { - $this->processBook($book); -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Devuelve los resultados como array asociativo. El primer argumento indica el nombre de la columna que se usará como clave del array y el segundo, el de la columna que se usará como valor: - -```php -$authors = $explorer->table('author')->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Si solo se indica el primer parámetro, el valor será la fila entera, es decir, el objeto `ActiveRow`: - -```php -$authors = $explorer->table('author')->fetchPairs('id'); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - -En caso de claves duplicadas se usa el valor de la última fila. Al usar `null` como clave, el array se indexa numéricamente empezando por cero (entonces no se producen colisiones): - -```php -$authors = $explorer->table('author')->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Alternativamente puede pasar como parámetro un callback que devuelva para cada fila un único valor o un par clave-valor. - -```php -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); -// ['First Book (John Novak)', ...] - -// El callback también puede devolver un array con un par clave y valor: -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => [$row->title, $row->author->name]); -// ['First Book' => 'John Novak', ...] -``` - - -fetchAll(): array .[method] ---------------------------- - -Devuelve todas las filas como array asociativo de objetos `ActiveRow`, donde las claves son los valores de la clave primaria. - -```php -$allBooks = $explorer->table('book')->fetchAll(); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - - -count(): int .[method] ----------------------- - -El método `count()` sin parámetro devuelve el número de filas del objeto `Selection`: - -```php -$table->where('category', 1); -$count = $table->count(); -$count = count($table); // alternativa -``` - -Nota: `count()` con un parámetro ejecuta la función de agregación COUNT en la base de datos, véase más abajo. - - -ActiveRow::toArray(): array .[method] -------------------------------------- - -Convierte el objeto `ActiveRow` en un array asociativo en el que las claves son los nombres de las columnas y los valores, los datos correspondientes. - -```php -$book = $explorer->table('book')->get(1); -$bookArray = $book->toArray(); -// $bookArray será ['id' => 1, 'title' => '...', 'author_id' => ..., ...] -``` - - -Agregación -========== - -La clase `Selection` ofrece métodos para ejecutar fácilmente funciones de agregación (COUNT, SUM, MIN, MAX, AVG, etc.). - -.[language-php] -| `count($expr)` | Cuenta el número de filas | -| `min($expr)` | Devuelve el valor mínimo de una columna | -| `max($expr)` | Devuelve el valor máximo de una columna | -| `sum($expr)` | Devuelve la suma de los valores de una columna | -| `aggregation($function)` | Permite cualquier función de agregación, como `AVG()` o `GROUP_CONCAT()` | - - -count(string $expr): int .[method] ----------------------------------- - -Ejecuta una consulta SQL con la función COUNT y devuelve el resultado. El método sirve para averiguar cuántas filas cumplen una determinada condición: - -```php -$count = $table->count('*'); // SELECT COUNT(*) FROM `table` -$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` -``` - -Nota: [#count()] sin parámetro devuelve solo el número de filas del objeto `Selection`. - - -min(string $expr) y max(string $expr) .[method] ------------------------------------------------ - -Los métodos `min()` y `max()` devuelven el valor mínimo y el máximo de la columna o la expresión indicada: - -```php -// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 -$maxPrice = $products->where('active', true) - ->max('price'); -``` - - -sum(string $expr): mixed .[method] ----------------------------------- - -Devuelve la suma de los valores de la columna o la expresión indicada: - -```php -// SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 -$totalPrice = $products->where('active', true) - ->sum('price * items_in_stock'); -``` - - -aggregation(string $function, ?string $groupFunction = null): mixed .[method] ------------------------------------------------------------------------------ - -Permite ejecutar cualquier función de agregación. - -```php -// precio medio de los productos de una categoría -$avgPrice = $products->where('category_id', 1) - ->aggregation('AVG(price)'); - -// une las etiquetas del producto en una sola cadena -$tags = $products->where('id', 1) - ->aggregation('GROUP_CONCAT(tag.name) AS tags') - ->fetch() - ->tags; -``` - -Si necesitamos agregar resultados que ya son a su vez el resultado de alguna función de agregación y de una agrupación (p. ej. `SUM(value)` sobre filas agrupadas), indicamos como segundo argumento la función de agregación que debe aplicarse a esos resultados intermedios: - -```php -// Calcula el precio total de los productos en stock de cada categoría y después suma esos precios. -$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') - ->group('category_id') - ->aggregation('SUM(category_total)', 'SUM'); -``` - -En este ejemplo calculamos primero el precio total de los productos de cada categoría (`SUM(price * stock) AS category_total`) y agrupamos los resultados por `category_id`. Después usamos `aggregation('SUM(category_total)', 'SUM')` para sumar esos totales intermedios `category_total`. El segundo argumento `'SUM'` indica que a los resultados intermedios debe aplicárseles la función SUM. - - -Insert, Update y Delete -======================= - -Nette Database Explorer simplifica insertar, actualizar y borrar datos. Todos los métodos mencionados lanzan una `Nette\Database\DriverException` en caso de error. - - -Selection::insert(iterable $data) .[method] -------------------------------------------- - -Inserta registros nuevos en la tabla. - -**Insertar un solo registro:** - -Pase el registro nuevo como array asociativo u objeto iterable (como el `ArrayHash` que se usa en los [formularios |forms:]), donde las claves corresponden a los nombres de las columnas de la tabla. - -Si la tabla tiene definida una clave primaria, el método devuelve un objeto `ActiveRow`, que se recarga de la base de datos para reflejar los cambios hechos a nivel de base de datos (triggers, valores por defecto de las columnas, cálculo de las columnas autoincrementales). Eso asegura la consistencia de los datos y hace que el objeto contenga siempre los datos actuales de la base de datos. Si la tabla no tiene clave primaria, no hay ninguna fila identificable y el método devuelve `null`. - -```php -$row = $explorer->table('users')->insert([ - 'name' => 'John Doe', - 'email' => 'john.doe@example.com', -]); -// $row es una instancia de ActiveRow y contiene los datos completos de la fila insertada, -// incluidos el ID generado automáticamente y los cambios hechos por los triggers -echo $row->id; // Imprime el ID del usuario recién insertado -echo $row->created_at; // Imprime la hora de creación si la estableció un trigger -``` - -**Insertar varios registros a la vez:** - -El método `insert()` permite insertar varios registros con una sola consulta SQL. En ese caso devuelve el número de filas insertadas. - -```php -$insertedRows = $explorer->table('users')->insert([ - [ - 'name' => 'John', - 'year' => 1994, - ], - [ - 'name' => 'Jack', - 'year' => 1995, - ], -]); -// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) -// $insertedRows será 2 -``` - -Como parámetro también se puede pasar un objeto `Selection` con una selección de datos. - -```php -$newUsers = $explorer->table('potential_users') - ->where('approved', 1) - ->select('name, email'); - -$insertedRows = $explorer->table('users')->insert($newUsers); -``` - -**Insertar valores especiales:** - -También podemos pasar como valores archivos, objetos `DateTime` o literales SQL: - -```php -$explorer->table('users')->insert([ - 'name' => 'John', - 'created_at' => new DateTime, // se convierte al formato de la base de datos - 'avatar' => fopen('image.jpg', 'rb'), // inserta el contenido binario del archivo - 'uuid' => $explorer::literal('UUID()'), // llama a la función UUID() -]); -``` - - -Selection::update(iterable $data): int .[method] ------------------------------------------------- - -Actualiza las filas de la tabla según el filtro indicado. Devuelve el número de filas realmente modificadas. - -Pase las columnas que hay que cambiar como array asociativo u objeto iterable (como el `ArrayHash` que se usa en los [formularios |forms:]), donde las claves corresponden a los nombres de las columnas de la tabla: - -```php -$affected = $explorer->table('users') - ->where('id', 10) - ->update([ - 'name' => 'John Smith', - 'year' => 1994, - ]); -// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 -``` - -Para cambiar valores numéricos puede usar los operadores `+=` y `-=`: - -```php -$explorer->table('users') - ->where('id', 10) - ->update([ - 'points+=' => 1, // aumenta en 1 el valor de la columna 'points' - 'coins-=' => 1, // reduce en 1 el valor de la columna 'coins' - ]); -// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 -``` - - -Selection::delete(): int .[method] ----------------------------------- - -Borra las filas de la tabla según el filtro indicado. Devuelve el número de filas borradas. - -```php -$count = $explorer->table('users') - ->where('id', 10) - ->delete(); -// DELETE FROM `users` WHERE `id` = 10 -``` - -.[caution] -Al llamar a `update()` o `delete()`, no olvide usar `where()` para indicar las filas que hay que modificar o borrar. ¡Si no se usa `where()`, la operación se realizará sobre toda la tabla! - - -ActiveRow::update(iterable $data): bool .[method] -------------------------------------------------- - -Actualiza los datos de la fila de la base de datos representada por el objeto `ActiveRow`. Acepta un iterable con los datos que hay que actualizar (las claves son los nombres de las columnas). Para cambiar valores numéricos puede usar los operadores `+=` y `-=`: - -Tras realizar la actualización, el `ActiveRow` se recarga automáticamente de la base de datos para reflejar los cambios hechos a nivel de base de datos (p. ej. triggers). El método devuelve `true` solo si se produjo un cambio real de datos. - -```php -$article = $explorer->table('article')->get(1); -$article->update([ - 'views += 1', // incrementa el contador de visitas -]); -echo $article->views; // Imprime el contador de visitas actual -``` - -Este método actualiza solo una fila concreta de la base de datos. Para actualizaciones masivas de varias filas use el método [#Selection::update()]. - - -ActiveRow::delete(): int .[method] ----------------------------------- - -Borra de la base de datos la fila representada por el objeto `ActiveRow`. Devuelve el número de filas borradas, que debería ser 1. - -```php -$book = $explorer->table('book')->get(1); -$book->delete(); // Borra el libro con ID 1 -``` - -Este método borra solo una fila concreta de la base de datos. Para borrados masivos de varias filas use el método [#Selection::delete()]. - - -Relaciones entre tablas -======================= - -En las bases de datos relacionales, los datos se reparten en varias tablas y se enlazan entre sí mediante claves foráneas. Nette Database Explorer ofrece una forma revolucionaria de trabajar con esas relaciones: sin escribir consultas JOIN y sin necesidad de configurar ni generar nada. - -Para ilustrar el trabajo con las relaciones usaremos una base de datos de libros de ejemplo ([la encontrará en GitHub |https://github.com/nette-examples/books]). En la base de datos tenemos las tablas: - -- `author`: escritores y traductores (columnas `id`, `name`, `web`, `born`) -- `book`: libros (columnas `id`, `author_id`, `translator_id`, `title`, `sequel_id`) -- `tag`: etiquetas (columnas `id`, `name`) -- `book_tag`: tabla de unión entre libros y etiquetas (columnas `book_id`, `tag_id`) - -[* db-schema-1-.webp *] *** Estructura de la base de datos usada en los ejemplos .<> - -En nuestra base de datos de libros de ejemplo encontramos varios tipos de relaciones (aunque el modelo está simplificado respecto a la realidad): - -- **Uno a muchos (1:N)**: cada libro **tiene un** autor; un autor puede escribir **varios** libros. -- **Cero a muchos (0:N)**: un libro **puede tener** traductor; un traductor puede traducir **varios** libros. -- **Cero a uno (0:1)**: un libro **puede tener** una continuación. -- **Muchos a muchos (M:N)**: un libro **puede tener varias** etiquetas y una etiqueta se puede asignar a **varios** libros. - -En estas relaciones siempre hay una **tabla padre** y una **tabla hija**. Por ejemplo, en la relación entre autores y libros, la tabla `author` es la padre y la tabla `book` es la hija; puede imaginárselo como que un libro siempre "pertenece" a un autor. Eso se refleja también en la estructura de la base de datos: la tabla hija `book` contiene la clave foránea `author_id`, que referencia a la tabla padre `author`. - -Si necesitamos listar los libros junto con los nombres de sus autores, tenemos dos opciones. O bien obtener los datos con una única consulta SQL usando JOIN: - -```sql -SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; -``` - -O bien obtener los datos en dos pasos, primero los libros y después sus autores, y luego juntarlos en PHP: - -```sql -SELECT * FROM book; -SELECT * FROM author WHERE id IN (1, 2, 3); -- IDs of authors from the selected books -``` - -El segundo enfoque es en realidad **más eficiente**, aunque pueda sorprender. Los datos se obtienen una sola vez y se pueden aprovechar mejor en la caché. Así es precisamente como funciona Nette Database Explorer: lo hace todo por debajo y le ofrece una API elegante: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo 'title: ' . $book->title; - echo 'written by: ' . $book->author->name; // $book->author es un registro de la tabla 'author' - echo 'translated by: ' . $book->translator?->name; -} -``` - - -Acceder a la tabla padre ------------------------- - -Acceder a la tabla padre es sencillo. Son relaciones del tipo *un libro tiene un autor* o *un libro puede tener traductor*. El registro relacionado se obtiene mediante una propiedad del objeto ActiveRow cuyo nombre corresponde al nombre de la columna de la clave foránea sin el sufijo `_id`: - -```php -$book = $explorer->table('book')->get(1); -echo $book->author->name; // encuentra el autor según la columna author_id -echo $book->translator?->name; // encuentra el traductor según la columna translator_id -``` - -Al acceder a la propiedad `$book->author`, Explorer busca en la tabla `book` una columna cuyo nombre contenga la cadena `author` (es decir, `author_id`). A partir del valor de esa columna carga el registro correspondiente de la tabla `author` y lo devuelve como `ActiveRow`. De forma parecida, `$book->translator` usa la columna `translator_id`. Como la columna `translator_id` puede contener `null`, en el código usamos el operador nullsafe `?->`. - -Un enfoque alternativo lo ofrece el método `ref()`, que acepta dos argumentos, el nombre de la tabla de destino y el nombre de la columna de unión, y devuelve una instancia de `ActiveRow` o `null`: - -```php -echo $book->ref('author', 'author_id')->name; // relación con el autor -echo $book->ref('author', 'translator_id')->name; // relación con el traductor -``` - -El método `ref()` es útil si no se puede usar el acceso por propiedad, por ejemplo porque la tabla contiene una columna con ese mismo nombre (es decir, `author`). En los demás casos se recomienda usar el acceso por propiedad, por su mejor legibilidad. - -Explorer optimiza automáticamente las consultas a la base de datos. Cuando recorremos los libros en un bucle y accedemos a sus registros relacionados (autores, traductores), Explorer no genera una consulta para cada libro por separado. En su lugar ejecuta solo **una consulta SELECT por cada tipo de relación**, lo que reduce notablemente la carga de la base de datos. Por ejemplo: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo $book->title . ': '; - echo $book->author->name; - echo $book->translator?->name; -} -``` - -Este código ejecuta solo estas tres consultas rapidísimas a la base de datos: - -```sql -SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- IDs from the author_id column of selected books -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- IDs from the translator_id column of selected books -``` - -.[note] -La lógica para encontrar la columna de unión la determina la implementación de [Conventions |api:Nette\Database\Conventions]. Recomendamos usar [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], que analiza las claves foráneas y le permite trabajar con facilidad con las relaciones existentes entre tablas. - - -Acceder a la tabla hija ------------------------ - -Acceder a la tabla hija funciona en sentido contrario. Ahora preguntamos *qué libros escribió este autor* o *qué libros tradujo este traductor*. Para este tipo de consulta usamos el método `related()`, que devuelve un `Selection` con los registros relacionados. Veamos un ejemplo: - -```php -$author = $explorer->table('author')->get(1); - -// Imprime todos los libros del autor -foreach ($author->related('book.author_id') as $book) { - echo "Wrote: $book->title"; -} - -// Imprime todos los libros traducidos por el autor -foreach ($author->related('book.translator_id') as $book) { - echo "Translated: $book->title"; -} -``` - -El método `related()` acepta la descripción de la unión como un solo argumento con notación de punto o como dos argumentos separados: - -```php -$author->related('book.translator_id'); // un solo argumento -$author->related('book', 'translator_id'); // dos argumentos -``` - -Explorer puede detectar automáticamente la columna de unión correcta a partir del nombre de la tabla padre. En este caso une por la columna `book.author_id`, porque el nombre de la tabla de origen es `author`: - -```php -$author->related('book'); // usa book.author_id -``` - -Si existen varias uniones posibles, Explorer lanzará una [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -Naturalmente, podemos usar el método `related()` al recorrer varios registros en un bucle, y Explorer optimizará también en ese caso las consultas automáticamente: - -```php -$authors = $explorer->table('author'); -foreach ($authors as $author) { - echo $author->name . ' wrote:'; - foreach ($author->related('book') as $book) { - echo $book->title; - } -} -``` - -Este código genera solo dos consultas SQL rapidísimas: - -```sql -SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- IDs of the selected authors -``` - - -Relación muchos a muchos ------------------------- - -Para una relación muchos a muchos (M:N) hace falta una **tabla de unión** (en nuestro caso, `book_tag`) que contiene dos columnas de clave foránea (`book_id`, `tag_id`). Cada una de esas columnas se refiere a la clave primaria de una de las tablas enlazadas. Para obtener los datos relacionados obtenemos primero los registros de la tabla de unión con `related('book_tag')` y después pasamos a los datos de destino: - -```php -$book = $explorer->table('book')->get(1); -// imprime los nombres de las etiquetas asignadas al libro -foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name; // imprime el nombre de la etiqueta a través de la tabla de unión -} - -$tag = $explorer->table('tag')->get(1); -// o al revés: imprime los nombres de los libros marcados con esta etiqueta -foreach ($tag->related('book_tag') as $bookTag) { - echo $bookTag->book->title; // imprime el título del libro -} -``` - -Explorer optimiza de nuevo las consultas SQL en una forma eficiente: - -```sql -SELECT * FROM `book`; -SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- IDs of the selected books -SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- IDs of the tags found in book_tag -``` - - -Consultar a través de tablas relacionadas ------------------------------------------ - -En los métodos `where()`, `select()`, `order()` y `group()` puede usar notaciones especiales para acceder a columnas de otras tablas. Explorer crea automáticamente los JOIN necesarios. - -La **notación con punto** (`tabla_padre.columna`) se usa para las relaciones 1:N desde la perspectiva de la tabla hija: - -```php -$books = $explorer->table('book'); - -// Encuentra los libros cuyo autor tiene un nombre que empieza por 'Jon' -$books->where('author.name LIKE ?', 'Jon%'); - -// Ordena los libros por el nombre del autor de forma descendente -$books->order('author.name DESC'); - -// Imprime el título del libro y el nombre del autor -$books->select('book.title, author.name'); -``` - -La **notación con dos puntos** (`:tabla_hija.columna`) se usa para las relaciones 1:N desde la perspectiva de la tabla padre: - -```php -$authors = $explorer->table('author'); - -// Encuentra los autores que escribieron un libro con 'PHP' en el título -$authors->where(':book.title LIKE ?', '%PHP%'); - -// Cuenta el número de libros de cada autor -$authors->select('*, COUNT(:book.id) AS book_count') - ->group('author.id'); -``` - -En el ejemplo anterior con la notación de dos puntos (`:book.title`) no se indica la columna de la clave foránea. Explorer detecta automáticamente la columna correcta a partir del nombre de la tabla padre. En este caso une por la columna `book.author_id`, porque el nombre de la tabla de origen es `author`. Si existen varias uniones posibles, Explorer lanzará una [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -La columna de unión se puede indicar explícitamente entre paréntesis: - -```php -// Encuentra los autores que tradujeron un libro con 'PHP' en el título -$authors->where(':book(translator_id).title LIKE ?', '%PHP%'); -``` - -Las notaciones se pueden encadenar para acceder a datos de varias tablas: - -```php -// Encuentra los autores de los libros etiquetados con 'PHP' -$authors->where(':book:book_tag.tag.name', 'PHP') - ->group('author.id'); -``` - - -Ampliar las condiciones del JOIN --------------------------------- - -El método `joinWhere()` amplía las condiciones indicadas al unir tablas en SQL tras la palabra clave `ON`. - -Digamos que queremos encontrar los libros traducidos por un traductor concreto: - -```php -// Encuentra los libros traducidos por un traductor llamado 'David' -$books = $explorer->table('book') - ->joinWhere('translator', 'translator.name', 'David'); -// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') -``` - -En la condición de `joinWhere()` puede usar las mismas construcciones que en el método `where()`: operadores, marcadores, arrays de valores o expresiones SQL. - -Para consultas más complejas con varios JOIN puede definir alias de tablas: - -```php -$tags = $explorer->table('tag') - ->joinWhere(':book_tag.book.author', 'book_author.born < ?', 1950) - ->alias(':book_tag.book.author', 'book_author'); -// LEFT JOIN `book_tag` ON `tag`.`id` = `book_tag`.`tag_id` -// LEFT JOIN `book` ON `book_tag`.`book_id` = `book`.`id` -// LEFT JOIN `author` `book_author` ON `book`.`author_id` = `book_author`.`id` -// AND (`book_author`.`born` < 1950) -``` - -Fíjese en que, mientras el método `where()` añade condiciones a la cláusula `WHERE`, el método `joinWhere()` amplía las condiciones de la cláusula `ON` al unir las tablas. diff --git a/database/es/guide.texy b/database/es/guide.texy deleted file mode 100644 index 4ba6820b71..0000000000 --- a/database/es/guide.texy +++ /dev/null @@ -1,218 +0,0 @@ -Nette Database -************** - -.[perex] -Nette Database es una capa de base de datos potente y elegante para PHP, centrada en la sencillez y en las funciones inteligentes. Ofrece dos formas de trabajar con la base de datos: el [Explorer |explorer] para desarrollar aplicaciones rápidamente, o el [enfoque SQL |SQL way] para manipular las consultas directamente. - -<div class="grid gap-3"> -<div> - - -[Enfoque SQL|sql-way] -===================== -- consultas seguras y parametrizadas -- control preciso sobre la estructura de la consulta SQL -- cuando se escriben consultas complejas con funciones avanzadas -- optimizar el rendimiento con funciones SQL concretas - -</div> - -<div> - - -[Explorer |explorer] -==================== -- desarrollar rápido sin escribir SQL -- manejo intuitivo de las relaciones entre tablas -- aprovechar la optimización automática de las consultas -- adecuado para un trabajo rápido y cómodo con la base de datos - -</div> - -</div> - - -Instalación -=========== - -Descargue e instale la biblioteca con [Composer|best-practices:composer]: - -```shell -composer require nette/database -``` - - -Bases de datos compatibles -========================== - -Nette Database soporta las siguientes bases de datos: - -|* Servidor de base de datos |* Nombre DSN |* Soporte en Explorer -|-----------------------|--------------|-----------------------| -| MySQL (>= 5.1) | mysql | SÍ | -| PostgreSQL (>= 9.0) | pgsql | SÍ | -| SQLite 3 (>= 3.8) | sqlite | SÍ | -| Oracle | oci | NO | -| MS SQL (PDO_SQLSRV) | sqlsrv | SÍ | -| MS SQL (PDO_DBLIB) | mssql | NO | -| ODBC | odbc | NO | - - -Dos enfoques para trabajar con la base de datos -=============================================== - -Nette Database le da a elegir: puede escribir las consultas SQL directamente (enfoque SQL) o dejar que se generen automáticamente (Explorer). Veamos cómo resuelven ambos enfoques las mismas tareas: - -[Enfoque SQL|sql-way] - consultas SQL - -```php -// Inserta un registro -$database->query('INSERT INTO books', [ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// Obtiene registros: autores de libros -$result = $database->query(' - SELECT authors.*, COUNT(books.id) AS books_count - FROM authors - LEFT JOIN books ON authors.id = books.author_id - WHERE authors.active = 1 - GROUP BY authors.id -'); - -// Muestra (no es óptimo, genera N consultas adicionales) -foreach ($result as $author) { - $books = $database->query(' - SELECT * FROM books - WHERE author_id = ? - ORDER BY published_at DESC - ', $author->id); - - echo "Author $author->name has written $author->books_count books:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -[Enfoque Explorer|explorer] - generación automática de SQL - -```php -// Inserta un registro -$database->table('books')->insert([ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// Obtiene registros: autores de libros -$authors = $database->table('authors') - ->where('active', 1); - -// Muestra (genera automáticamente solo 2 consultas optimizadas) -foreach ($authors as $author) { - $books = $author->related('books') - ->order('published_at DESC'); - - echo "Author $author->name has written {$books->count()} books:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -El enfoque Explorer genera y optimiza las consultas SQL automáticamente. En el ejemplo anterior, el enfoque SQL genera N+1 consultas (una para los autores y luego una para los libros de cada autor), mientras que Explorer optimiza las consultas automáticamente y ejecuta solo dos: una para los autores y otra para todos sus libros. - -Ambos enfoques se pueden combinar libremente en su aplicación según haga falta. - - -Conexión y configuración -======================== - -Para conectarse a la base de datos basta con crear una instancia de la clase [api:Nette\Database\Connection]: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password); -``` - -El parámetro `$dsn` (Data Source Name) es el mismo que [usa PDO |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], p. ej. `host=127.0.0.1;dbname=test`. En caso de fallo lanza una `Nette\Database\ConnectionException`. - -Un método más cómodo lo ofrece, sin embargo, la [configuración de la aplicación |configuration], donde solo hay que añadir una sección `database`. Eso crea los objetos necesarios y también un panel de base de datos en la barra de [Tracy |tracy:]. - -```neon -database: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password -``` - -El objeto de la conexión se puede después [obtener como servicio del contenedor DI |dependency-injection:passing-dependencies], p. ej.: - -```php -class Model -{ - public function __construct( - // o Nette\Database\Explorer - private Nette\Database\Connection $database, - ) { - } -} -``` - -Más información sobre la [configuración de la base de datos|configuration]. - - -Creación manual de Explorer ---------------------------- - -Si no usa el contenedor DI de Nette, puede crear una instancia de `Nette\Database\Explorer` manualmente: - -```php -// conexión a la base de datos -$connection = new Nette\Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); -// almacenamiento de caché, implementa Nette\Caching\Storage, p. ej.: -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); -// se ocupa de la reflexión de la estructura de la base de datos -$structure = new Nette\Database\Structure($connection, $storage); -// define las reglas de mapeo de nombres de tablas, columnas y claves foráneas -$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); -$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage); -``` - - -Gestión de la conexión -====================== - -Al crear un objeto `Connection`, la conexión se establece automáticamente. Si quiere retrasarla, use el modo lazy: actívelo en la [configuración|configuration] con `lazy`, o así: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => true]); -``` - -Para gestionar la conexión use los métodos `connect()`, `disconnect()` y `reconnect()`. -- `connect()` crea la conexión si todavía no existe y puede lanzar una `Nette\Database\ConnectionException`. -- `disconnect()` cierra la conexión actual a la base de datos. -- `reconnect()` cierra la conexión y vuelve a conectarse a la base de datos. Este método también puede lanzar una `Nette\Database\ConnectionException`. - -Además puede vigilar los eventos relacionados con la conexión mediante el evento `onConnect`, que es un array de callbacks que se llaman después de establecer la conexión con la base de datos. - -```php -// se ejecuta tras conectar con la base de datos -$database->onConnect[] = function($database) { - echo "Connected to the database"; -}; -``` - -El evento `onQuery` funciona de forma parecida: es un array de callbacks invocados después de cada consulta ejecutada (y cuando una consulta falla), útil para el registro o el perfilado. - - -Barra de depuración de Tracy -============================ - -Si usa [Tracy |tracy:], el panel Database de la Debug Bar se activa automáticamente. Muestra todas las consultas ejecutadas, sus parámetros, el tiempo de ejecución y el lugar del código desde el que se llamaron. - -[* db-panel.webp *] diff --git a/database/es/reflection.texy b/database/es/reflection.texy deleted file mode 100644 index 1f761d220a..0000000000 --- a/database/es/reflection.texy +++ /dev/null @@ -1,127 +0,0 @@ -Reflexión de la estructura -************************** - -.{data-version:3.2.1} -Nette Database ofrece herramientas para introspeccionar la estructura de la base de datos mediante la clase [api:Nette\Database\Reflection]. Permite obtener información sobre las tablas, las columnas, los índices y las claves foráneas. Puede usar la reflexión para generar esquemas, crear aplicaciones flexibles que trabajen con la base de datos o herramientas generales de bases de datos. - -El objeto de reflexión se obtiene de la instancia de la conexión a la base de datos: - -```php -$reflection = $database->getReflection(); -``` - - -Obtener las tablas ------------------- - -La propiedad de solo lectura `$reflection->tables` contiene un array asociativo de todas las tablas de la base de datos: - -```php -// Lista los nombres de todas las tablas -foreach ($reflection->tables as $name => $table) { - echo $name . "\n"; -} -``` - -Hay disponibles otros dos métodos: - -```php -// Comprueba la existencia de una tabla -if ($reflection->hasTable('users')) { - echo "Table users exists"; -} - -// Devuelve el objeto de la tabla; lanza una excepción si no existe -$table = $reflection->getTable('users'); -``` - - -Información de la tabla ------------------------ - -Una tabla está representada por el objeto [Table|api:Nette\Database\Reflection\Table], que ofrece las siguientes propiedades de solo lectura: - -- `$name: string`: nombre de la tabla -- `$view: bool`: si es una vista -- `$fullName: ?string`: nombre completo de la tabla, incluido el esquema (si existe) -- `$columns: array<string, Column>`: array asociativo de las columnas de la tabla -- `$indexes: Index[]`: array de los índices de la tabla -- `$primaryKey: ?Index`: clave primaria de la tabla, o null -- `$foreignKeys: ForeignKey[]`: array de las claves foráneas de la tabla -- `$comment: ?string`: comentario de la tabla - - -Columnas --------- - -La propiedad `columns` de la tabla ofrece un array asociativo de columnas en el que la clave es el nombre de la columna y el valor es una instancia de [Column|api:Nette\Database\Reflection\Column] con estas propiedades: - -- `$name: string`: nombre de la columna -- `$table: ?Table`: referencia a la tabla de la columna -- `$nativeType: string`: tipo nativo de la base de datos -- `$size: ?int`: tamaño o longitud del tipo -- `$nullable: bool`: si la columna puede contener NULL -- `$default: mixed`: valor por defecto de la columna -- `$autoIncrement: bool`: si la columna es autoincremental -- `$primary: bool`: si forma parte de la clave primaria -- `$vendor: array`: metadatos adicionales específicos del sistema de base de datos -- `$comment: ?string`: comentario de la columna - -```php -foreach ($table->columns as $name => $column) { - echo "Column: $name\n"; - echo "Type: {$column->nativeType}\n"; - echo "Nullable: " . ($column->nullable ? 'Yes' : 'No') . "\n"; -} -``` - - -Índices -------- - -La propiedad `indexes` de la tabla ofrece un array de índices en el que cada índice es una instancia de [Index|api:Nette\Database\Reflection\Index] con estas propiedades: - -- `$columns: Column[]`: array de las columnas que forman el índice -- `$unique: bool`: si el índice es único -- `$primary: bool`: si es una clave primaria -- `$name: ?string`: nombre del índice - -La clave primaria de la tabla se puede obtener con la propiedad `primaryKey`, que devuelve un objeto `Index` o `null` si la tabla no tiene clave primaria. - -```php -// Lista los índices -foreach ($table->indexes as $index) { - $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); - echo "Index" . ($index->name ? " {$index->name}" : '') . ":\n"; - echo " Columns: $columns\n"; - echo " Unique: " . ($index->unique ? 'Yes' : 'No') . "\n"; -} - -// Lista la clave primaria -if ($primaryKey = $table->primaryKey) { - $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); - echo "Primary Key: $columns\n"; -} -``` - - -Claves foráneas ---------------- - -La propiedad `foreignKeys` de la tabla ofrece un array de claves foráneas en el que cada clave foránea es una instancia de [ForeignKey|api:Nette\Database\Reflection\ForeignKey] con estas propiedades: - -- `$foreignTable: Table`: la tabla referenciada -- `$localColumns: Column[]`: array de las columnas locales -- `$foreignColumns: Column[]`: array de las columnas referenciadas -- `$name: string`: nombre de la clave foránea - -```php -// Lista las claves foráneas -foreach ($table->foreignKeys as $fk) { - $localCols = implode(', ', array_map(fn($col) => $col->name, $fk->localColumns)); - $foreignCols = implode(', ', array_map(fn($col) => $col->name, $fk->foreignColumns)); - - echo "FK" . ($fk->name ? " {$fk->name}" : '') . ":\n"; - echo " $localCols -> {$fk->foreignTable->name}($foreignCols)\n"; -} -``` diff --git a/database/es/security.texy b/database/es/security.texy deleted file mode 100644 index 46d4b0550c..0000000000 --- a/database/es/security.texy +++ /dev/null @@ -1,185 +0,0 @@ -Riesgos de seguridad -******************** - -<div class=perex> - -Las bases de datos contienen a menudo datos sensibles y permiten realizar operaciones peligrosas. Para trabajar con Nette Database de forma segura es crucial: - -- entender la diferencia entre las API seguras y las inseguras -- usar consultas parametrizadas -- validar correctamente los datos de entrada - -</div> - - -¿Qué es la inyección SQL? -========================= - -La inyección SQL es el riesgo de seguridad más grave al trabajar con bases de datos. Se produce cuando una entrada de usuario sin sanear pasa a formar parte de una consulta SQL. Un atacante puede insertar sus propios comandos SQL y con ello: -- obtener acceso no autorizado a los datos -- modificar o borrar datos de la base de datos -- saltarse la autenticación - -```php -// ❌ CÓDIGO PELIGROSO - vulnerable a la inyección SQL -$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); - -// Un atacante podría introducir un valor como: ' OR '1'='1 -// La consulta resultante sería: SELECT * FROM users WHERE name = '' OR '1'='1' -// Que devuelve todos los usuarios -``` - -Lo mismo vale para Database Explorer: - -```php -// ❌ CÓDIGO PELIGROSO - vulnerable a la inyección SQL -$table->where('name = ' . $_GET['name']); -$table->where("name = '$_GET[name]'"); -``` - - -Consultas parametrizadas -======================== - -La defensa fundamental contra la inyección SQL son las consultas parametrizadas. Nette Database ofrece varias formas de usarlas. - -La más sencilla es usar **marcadores con signo de interrogación**: - -```php -// ✅ Consulta parametrizada segura -$database->query('SELECT * FROM users WHERE name = ?', $name); - -// ✅ Condición segura en Explorer -$table->where('name = ?', $name); -``` - -Esto vale para todos los demás métodos de [Database Explorer|explorer] que permiten insertar expresiones con marcadores de interrogación y parámetros. - -Para los comandos INSERT y UPDATE o para la cláusula WHERE podemos pasar los valores en un array: - -```php -// ✅ INSERT seguro -$database->query('INSERT INTO users', [ - 'name' => $name, - 'email' => $email, -]); - -// ✅ INSERT seguro en Explorer -$table->insert([ - 'name' => $name, - 'email' => $email, -]); -``` - - -Validación de los valores de los parámetros -=========================================== - -Las consultas parametrizadas son la piedra angular del trabajo seguro con la base de datos. Pero los valores que insertamos en ellas deben pasar por varios niveles de comprobación: - - -Comprobación del tipo ---------------------- - -**Lo más importante es asegurar el tipo de dato correcto de los parámetros**; es una condición necesaria para usar Nette Database con seguridad. La base de datos da por hecho que todos los datos de entrada tienen el tipo de dato correcto, el que corresponde a cada columna. - -Por ejemplo, si `$name` en los ejemplos anteriores fuera inesperadamente un array en lugar de una cadena, Nette Database intentaría insertar todos sus elementos en la consulta SQL, lo que provocaría un error. Por eso, **nunca use** datos sin validar de `$_GET`, `$_POST` o `$_COOKIE` directamente en las consultas a la base de datos. - - -Validación del formato ----------------------- - -En el segundo nivel comprobamos el formato de los datos: por ejemplo, si las cadenas están en codificación UTF-8 y su longitud corresponde a la definición de la columna, o si los valores numéricos están dentro del rango permitido para el tipo de dato de esa columna. - -Para este nivel de validación podemos apoyarnos en parte en la propia base de datos: muchas rechazan los datos no válidos. Pero el comportamiento puede variar; algunas pueden truncar en silencio las cadenas largas o recortar los números que se salen del rango. - - -Validación específica del dominio ---------------------------------- - -El tercer nivel son las comprobaciones lógicas específicas de su aplicación. Por ejemplo, verificar que los valores de los desplegables corresponden a las opciones ofrecidas, que los números están dentro del rango esperado (p. ej. una edad de 0 a 150 años) o que las dependencias mutuas entre valores tienen sentido. - - -Métodos de validación recomendados ----------------------------------- - -- Use [Nette Forms|forms:], que aseguran automáticamente la validación correcta de todas las entradas. -- Use [presenters|application:] e indique los tipos de dato de los parámetros en los métodos `action*()` y `render*()`. -- O implemente su propia capa de validación con las herramientas estándar de PHP, como `filter_var()`. - - -Trabajo seguro con las columnas -=============================== - -En la sección anterior hemos mostrado cómo validar correctamente los valores de los parámetros. Pero, al usar arrays en las consultas SQL, hay que prestar la misma atención a sus claves. - -```php -// ❌ CÓDIGO PELIGROSO - las claves del array no están saneadas -$database->query('INSERT INTO users', $_POST); -``` - -En los comandos INSERT y UPDATE esto es un fallo de seguridad crítico: un atacante puede insertar o modificar cualquier columna de la base de datos. Podría, por ejemplo, poner `is_admin = 1` o insertar datos arbitrarios en columnas sensibles (la llamada Mass Assignment Vulnerability). - -En las condiciones WHERE es aún más peligroso, porque pueden contener operadores: - -```php -// ❌ CÓDIGO PELIGROSO - las claves del array no están saneadas -$_POST['salary >'] = 100000; -$database->query('SELECT * FROM users WHERE', $_POST); -// ejecuta la consulta WHERE (`salary` > 100000) -``` - -Un atacante puede usar este enfoque para descubrir sistemáticamente los salarios de los empleados. Podría empezar, por ejemplo, con una consulta de los salarios superiores a 100 000, luego los inferiores a 50 000 y, estrechando el rango poco a poco, revelar los salarios aproximados de todos los empleados. A este tipo de ataque se le llama enumeración SQL. - -Los métodos `where()` y `whereOr()` son incluso [mucho más flexibles |explorer#where()] y admiten expresiones SQL, incluidos operadores y funciones, tanto en las claves como en los valores. Eso le da al atacante la posibilidad de realizar una inyección SQL: - -```php -// ❌ CÓDIGO PELIGROSO - el atacante puede insertar su propio SQL -$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1']; -$table->where($_POST); -// ejecuta la consulta WHERE (0) UNION SELECT name, salary FROM users WHERE (1) -``` - -Este ataque termina la condición original con `0)`, añade su propio `SELECT` mediante `UNION` para obtener datos sensibles de la tabla `users` y cierra la consulta de forma sintácticamente correcta con `WHERE (1)`. - - -Lista blanca de columnas ------------------------- - -Para trabajar con seguridad con los nombres de las columnas necesitamos un mecanismo que asegure que el usuario solo puede trabajar con las columnas permitidas y no puede añadir las suyas. Podríamos intentar detectar y bloquear los nombres de columna peligrosos (lista negra), pero ese enfoque no es fiable: un atacante siempre puede idear una nueva forma de escribir un nombre de columna peligroso que no habíamos previsto. - -Por eso es mucho más seguro invertir la lógica y definir una lista explícita de columnas permitidas (lista blanca): - -```php -// Columnas que el usuario puede modificar -$allowedColumns = ['name', 'email', 'active']; - -// Elimina de la entrada todas las columnas no autorizadas -$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); - -// ✅ Ahora es seguro usarlo en consultas, como: -$database->query('INSERT INTO users', $filteredData); -$table->update($filteredData); -$table->where($filteredData); -``` - - -Identificadores dinámicos -========================= - -Para los nombres dinámicos de tablas y columnas use el marcador `?name`. Este asegura el escapado correcto de los identificadores según la sintaxis de cada base de datos (p. ej. con acentos graves en MySQL): - -```php -// ✅ Uso seguro de identificadores de confianza -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name', $column, $table); -// Resultado en MySQL: SELECT `name` FROM `users` -``` - -Importante: use el símbolo `?name` solo para valores de confianza definidos en el código de la aplicación. Para los valores que vienen del usuario, use de nuevo una [lista blanca |#Lista blanca de columnas]. De lo contrario se expone a riesgos de seguridad: - -```php -// ❌ PELIGROSO - nunca use la entrada del usuario -$database->query('SELECT ?name FROM users', $_GET['column']); -``` diff --git a/database/es/sql-way.texy b/database/es/sql-way.texy deleted file mode 100644 index 1cf378fa36..0000000000 --- a/database/es/sql-way.texy +++ /dev/null @@ -1,513 +0,0 @@ -Enfoque SQL -*********** - -.[perex] -Nette Database ofrece dos formas de trabajar: puede escribir usted mismo las consultas SQL (enfoque SQL) o dejar que se generen automáticamente (véase [Explorer |explorer]). El enfoque SQL le da control total sobre las consultas y a la vez garantiza que se construyan de forma segura. - -.[note] -Los detalles sobre la conexión y la configuración de la base de datos los encontrará en el capítulo [Conexión y configuración |guide#Conexión y configuración]. - - -Consultas básicas -================= - -Para consultar la base de datos se usa el método `query()`. Devuelve un objeto [ResultSet |api:Nette\Database\ResultSet], que representa el resultado de la consulta. Si la consulta falla, el método [lanza una excepción|exceptions]. Puede recorrer el resultado de la consulta con un bucle `foreach` o usar alguno de los [métodos auxiliares |#Obtener los datos]. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; -} -``` - -Para insertar valores en las consultas SQL de forma segura, use consultas parametrizadas. Nette Database lo hace extremadamente sencillo: basta con añadir una coma y el valor después de la consulta SQL: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Con varios parámetros tiene dos opciones. Puede intercalar la consulta SQL con los parámetros: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age); -``` - -O escribir primero la consulta SQL entera y añadir después todos los parámetros: - -```php -$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); -``` - - -Protección contra la inyección SQL -================================== - -¿Por qué es importante usar consultas parametrizadas? Porque le protegen de un ataque llamado inyección SQL, en el que un atacante podría inyectar sus propios comandos SQL y con ello acceder a los datos de la base de datos o dañarlos. - -.[warning] -**¡Nunca inserte variables directamente en una consulta SQL!** Use siempre consultas parametrizadas, que le protegen de la inyección SQL. - -```php -// ❌ CÓDIGO PELIGROSO - vulnerable a la inyección SQL -$database->query("SELECT * FROM users WHERE name = '$name'"); - -// ✅ Consulta parametrizada segura -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Familiarícese con los [posibles riesgos de seguridad |security]. - - -Técnicas de consulta -==================== - - -Condiciones WHERE ------------------ - -Las condiciones `WHERE` se pueden escribir como un array asociativo en el que las claves son los nombres de las columnas y los valores son los datos con los que comparar. Nette Database elige automáticamente el operador SQL más adecuado según el tipo del valor. - -```php -$database->query('SELECT * FROM users WHERE', [ - 'name' => 'John', - 'active' => true, -]); -// WHERE `name` = 'John' AND `active` = 1 -``` - -También puede indicar explícitamente el operador de comparación en la clave: - -```php -$database->query('SELECT * FROM users WHERE', [ - 'age >' => 25, // usa el operador > - 'name LIKE' => '%John%', // usa el operador LIKE - 'email NOT LIKE' => '%example.com%', // usa el operador NOT LIKE -]); -// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' -``` - -Nette gestiona automáticamente los casos especiales, como los valores `null` o los arrays. - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name' => 'Laptop', // usa el operador = - 'category_id' => [1, 2, 3], // usa IN - 'description' => null, // usa IS NULL -]); -// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL -``` - -Para las condiciones negativas use el operador `NOT`: - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // usa el operador != - 'category_id NOT' => [1, 2, 3], // usa NOT IN - 'description NOT' => null, // usa IS NOT NULL - 'id NOT' => [], // se omite -]); -// WHERE `name` != 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL -``` - -De forma predeterminada, las condiciones se unen con el operador `AND`. Se puede cambiar con el [marcador ?or |#Marcadores para construir SQL]. - - -Reglas de ORDER BY ------------------- - -La cláusula `ORDER BY` se puede escribir con un array. Indique las columnas en las claves y use un valor booleano para señalar el orden ascendente (`true`) o descendente (`false`): - -```php -$database->query('SELECT id FROM author ORDER BY', [ - 'id' => true, // ascendente - 'name' => false, // descendente -]); -// SELECT id FROM author ORDER BY `id`, `name` DESC -``` - - -Insertar datos (INSERT) ------------------------ - -Para insertar registros se usa el comando SQL `INSERT`. - -```php -$values = [ - 'name' => 'John Doe', - 'email' => 'john@example.com', -]; -$database->query('INSERT INTO users ?', $values); -$userId = $database->getInsertId(); -``` - -El método `getInsertId()` devuelve el ID de la última fila insertada. En algunas bases de datos (p. ej. PostgreSQL) hay que indicar como parámetro el nombre de la secuencia de la que debe generarse el ID, con `$database->getInsertId($sequenceId)`. - -Como parámetros también puede pasar [#Valores especiales], como archivos, objetos DateTime o tipos enum. - -Insertar varios registros a la vez: - -```php -$database->query('INSERT INTO users ?', [ - ['name' => 'User 1', 'email' => 'user1@mail.com'], - ['name' => 'User 2', 'email' => 'user2@mail.com'], -]); -``` - -Un INSERT de varios registros es mucho más rápido, porque se ejecuta una sola consulta a la base de datos en lugar de muchas individuales. - -**Nota de seguridad:** nunca use datos sin validar como `$values`. Familiarícese con los [posibles riesgos |security#Trabajo seguro con las columnas]. - - -Actualizar datos (UPDATE) -------------------------- - -Para actualizar registros se usa el comando SQL `UPDATE`. - -```php -// Actualiza un solo registro -$values = [ - 'name' => 'John Smith', -]; -$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); -``` - -El número de filas afectadas lo devuelve `$result->getRowCount()`. - -En `UPDATE` podemos usar los operadores `+=` y `-=`: - -```php -$database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // incrementa login_count -], 1); -``` - -Ejemplo de insertar o actualizar un registro si ya existe. Usamos la técnica `ON DUPLICATE KEY UPDATE`: - -```php -$values = [ - 'name' => $name, - 'year' => $year, -]; -$database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', - $values + ['id' => $id], - $values, -); -// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978) -// ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 -``` - -Fíjese en que Nette Database reconoce el contexto en el que se usa un parámetro de tipo array dentro del comando SQL y construye el código SQL en consecuencia. Así, del primer array construyó `(id, name, year) VALUES (123, 'Jim', 1978)`, mientras que el segundo lo convirtió en la forma `name = 'Jim', year = 1978`. Lo tratamos con más detalle en la sección [#Marcadores para construir SQL]. - - -Borrar datos (DELETE) ---------------------- - -Para borrar registros se usa el comando SQL `DELETE`. Ejemplo de cómo obtener el número de filas borradas: - -```php -$count = $database->query('DELETE FROM users WHERE id = ?', 1) - ->getRowCount(); -``` - - -Marcadores para construir SQL ------------------------------ - -Un marcador es un símbolo especial en una consulta SQL que indica cómo debe convertirse el valor del parámetro en una expresión SQL: - -| Marcador | Descripción | Se usa automáticamente en -|-----------|-------------------------------------------------|----------------------------- -| `?name` | Sirve para insertar nombres de tablas o columnas | - -| `?values` | Genera `(clave, ...) VALUES (valor, ...)` | `INSERT ... ?`, `REPLACE ... ?` -| `?set` | Genera asignaciones `clave = valor, ...` | `SET ?`, `KEY UPDATE ?` -| `?and` | Une las condiciones de un array con `AND` | `WHERE ?`, `HAVING ?` -| `?or` | Une las condiciones de un array con `OR` | - -| `?order` | Genera la cláusula `ORDER BY` | `ORDER BY ?`, `GROUP BY ?` - -El marcador `?name` sirve para insertar dinámicamente nombres de tablas y columnas en la consulta. Nette Database se encarga del entrecomillado correcto de los identificadores según las convenciones de la base de datos (p. ej. encerrándolos entre acentos graves en MySQL). - -```php -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); -// SELECT `name` FROM `users` WHERE id = 1 (en MySQL) -``` - -**Atención:** use el marcador `?name` solo para nombres de tablas y columnas validados. De lo contrario se arriesga a sufrir [vulnerabilidades de seguridad |security#Identificadores dinámicos]. - -Los demás marcadores no suelen hacer falta indicarlos, porque Nette usa una autodetección inteligente al construir la consulta SQL (véase la tercera columna de la tabla). Pero puede usarlos, por ejemplo, en una situación en la que quiera unir las condiciones con `OR` en lugar de `AND`: - -```php -$database->query('SELECT * FROM users WHERE ?or', [ - 'name' => 'John', - 'email' => 'john@example.com', -]); -// SELECT * FROM users WHERE `name` = 'John' OR `email` = 'john@example.com' -``` - - -Valores especiales ------------------- - -Además de los tipos escalares habituales (string, int, bool), como parámetros también puede pasar valores especiales: - -- archivos: `fopen('image.gif', 'r')` inserta el contenido binario del archivo -- fecha y hora: los objetos `DateTimeInterface` se convierten al formato de la base de datos -- tipos enum: las instancias de `enum` se convierten a su valor -- literales SQL: creados con `Connection::literal('NOW()')`, se insertan directamente en la consulta - -```php -$database->query('INSERT INTO articles ?', [ - 'title' => 'My Article', - 'published_at' => new DateTimeImmutable, // o new DateTime - 'content' => fopen('image.png', 'r'), - 'state' => Status::Draft, -]); -``` - -En las bases de datos que no tienen soporte nativo para el tipo de dato `datetime` (como SQLite y Oracle), los objetos `DateTime` y `DateTimeImmutable` se convierten a un valor indicado en la [configuración de la base de datos|configuration] mediante el elemento `formatDateTime` (el valor por defecto es `U`, el timestamp de Unix). - - -Literales SQL -------------- - -En algunos casos necesita pasar como valor código SQL en bruto, que no debe tratarse como cadena ni escaparse. Para eso sirven los objetos de la clase `Nette\Database\SqlLiteral`. Se crean con el método `Connection::literal()`. - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - 'year >' => $database::literal('YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) -``` - -O alternativamente: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) -``` - -Los literales SQL pueden contener parámetros: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > ? AND year < ?', $min, $max), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) -``` - -Eso permite combinaciones interesantes: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('?or', [ - 'active' => true, - 'role' => $role, - ]), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (`active` = 1 OR `role` = 'admin') -``` - - -Obtener los datos -================= - - -Atajos para las consultas SELECT --------------------------------- - -Para simplificar la obtención de datos, `Connection` ofrece varios atajos que combinan una llamada a `query()` con la llamada posterior a `fetch*()`. Estos métodos aceptan los mismos parámetros que `query()`, es decir, una consulta SQL y parámetros opcionales. La descripción completa de los métodos `fetch*()` la encontrará [más abajo |#fetch()]. - -| `fetch($sql, ...$params): ?Row` | Ejecuta la consulta y devuelve la primera fila como objeto `Row`, o `null`. -| `fetchAll($sql, ...$params): array` | Ejecuta la consulta y devuelve todas las filas como array de objetos `Row`. -| `fetchPairs($sql, ...$params): array` | Ejecuta la consulta y devuelve un array asociativo (pares clave => valor). -| `fetchField($sql, ...$params): mixed` | Ejecuta la consulta y devuelve el valor de la primera columna de la primera fila. -| `fetchList($sql, ...$params): ?array` | Ejecuta la consulta y devuelve la primera fila como array indexado, o `null`. - -Ejemplo: - -```php -// fetchField() - devuelve el valor de la primera celda -$count = $database->query('SELECT COUNT(*) FROM articles') - ->fetchField(); -``` - - -`foreach`: recorrer las filas ------------------------------ - -Tras ejecutar una consulta se devuelve un objeto [ResultSet|api:Nette\Database\ResultSet], que permite recorrer los resultados de varias maneras. La forma más fácil de ejecutar una consulta y obtener las filas es recorrerla con un bucle `foreach`. Este método es el más eficiente en memoria, porque obtiene los datos fila a fila y no carga todo el conjunto de resultados en memoria de una vez. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; - // ... -} -``` - -.[note] -El `ResultSet` solo se puede recorrer una vez. Si necesita recorrerlo repetidamente, primero tiene que cargar los datos en un array, por ejemplo con el método `fetchAll()`. - - -fetch(): ?Row .[method] ------------------------ - -Devuelve una fila como objeto `Row`. Si ya no hay más filas, devuelve `null`. Avanza el puntero interno a la fila siguiente. - -```php -$result = $database->query('SELECT * FROM users'); -$row = $result->fetch(); // carga la primera fila -if ($row) { - echo $row->name; -} -``` - - -fetchAll(): array .[method] ---------------------------- - -Devuelve todas las filas restantes del `ResultSet` como array de objetos `Row`. - -```php -$result = $database->query('SELECT * FROM users'); -$rows = $result->fetchAll(); // carga todas las filas -foreach ($rows as $row) { - echo $row->name; -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Devuelve el conjunto de resultados como array asociativo. El primer argumento indica la columna que se usará como claves y el segundo, la que se usará como valores: - -```php -$result = $database->query('SELECT id, name FROM users'); -$names = $result->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Si solo se indica el primer parámetro (`$key`), como valor se usará la fila entera (el objeto `Row`): - -```php -$rows = $result->fetchPairs('id'); -// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...] -``` - -En caso de claves duplicadas se usa el valor de la última fila. Usar `null` como clave da un array indexado numéricamente (empezando desde cero), lo que evita las colisiones de claves: - -```php -$names = $result->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Alternativamente puede indicar un callback que procese cada fila. El callback puede devolver un único valor o un par clave-valor. - -```php -$result = $database->query('SELECT * FROM users'); -$items = $result->fetchPairs(fn($row) => "$row->id - $row->name"); -// ['1 - John', '2 - Jane', ...] - -// El callback también puede devolver un array con un par clave y valor: -$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); -// ['John' => 46, 'Jane' => 21, ...] -``` - - -fetchField(): mixed .[method] ------------------------------ - -Devuelve el valor de la primera columna de la fila actual. Si ya no hay más filas, devuelve `null`. Avanza el puntero interno a la fila siguiente. - -```php -$result = $database->query('SELECT name FROM users'); -$name = $result->fetchField(); // carga el nombre de la primera fila -``` - - -fetchList(): ?array .[method] ------------------------------ - -Devuelve la fila como array indexado. Si ya no hay más filas, devuelve `null`. Avanza el puntero interno a la fila siguiente. - -```php -$result = $database->query('SELECT name, email FROM users'); -$row = $result->fetchList(); // ['John', 'john@example.com'] -``` - - -getRowCount(): ?int .[method] ------------------------------ - -Devuelve el número de filas afectadas por la última consulta `UPDATE` o `DELETE`. En las consultas `SELECT` devuelve el número de filas del conjunto de resultados. Eso, sin embargo, no siempre se puede saber, y en ese caso el método devuelve `null`. - - -getColumnCount(): ?int .[method] --------------------------------- - -Devuelve el número de columnas del `ResultSet`. - - -Información sobre la consulta -============================= - -Para depurar podemos obtener información sobre la última consulta ejecutada: - -```php -echo $database->getLastQueryString(); // imprime la consulta SQL - -$result = $database->query('SELECT * FROM articles'); -echo $result->getQueryString(); // imprime la consulta SQL -echo $result->getTime(); // imprime el tiempo de ejecución en segundos -``` - -Para mostrar el resultado como tabla HTML puede usar: - -```php -$result = $database->query('SELECT * FROM articles'); -$result->dump(); -``` - -`ResultSet` ofrece información sobre los tipos de las columnas: - -```php -$result = $database->query('SELECT * FROM articles'); -$types = $result->getColumnTypes(); - -foreach ($types as $column => $type) { - echo "$column is of type $type"; // p. ej. 'id is of type int' -} -``` - - -Registro de las consultas -------------------------- - -Podemos implementar nuestro propio registro de consultas. El evento `onQuery` es un array de callbacks que se llaman después de cada consulta ejecutada: - -```php -$database->onQuery[] = function ($database, $result) use ($logger) { - $logger->info('Query: ' . $result->getQueryString()); - $logger->info('Time: ' . $result->getTime()); - - if ($result->getRowCount() > 1000) { - $logger->warning('Large result set: ' . $result->getRowCount() . ' rows'); - } -}; -``` diff --git a/database/es/transactions.texy b/database/es/transactions.texy deleted file mode 100644 index f1640331a1..0000000000 --- a/database/es/transactions.texy +++ /dev/null @@ -1,45 +0,0 @@ -Transacciones -************* - -.[perex] -Las transacciones garantizan que, o se ejecutan todas las operaciones de la transacción, o no se ejecuta ninguna. Son útiles para asegurar la consistencia de los datos en operaciones complejas. - -La forma más sencilla de usar transacciones es esta: - -```php -$database->beginTransaction(); -try { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); - $database->commit(); -} catch (\Exception $e) { - $database->rollBack(); - throw $e; -} -``` - -El mismo resultado se consigue de forma mucho más elegante con el método `transaction()`. Acepta un callback que se ejecuta dentro de la transacción. Si el callback se ejecuta sin excepción, la transacción se confirma automáticamente. Si se produce una excepción, la transacción se revierte y la excepción se propaga hacia arriba. - -```php -$database->transaction(function ($database) use ($id) { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); -}); -``` - -Las llamadas a `transaction()` se pueden anidar, lo que facilita componer métodos que gestionan cada uno su propia transacción. Solo la transacción más externa se envía realmente a la base de datos como `BEGIN`/`COMMIT`; las llamadas internas se limitan a llevar la cuenta del nivel de anidamiento. Llamar manualmente a `beginTransaction()`, `commit()` o `rollBack()` dentro de un callback de `transaction()` lanza una `LogicException`. - -El método `transaction()` también puede devolver valores: - -```php -$count = $database->transaction(function ($database) { - $result = $database->query('UPDATE users SET active = ?', true); - return $result->getRowCount(); // devuelve el número de filas actualizadas -}); -``` diff --git a/database/es/type-conversion.texy b/database/es/type-conversion.texy deleted file mode 100644 index 25e3dd8ebc..0000000000 --- a/database/es/type-conversion.texy +++ /dev/null @@ -1,55 +0,0 @@ -Conversión de tipos -******************* - -.[perex] -Nette Database convierte automáticamente los valores devueltos por la base de datos a los tipos de PHP correspondientes. - - -Fecha y hora ------------- - -Los valores temporales se convierten a objetos `Nette\Utils\DateTime`. Si quiere que se conviertan a objetos inmutables `Nette\Database\DateTime`, active la opción `newDateTime` en la [configuración |configuration]. - -```php -$row = $database->fetch('SELECT created_at FROM articles'); -echo $row->created_at instanceof DateTime; // true -echo $row->created_at->format('j. n. Y'); -``` - -En el caso de MySQL, el tipo de dato `TIME` se convierte a objetos `DateInterval`. - - -Valores booleanos ------------------ - -Los valores booleanos se convierten automáticamente a `true` o `false`. En MySQL se convierte `TINYINT(1)` si activamos `convertBoolean` en la [configuración |configuration]. - -```php -$row = $database->fetch('SELECT is_published FROM articles'); -echo gettype($row->is_published); // 'boolean' -``` - - -Valores numéricos ------------------ - -Los valores numéricos se convierten a `int` o `float` según el tipo de la columna en la base de datos: - -```php -$row = $database->fetch('SELECT id, price FROM products'); -echo gettype($row->id); // integer -echo gettype($row->price); // float -``` - - -Normalización propia --------------------- - -Con el método `setRowNormalizer(?callable $normalizer)` puede establecer una función propia para transformar las filas que llegan de la base de datos. Es útil, por ejemplo, para la conversión automática de tipos de dato. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // aquí ocurre la conversión de tipos - return $row; -}); -``` diff --git a/database/es/upgrading.texy b/database/es/upgrading.texy deleted file mode 100644 index ac811ee6a0..0000000000 --- a/database/es/upgrading.texy +++ /dev/null @@ -1,36 +0,0 @@ -Actualización -************* - - -Actualización a la versión 3.2 -============================== - -La versión mínima de PHP requerida es la 8.1. - -El código se ha afinado cuidadosamente para PHP 8.1. Se han añadido todos los nuevos type hints de métodos y propiedades. Los cambios son menores: - -- MySQL: la fecha cero `0000-00-00` se devuelve como `null` -- MySQL: un decimal sin decimales se devuelve como int en lugar de float -- el tipo `time` se devuelve como objeto `DateTime` con la fecha puesta a `0001-01-01` en lugar de la fecha actual - - -Actualización a la versión 3.1 -============================== - -- la clase `Nette\Database\Context` se ha renombrado a `Nette\Database\Explorer` por coherencia con el nombre [Database Explorer|explorer] -- las interfaces `Nette\Database\IRow` y `Nette\Database\IRowContainer` están marcadas como obsoletas por innecesarias -- el driver `MySqlDriver` usa subconsultas -- el traductor de sentencias SQL controla mejor dónde se pueden pasar arrays - - -Actualización a la versión 3.0 -============================== - -Algunos métodos, como `fetch()` o `fetchField()`, devuelven ahora `null` en lugar de `false` cuando no hay una fila siguiente. - - -Actualización a la versión 2.3 -============================== - -- `MySqlDriver` usa de forma predeterminada la codificación `utf8mb4` para MySQL >= 5.5.3 en lugar de `utf8` -- `IReflection` se ha dividido en las interfaces gemelas `IStructure` e `IConventions` diff --git a/database/files/db-schema-1-.webp b/database/files/db-schema-1-.webp deleted file mode 100644 index 6bd9b0598d..0000000000 Binary files a/database/files/db-schema-1-.webp and /dev/null differ diff --git a/database/fr/@home.texy b/database/fr/@home.texy deleted file mode 100644 index 071520fd83..0000000000 --- a/database/fr/@home.texy +++ /dev/null @@ -1,18 +0,0 @@ -Bases de données prises en charge -================================= - -Ces serveurs de bases de données sont pris en charge : - -|* Serveur de base de données |* Nom DSN |* Prise en charge Core |* Prise en charge Explorer -| MySQL (>= 5.1) | mysql | OUI | OUI -| PostgreSQL (>= 9.0) | pgsql | OUI | OUI -| Sqlite 3 (>= 3.8) | sqlite | OUI | OUI -| Oracle | oci | OUI | - -| MS SQL (PDO_SQLSRV) | sqlsrv | OUI | OUI -| MS SQL (PDO_DBLIB) | mssql | OUI | - -| ODBC | odbc | OUI | - - - - -{{maintitle: Nette Database - une superbe couche de base de données pour PHP}} -{{description: Nette Database simplifie considérablement la récupération des données depuis la base sans écrire de requêtes SQL. Elle exécute des requêtes efficaces et ne transfère pas de données inutiles.}} diff --git a/database/fr/@left-menu.texy b/database/fr/@left-menu.texy deleted file mode 100644 index 941ea60671..0000000000 --- a/database/fr/@left-menu.texy +++ /dev/null @@ -1,21 +0,0 @@ -Nette Database -************** -- [Premiers pas |guide] -- [Approche SQL|sql-way] -- [Explorer|explorer] -- [Transactions|transactions] -- [Exceptions|exceptions] -- [Réflexion|reflection] -- [Conversion de types |type-conversion] -- [Configuration|configuration] -- [Risques de sécurité |security] -- [Mise à niveau|upgrading] - - -Pour aller plus loin -******************** -- [Documentation Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Bonnes pratiques |best-practices:] -- [Résolution de problèmes |nette:troubleshooting] diff --git a/database/fr/@meta.texy b/database/fr/@meta.texy deleted file mode 100644 index 72ae4b8db8..0000000000 --- a/database/fr/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentation Nette}} diff --git a/database/fr/configuration.texy b/database/fr/configuration.texy deleted file mode 100644 index b90d0e9d24..0000000000 --- a/database/fr/configuration.texy +++ /dev/null @@ -1,110 +0,0 @@ -Configuration de la base de données -*********************************** - -.[perex] -Aperçu des options de configuration de Nette Database. - -Si vous n'utilisez pas tout le framework, mais seulement cette bibliothèque, lisez [comment charger la configuration|bootstrap:]. - - -Connexion unique ----------------- - -Configuration d'une seule connexion à la base de données : - -```neon -database: - # DSN, la seule clé obligatoire - dsn: "sqlite:%appDir%/Model/demo.db" - user: ... - password: ... -``` - -Cela crée les services `Nette\Database\Connection` et `Nette\Database\Explorer`, qui sont généralement passés par [autowiring |dependency-injection:autowiring] ou en se référant à [leur nom |#Services DI]. - -Autres réglages : - -```neon -database: - # afficher le panneau de la base de données dans la Tracy Bar ? - debugger: ... # (bool) activé par défaut si Tracy est actif - - # afficher l'EXPLAIN des requêtes dans la Tracy Bar ? - explain: ... # (bool) true par défaut - - # activer l'autowiring pour cette connexion ? - autowired: ... # (bool) true par défaut pour la première connexion - - # conventions de tables : discovered, static, ou nom de classe - conventions: discovered # (string) 'discovered' par défaut - - options: - # ne se connecter à la base que lorsque c'est nécessaire ? - lazy: ... # (bool) false par défaut - - # classe du driver de base de données PHP - driverClass: # (string) - - # MySQL uniquement : définit sql_mode - sqlmode: # (string) - - # MySQL uniquement : définit SET NAMES - charset: # (string) 'utf8mb4' par défaut - - # MySQL uniquement : convertit TINYINT(1) en bool - convertBoolean: # (bool) false par défaut - - # renvoie les colonnes de date comme objets immuables (depuis la version 3.2.1) - newDateTime: # (bool) false par défaut - - # Oracle et SQLite uniquement : format de stockage de la date - formatDateTime: # (string) 'U' par défaut -``` - -La clé `options` peut contenir d'autres options décrites dans la [documentation du driver PDO |https://www.php.net/manual/en/pdo.drivers.php], par exemple : - -```neon -database: - options: - PDO::MYSQL_ATTR_COMPRESS: true -``` - - -Connexions multiples --------------------- - -Dans la configuration, nous pouvons définir plusieurs connexions à des bases de données en les répartissant dans des sections nommées : - -```neon -database: - main: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password - - another: - dsn: 'sqlite::memory:' -``` - -L'autowiring n'est activé que pour les services de la première section. Cela peut être changé à l'aide d'`autowired: false` ou d'`autowired: true`. - - -Services DI ------------ - -Ces services sont ajoutés au conteneur DI, où `###` représente le nom de la connexion : - -| Nom | Type | Description -|---------------------------|---------------------------------|--------------------------- -| `database.###.connection` | [api:Nette\Database\Connection] | connexion à la base de données -| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |explorer] - - -Si nous ne définissons qu'une seule connexion, les noms des services seront `database.default.connection` et `database.default.explorer`. Si nous définissons plusieurs connexions comme dans l'exemple ci-dessus, les noms correspondront aux sections, c'est-à-dire `database.main.connection`, `database.main.explorer`, ainsi que `database.another.connection` et `database.another.explorer`. - -Nous passons les services non autowirés explicitement, en nous référant à leur nom : - -```neon -services: - - UserFacade(@database.another.connection) -``` diff --git a/database/fr/exceptions.texy b/database/fr/exceptions.texy deleted file mode 100644 index 6fa8ef384a..0000000000 --- a/database/fr/exceptions.texy +++ /dev/null @@ -1,37 +0,0 @@ -Exceptions -********** - -Nette Database utilise une hiérarchie d'exceptions. La classe de base est `Nette\Database\DriverException`, qui étend `PDOException` et offre des fonctionnalités enrichies pour travailler avec les erreurs de base de données : - -- La méthode `getDriverCode()` renvoie le code d'erreur du driver de base de données. -- La méthode `getSqlState()` renvoie le code SQLSTATE. -- Les méthodes `getQueryString()` et `getParameters()` permettent d'obtenir la requête d'origine et ses paramètres. - -La classe `DriverException` est étendue par les exceptions spécialisées suivantes : - -- `ConnectionException` - signale un échec de connexion au serveur de base de données. - - `ConnectionLostException` .{data-version:3.2.9} - la connexion a été perdue au cours d'une opération (redémarrage du serveur, panne réseau, idle timeout) ; une reconnexion est nécessaire avant toute utilisation ultérieure. -- `ConstraintViolationException` - la classe de base des violations de contraintes de la base de données, dont héritent les exceptions suivantes : - - `ForeignKeyConstraintViolationException` - violation d'une contrainte de clé étrangère. - - `NotNullConstraintViolationException` - violation d'une contrainte NOT NULL. - - `UniqueConstraintViolationException` - violation d'une contrainte d'unicité. - - `CheckConstraintViolationException` .{data-version:3.2.9} - violation d'une contrainte CHECK. -- `DeadlockException` .{data-version:3.2.9} - deadlock ou conflit de sérialisation détecté par le serveur ; la transaction a été annulée et peut être retentée. -- `LockTimeoutException` .{data-version:3.2.9} - le délai d'attente d'un verrou a été dépassé ; l'instruction a été interrompue, mais la transaction environnante reste généralement ouverte. - -L'exemple suivant montre comment attraper une `UniqueConstraintViolationException`, qui survient lorsqu'on tente d'insérer un utilisateur avec une adresse e-mail déjà présente dans la base (en supposant que la colonne `email` porte un index unique) : - -```php -try { - $database->query('INSERT INTO users', [ - 'email' => 'john@example.com', - 'name' => 'John Doe', - 'password' => $hashedPassword, - ]); -} catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'Un utilisateur avec cet e-mail existe déjà.'; - -} catch (Nette\Database\DriverException $e) { - echo 'Une erreur est survenue lors de l\'inscription : ' . $e->getMessage(); -} -``` diff --git a/database/fr/explorer.texy b/database/fr/explorer.texy deleted file mode 100644 index 01cb9e92f6..0000000000 --- a/database/fr/explorer.texy +++ /dev/null @@ -1,912 +0,0 @@ -Database Explorer -***************** - -<div class=perex> - -Explorer offre une façon intuitive et efficace de travailler avec votre base de données. Il gère automatiquement les relations entre les tables et optimise les requêtes, ce qui vous permet de vous concentrer sur la logique de votre application. Il fonctionne immédiatement, sans configuration. Si vous avez besoin du contrôle total sur les requêtes SQL, vous pouvez utiliser l'[approche SQL |SQL way]. - -- Le travail avec les données est naturel et facile à comprendre -- Il génère des requêtes SQL optimisées qui ne récupèrent que les données nécessaires -- Il donne un accès simple aux données liées sans avoir à écrire de requêtes JOIN -- Il fonctionne immédiatement, sans configuration ni génération d'entités - -</div> - - -Le travail avec Explorer commence par l'appel de la méthode `table()` sur l'objet [api:Nette\Database\Explorer] (voir [Connexion et configuration |guide#Connexion et configuration] pour les détails de la mise en place de la connexion) : - -```php -$books = $explorer->table('book'); // 'book' est le nom de la table -``` - -La méthode renvoie un objet [Selection |api:Nette\Database\Table\Selection], qui représente une requête SQL. D'autres méthodes peuvent être chaînées sur cet objet pour filtrer et trier les résultats. La requête n'est assemblée et exécutée qu'au moment où les données sont demandées, par exemple lors d'un parcours en `foreach`. Chaque ligne est représentée par un objet [ActiveRow |api:Nette\Database\Table\ActiveRow] : - -```php -foreach ($books as $book) { - echo $book->title; // affiche la colonne 'title' - echo $book->author_id; // affiche la colonne 'author_id' -} -``` - -Explorer simplifie énormément le travail avec les [relations entre les tables |#Relations entre les tables]. L'exemple suivant montre avec quelle facilité nous pouvons afficher des données de tables liées (les livres et leurs auteurs). Remarquez qu'aucune requête JOIN n'a besoin d'être écrite ; Nette les génère pour nous : - -```php -$books = $explorer->table('book'); - -foreach ($books as $book) { - echo 'Livre : ' . $book->title; - echo 'Auteur : ' . $book->author->name; // crée un JOIN vers la table 'author' -} -``` - -Nette Database Explorer optimise les requêtes pour une efficacité maximale. L'exemple ci-dessus n'exécute que deux requêtes SELECT, que nous traitions 10 ou 10 000 livres. - -De plus, Explorer suit les colonnes utilisées dans le code et ne récupère que celles-là depuis la base, ce qui améliore encore les performances. Ce comportement est totalement automatique et adaptatif. Si vous modifiez plus tard le code pour utiliser d'autres colonnes, Explorer ajuste automatiquement les requêtes. Vous n'avez rien à configurer ni à réfléchir aux colonnes qui seront nécessaires : laissez cela à Nette. - - -Filtrer et trier -================ - -La classe `Selection` fournit des méthodes pour filtrer et trier les sélections de données. - -.[language-php] -| `where($condition, ...$params)` | Ajoute une condition WHERE. Plusieurs conditions sont combinées par AND | -| `whereOr(array $conditions)` | Ajoute un groupe de conditions WHERE combinées par OR | -| `wherePrimary($value)` | Ajoute une condition WHERE sur la clé primaire | -| `order($columns, ...$params)` | Définit le tri avec ORDER BY | -| `select($columns, ...$params)` | Indique quelles colonnes récupérer | -| `limit($limit, $offset = null)` | Limite le nombre de lignes (LIMIT) et fixe éventuellement l'OFFSET | -| `page($page, $itemsPerPage, &$numOfPages = null)` | Met en place la pagination | -| `group($columns, ...$params)` | Groupe les lignes (GROUP BY) | -| `having($condition, ...$params)`| Ajoute une condition HAVING pour filtrer les lignes groupées | - -Les méthodes peuvent être chaînées (ce qu'on appelle une [interface fluide |nette:introduction-to-object-oriented-programming#Interfaces fluides]) : `$table->where(...)->order(...)->limit(...)`. - -Dans ces méthodes, vous pouvez aussi utiliser des notations particulières pour accéder aux [données des tables liées |#Requêter à travers les tables liées]. - - -Échappement et identifiants ---------------------------- - -Les méthodes échappent automatiquement les paramètres et mettent les identifiants (noms de tables et de colonnes) entre quotes, ce qui empêche l'injection SQL. Pour que cela fonctionne correctement, quelques règles doivent être respectées : - -- Écrivez les mots-clés, les noms de fonctions, de procédures, etc. en **majuscules**. -- Écrivez les noms de colonnes et de tables en **minuscules**. -- Passez toujours les chaînes par des **paramètres**. - -```php -where('name = ' . $name); // FAILLE CRITIQUE : injection SQL -where('name LIKE "%search%"'); // MAUVAIS : complique la mise entre quotes automatique -where('name LIKE ?', '%search%'); // CORRECT : la valeur est passée en paramètre - -where('name like ?', $name); // MAUVAIS : génère : `name` `like` ? -where('name LIKE ?', $name); // CORRECT : génère : `name` LIKE ? -where('LOWER(name) = ?', $value);// CORRECT : LOWER(`name`) = ? -``` - - -where(string|array $condition, ...$parameters): static .[method] ----------------------------------------------------------------- - -Filtre les résultats à l'aide de conditions WHERE. Sa force réside dans le traitement intelligent des différents types de valeurs et le choix automatique des opérateurs SQL appropriés. - -Usage de base : - -```php -$table->where('id', $value); // WHERE `id` = 123 -$table->where('id > ?', $value); // WHERE `id` > 123 -$table->where('id = ? OR name = ?', $id, $name); // WHERE `id` = 1 OR `name` = 'Jon Snow' -``` - -Grâce à la détection automatique de l'opérateur approprié, vous n'avez pas à traiter les différents cas particuliers : Nette les résout pour vous : - -```php -$table->where('id', 1); // WHERE `id` = 1 -$table->where('id', null); // WHERE `id` IS NULL -$table->where('id', [1, 2, 3]); // WHERE `id` IN (1, 2, 3) -// Vous pouvez aussi utiliser le placeholder ? sans opérateur : -$table->where('id ?', 1); // WHERE `id` = 1 -``` - -La méthode traite correctement les conditions négatives et les tableaux vides : - -```php -$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- ne trouve rien -$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- trouve tout -$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- trouve tout -// $table->where('NOT id ?', $ids); // ATTENTION : cette syntaxe n'est pas prise en charge -``` - -Vous pouvez aussi passer comme paramètre le résultat d'une autre requête sur une table, ce qui crée une sous-requête : - -```php -// WHERE `id` IN (SELECT `id` FROM `tableName`) -$table->where('id', $explorer->table($tableName)); - -// WHERE `id` IN (SELECT `col` FROM `tableName`) -$table->where('id', $explorer->table($tableName)->select('col')); -``` - -Les conditions peuvent aussi être passées sous forme de tableau, dont les éléments sont combinés par AND : - -```php -// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) -$table->where([ - 'price_final < price_original', - 'stock_count > min_stock', -]); -``` - -Dans le tableau, vous pouvez utiliser des paires clé => valeur, et Nette choisira là encore automatiquement les bons opérateurs : - -```php -// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3)) -$table->where([ - 'status' => 'active', - 'id' => [1, 2, 3], -]); -``` - -Dans le tableau, vous pouvez combiner des expressions SQL avec des placeholders et plusieurs paramètres. Cela convient aux conditions complexes avec des opérateurs précisément définis : - -```php -// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) -$table->where([ - 'age > ?' => 18, - 'ROUND(score, ?) > ?' => [2, 75.5], // deux paramètres sont passés sous forme de tableau -]); -``` - -Des appels répétés à `where()` combinent automatiquement les conditions par AND. - - -whereOr(array $parameters): static .[method] --------------------------------------------- - -Comme `where()`, cette méthode ajoute des conditions, mais les combine par OR : - -```php -// WHERE (`status` = 'active') OR (`deleted` = 1) -$table->whereOr([ - 'status' => 'active', - 'deleted' => true, -]); -``` - -Des expressions plus complexes peuvent aussi être utilisées ici : - -```php -// WHERE (`price` > 1000) OR (`price_with_tax` > 1500) -$table->whereOr([ - 'price > ?' => 1000, - 'price_with_tax > ?' => 1500, -]); -``` - - -wherePrimary(mixed $key): static .[method] ------------------------------------------- - -Ajoute une condition sur la clé primaire de la table : - -```php -// WHERE `id` = 123 -$table->wherePrimary(123); - -// WHERE `id` IN (1, 2, 3) -$table->wherePrimary([1, 2, 3]); -``` - -Si la table a une clé primaire composite (par exemple `foo_id`, `bar_id`), passez-la sous forme de tableau : - -```php -// WHERE `foo_id` = 1 AND `bar_id` = 5 -$table->wherePrimary(['foo_id' => 1, 'bar_id' => 5])->fetch(); - -// WHERE (`foo_id`, `bar_id`) IN ((1, 5), (2, 3)) -$table->wherePrimary([ - ['foo_id' => 1, 'bar_id' => 5], - ['foo_id' => 2, 'bar_id' => 3], -])->fetchAll(); -``` - - -order(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Indique l'ordre dans lequel les lignes sont renvoyées. Vous pouvez trier par une ou plusieurs colonnes, en ordre croissant ou décroissant, ou selon une expression personnalisée : - -```php -$table->order('created'); // ORDER BY `created` -$table->order('created DESC'); // ORDER BY `created` DESC -$table->order('priority DESC, created'); // ORDER BY `priority` DESC, `created` -$table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC -``` - - -select(string $columns, ...$parameters): static .[method] ---------------------------------------------------------- - -Indique les colonnes à renvoyer depuis la base de données. Par défaut, Nette Database Explorer ne renvoie que les colonnes réellement utilisées dans le code. Utilisez la méthode `select()` lorsque vous avez besoin de récupérer des expressions précises : - -```php -// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` -$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); -``` - -Les alias définis à l'aide d'`AS` sont ensuite accessibles comme propriétés de l'objet `ActiveRow` : - -```php -foreach ($table as $row) { - echo $row->formatted_date; // accès à l'alias -} -``` - - -limit(?int $limit, ?int $offset = null): static .[method] ---------------------------------------------------------- - -Limite le nombre de lignes renvoyées (LIMIT) et permet éventuellement de fixer un décalage : - -```php -$table->limit(10); // LIMIT 10 (renvoie les 10 premières lignes) -$table->limit(10, 20); // LIMIT 10 OFFSET 20 -``` - -Pour la pagination, il est plus approprié d'utiliser la méthode `page()`. - - -page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] -------------------------------------------------------------------------- - -Facilite la pagination des résultats. Elle accepte le numéro de page (à partir de 1) et le nombre d'éléments par page. Vous pouvez éventuellement passer une référence vers une variable où sera stocké le nombre total de pages : - -```php -$numOfPages = null; -$table->page(page: 3, itemsPerPage: 10, numOfPages: $numOfPages); -echo "Nombre total de pages : $numOfPages"; -``` - - -group(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Groupe les lignes selon les colonnes indiquées (GROUP BY). Elle s'utilise généralement avec des fonctions d'agrégation : - -```php -// Compte le nombre de produits dans chaque catégorie -$table->select('category_id, COUNT(*) AS count') - ->group('category_id'); -``` - - -having(string $having, ...$parameters): static .[method] --------------------------------------------------------- - -Définit une condition de filtrage des lignes groupées (HAVING). Elle s'utilise avec la méthode `group()` et des fonctions d'agrégation : - -```php -// Trouve les catégories qui ont plus de 100 produits -$table->select('category_id, COUNT(*) AS count') - ->group('category_id') - ->having('count > ?', 100); -``` - - -Lire les données -================ - -Pour lire les données de la base, plusieurs méthodes utiles sont disponibles : - -.[language-php] -| `foreach ($table as $key => $row)` | Parcourt toutes les lignes, `$key` est la valeur de la clé primaire, `$row` un objet ActiveRow | -| `$row = $table->get($key)` | Renvoie une seule ligne d'après la clé primaire | -| `$row = $table->fetch()` | Renvoie la ligne courante et déplace le pointeur sur la suivante | -| `$array = $table->fetchPairs()` | Crée un tableau associatif à partir des résultats | -| `$array = $table->fetchAll()` | Renvoie toutes les lignes sous forme de tableau | -| `count($table)` | Renvoie le nombre de lignes de l'objet Selection | - -L'objet [ActiveRow |api:Nette\Database\Table\ActiveRow] est en lecture seule. Cela signifie que vous ne pouvez pas changer les valeurs de ses propriétés. Cette restriction garantit la cohérence des données et évite les effets de bord inattendus. Les données sont chargées depuis la base, et toute modification doit être faite explicitement et de façon maîtrisée. - - -`foreach` - parcourir toutes les lignes ---------------------------------------- - -La façon la plus simple d'exécuter une requête et de récupérer les lignes est de les parcourir dans une boucle `foreach`. Elle exécute automatiquement la requête SQL. - -```php -$books = $explorer->table('book'); -foreach ($books as $key => $book) { - // $key est la valeur de la clé primaire, $book est un ActiveRow - echo "$book->title ({$book->author->name})"; -} -``` - - -get($key): ?ActiveRow .[method] -------------------------------- - -Exécute la requête SQL et renvoie la ligne d'après la clé primaire, ou `null` si elle n'existe pas. - -```php -$book = $explorer->table('book')->get(123); // renvoie l'ActiveRow d'ID 123 ou null -if ($book) { - echo $book->title; -} -``` - - -fetch(): ?ActiveRow .[method] ------------------------------ - -Renvoie la ligne courante et déplace le pointeur interne sur la suivante. S'il n'y a plus de lignes, renvoie `null`. - -```php -$books = $explorer->table('book'); -while ($book = $books->fetch()) { - $this->processBook($book); -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Renvoie les résultats sous forme de tableau associatif. Le premier argument indique le nom de la colonne à utiliser comme clé du tableau, le second le nom de la colonne à utiliser comme valeur : - -```php -$authors = $explorer->table('author')->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Si seul le premier paramètre est fourni, la valeur sera la ligne entière, c'est-à-dire l'objet `ActiveRow` : - -```php -$authors = $explorer->table('author')->fetchPairs('id'); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - -En cas de clés en double, c'est la valeur de la dernière ligne qui est utilisée. En utilisant `null` comme clé, le tableau sera indexé numériquement à partir de zéro (aucune collision ne se produit alors) : - -```php -$authors = $explorer->table('author')->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Vous pouvez aussi passer en paramètre un callback, qui renverra pour chaque ligne soit une seule valeur, soit une paire clé-valeur. - -```php -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); -// ['First Book (John Novak)', ...] - -// Le callback peut aussi renvoyer un tableau formant une paire clé & valeur : -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => [$row->title, $row->author->name]); -// ['First Book' => 'John Novak', ...] -``` - - -fetchAll(): array .[method] ---------------------------- - -Renvoie toutes les lignes sous forme de tableau associatif d'objets `ActiveRow`, où les clés sont les valeurs de la clé primaire. - -```php -$allBooks = $explorer->table('book')->fetchAll(); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - - -count(): int .[method] ----------------------- - -La méthode `count()` sans paramètre renvoie le nombre de lignes de l'objet `Selection` : - -```php -$table->where('category', 1); -$count = $table->count(); -$count = count($table); // variante -``` - -Remarque : `count()` avec un paramètre exécute la fonction d'agrégation COUNT dans la base de données, voir plus bas. - - -ActiveRow::toArray(): array .[method] -------------------------------------- - -Convertit l'objet `ActiveRow` en tableau associatif, où les clés sont les noms des colonnes et les valeurs les données correspondantes. - -```php -$book = $explorer->table('book')->get(1); -$bookArray = $book->toArray(); -// $bookArray sera ['id' => 1, 'title' => '...', 'author_id' => ..., ...] -``` - - -Agrégation -========== - -La classe `Selection` fournit des méthodes permettant d'exécuter facilement des fonctions d'agrégation (COUNT, SUM, MIN, MAX, AVG, etc.). - -.[language-php] -| `count($expr)` | Compte le nombre de lignes | -| `min($expr)` | Renvoie la valeur minimale d'une colonne | -| `max($expr)` | Renvoie la valeur maximale d'une colonne | -| `sum($expr)` | Renvoie la somme des valeurs d'une colonne | -| `aggregation($function)` | Permet n'importe quelle fonction d'agrégation, comme `AVG()` ou `GROUP_CONCAT()` | - - -count(string $expr): int .[method] ----------------------------------- - -Exécute une requête SQL avec la fonction COUNT et renvoie le résultat. La méthode sert à déterminer combien de lignes correspondent à une condition donnée : - -```php -$count = $table->count('*'); // SELECT COUNT(*) FROM `table` -$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` -``` - -Remarque : [#count()] sans paramètre ne renvoie que le nombre de lignes de l'objet `Selection`. - - -min(string $expr) et max(string $expr) .[method] ------------------------------------------------- - -Les méthodes `min()` et `max()` renvoient les valeurs minimale et maximale de la colonne ou de l'expression indiquée : - -```php -// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 -$maxPrice = $products->where('active', true) - ->max('price'); -``` - - -sum(string $expr): mixed .[method] ----------------------------------- - -Renvoie la somme des valeurs de la colonne ou de l'expression indiquée : - -```php -// SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 -$totalPrice = $products->where('active', true) - ->sum('price * items_in_stock'); -``` - - -aggregation(string $function, ?string $groupFunction = null): mixed .[method] ------------------------------------------------------------------------------ - -Permet d'exécuter n'importe quelle fonction d'agrégation. - -```php -// prix moyen des produits d'une catégorie -$avgPrice = $products->where('category_id', 1) - ->aggregation('AVG(price)'); - -// réunit les tags des produits en une seule chaîne -$tags = $products->where('id', 1) - ->aggregation('GROUP_CONCAT(tag.name) AS tags') - ->fetch() - ->tags; -``` - -Si nous avons besoin d'agréger des résultats qui proviennent déjà d'une fonction d'agrégation et d'un groupement (par exemple `SUM(value)` sur des lignes groupées), nous indiquons en deuxième argument la fonction d'agrégation à appliquer à ces résultats intermédiaires : - -```php -// Calcule le prix total des produits en stock pour chaque catégorie, puis additionne ces prix. -$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') - ->group('category_id') - ->aggregation('SUM(category_total)', 'SUM'); -``` - -Dans cet exemple, nous calculons d'abord le prix total des produits de chaque catégorie (`SUM(price * stock) AS category_total`) et groupons les résultats par `category_id`. Nous utilisons ensuite `aggregation('SUM(category_total)', 'SUM')` pour additionner ces totaux intermédiaires `category_total`. Le deuxième argument `'SUM'` indique que la fonction SUM doit être appliquée aux résultats intermédiaires. - - -Insert, Update & Delete -======================= - -Nette Database Explorer simplifie l'insertion, la mise à jour et la suppression des données. Toutes les méthodes mentionnées lèvent une `Nette\Database\DriverException` en cas d'erreur. - - -Selection::insert(iterable $data) .[method] -------------------------------------------- - -Insère de nouveaux enregistrements dans la table. - -**Insertion d'un seul enregistrement :** - -Passez le nouvel enregistrement sous forme de tableau associatif ou d'objet itérable (comme l'`ArrayHash` utilisé dans les [formulaires |forms:]), où les clés correspondent aux noms des colonnes de la table. - -Si la table a une clé primaire définie, la méthode renvoie un objet `ActiveRow`, rechargé depuis la base pour refléter les éventuelles modifications faites au niveau de la base (triggers, valeurs par défaut des colonnes, calcul des colonnes auto-incrémentées). Cela garantit la cohérence des données, et l'objet contient toujours les données actuelles de la base. Si la table n'a pas de clé primaire, aucune ligne n'est identifiable et la méthode renvoie `null`. - -```php -$row = $explorer->table('users')->insert([ - 'name' => 'John Doe', - 'email' => 'john.doe@example.com', -]); -// $row est une instance d'ActiveRow et contient toutes les données de la ligne insérée, -// y compris l'ID généré automatiquement et les éventuelles modifications faites par les triggers -echo $row->id; // Affiche l'ID de l'utilisateur nouvellement inséré -echo $row->created_at; // Affiche l'heure de création si elle est définie par un trigger -``` - -**Insertion de plusieurs enregistrements à la fois :** - -La méthode `insert()` permet d'insérer plusieurs enregistrements par une seule requête SQL. Dans ce cas, elle renvoie le nombre de lignes insérées. - -```php -$insertedRows = $explorer->table('users')->insert([ - [ - 'name' => 'John', - 'year' => 1994, - ], - [ - 'name' => 'Jack', - 'year' => 1995, - ], -]); -// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) -// $insertedRows vaudra 2 -``` - -Un objet `Selection` contenant une sélection de données peut aussi être passé en paramètre. - -```php -$newUsers = $explorer->table('potential_users') - ->where('approved', 1) - ->select('name, email'); - -$insertedRows = $explorer->table('users')->insert($newUsers); -``` - -**Insertion de valeurs particulières :** - -Nous pouvons aussi passer comme valeurs des fichiers, des objets `DateTime` ou des littéraux SQL : - -```php -$explorer->table('users')->insert([ - 'name' => 'John', - 'created_at' => new DateTime, // convertit au format de la base - 'avatar' => fopen('image.jpg', 'rb'), // insère le contenu binaire du fichier - 'uuid' => $explorer::literal('UUID()'), // appelle la fonction UUID() -]); -``` - - -Selection::update(iterable $data): int .[method] ------------------------------------------------- - -Met à jour les lignes de la table selon le filtre indiqué. Renvoie le nombre de lignes réellement modifiées. - -Passez les colonnes à modifier sous forme de tableau associatif ou d'objet itérable (comme l'`ArrayHash` utilisé dans les [formulaires |forms:]), où les clés correspondent aux noms des colonnes de la table : - -```php -$affected = $explorer->table('users') - ->where('id', 10) - ->update([ - 'name' => 'John Smith', - 'year' => 1994, - ]); -// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 -``` - -Pour modifier des valeurs numériques, vous pouvez utiliser les opérateurs `+=` et `-=` : - -```php -$explorer->table('users') - ->where('id', 10) - ->update([ - 'points+=' => 1, // augmente de 1 la valeur de la colonne 'points' - 'coins-=' => 1, // diminue de 1 la valeur de la colonne 'coins' - ]); -// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 -``` - - -Selection::delete(): int .[method] ----------------------------------- - -Supprime les lignes de la table selon le filtre indiqué. Renvoie le nombre de lignes supprimées. - -```php -$count = $explorer->table('users') - ->where('id', 10) - ->delete(); -// DELETE FROM `users` WHERE `id` = 10 -``` - -.[caution] -Lors de l'appel d'`update()` ou de `delete()`, n'oubliez pas d'utiliser `where()` pour indiquer les lignes à modifier ou à supprimer. Si `where()` n'est pas utilisée, l'opération portera sur toute la table ! - - -ActiveRow::update(iterable $data): bool .[method] -------------------------------------------------- - -Met à jour les données de la ligne représentée par l'objet `ActiveRow`. Elle accepte un itérable contenant les données à mettre à jour (les clés sont les noms des colonnes). Pour modifier des valeurs numériques, vous pouvez utiliser les opérateurs `+=` et `-=` : - -Après la mise à jour, l'`ActiveRow` est automatiquement rechargé depuis la base pour refléter les éventuelles modifications faites au niveau de la base (par exemple par des triggers). La méthode ne renvoie `true` que si un changement réel des données a eu lieu. - -```php -$article = $explorer->table('article')->get(1); -$article->update([ - 'views += 1', // incrémente le nombre de vues -]); -echo $article->views; // Affiche le nombre de vues actuel -``` - -Cette méthode ne met à jour qu'une seule ligne précise de la base. Pour la mise à jour en masse de plusieurs lignes, utilisez la méthode [#Selection::update()]. - - -ActiveRow::delete(): int .[method] ----------------------------------- - -Supprime de la base la ligne représentée par l'objet `ActiveRow`. Renvoie le nombre de lignes supprimées, qui devrait être 1. - -```php -$book = $explorer->table('book')->get(1); -$book->delete(); // Supprime le livre d'ID 1 -``` - -Cette méthode ne supprime qu'une seule ligne précise de la base. Pour la suppression en masse de plusieurs lignes, utilisez la méthode [#Selection::delete()]. - - -Relations entre les tables -========================== - -Dans les bases de données relationnelles, les données sont réparties entre plusieurs tables et reliées entre elles par des clés étrangères. Nette Database Explorer offre une façon révolutionnaire de travailler avec ces relations : sans écrire de requêtes JOIN et sans avoir besoin de configurer ni de générer quoi que ce soit. - -Pour illustrer le travail avec les relations, nous utiliserons une base de données de livres en exemple ([à retrouver sur GitHub |https://github.com/nette-examples/books]). Dans cette base, nous avons les tables : - -- `author` - les écrivains et les traducteurs (colonnes `id`, `name`, `web`, `born`) -- `book` - les livres (colonnes `id`, `author_id`, `translator_id`, `title`, `sequel_id`) -- `tag` - les tags (colonnes `id`, `name`) -- `book_tag` - table de jonction entre les livres et les tags (colonnes `book_id`, `tag_id`) - -[* db-schema-1-.webp *] *** Structure de la base de données utilisée dans les exemples .<> - -Dans notre base de livres, nous trouvons plusieurs types de relations (même si le modèle est simplifié par rapport à la réalité) : - -- **Un-à-plusieurs (1:N)** - Chaque livre **a un** auteur ; un auteur peut écrire **plusieurs** livres. -- **Zéro-à-plusieurs (0:N)** - Un livre **peut avoir** un traducteur ; un traducteur peut traduire **plusieurs** livres. -- **Zéro-à-un (0:1)** - Un livre **peut avoir** une suite. -- **Plusieurs-à-plusieurs (M:N)** - Un livre **peut avoir plusieurs** tags, et un tag peut être attribué à **plusieurs** livres. - -Dans ces relations, il y a toujours une **table parente** et une **table enfant**. Par exemple, dans la relation entre les auteurs et les livres, la table `author` est la parente et la table `book` l'enfant : on peut se le représenter en disant qu'un livre "appartient" toujours à un auteur. Cela se reflète aussi dans la structure de la base : la table enfant `book` contient la clé étrangère `author_id`, qui référence la table parente `author`. - -Si nous avons besoin de lister les livres avec le nom de leurs auteurs, nous avons deux possibilités. Soit récupérer les données par une seule requête SQL avec un JOIN : - -```sql -SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; -``` - -Soit récupérer les données en deux étapes - d'abord les livres, puis leurs auteurs - et les assembler ensuite en PHP : - -```sql -SELECT * FROM book; -SELECT * FROM author WHERE id IN (1, 2, 3); -- IDs des auteurs des livres sélectionnés -``` - -La deuxième approche est en réalité **plus efficace**, même si cela peut surprendre. Les données ne sont récupérées qu'une fois et peuvent être mieux exploitées dans le cache. C'est exactement ainsi que fonctionne Nette Database Explorer : il s'occupe de tout sous le capot et vous offre une API élégante : - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo 'titre : ' . $book->title; - echo 'écrit par : ' . $book->author->name; // $book->author est un enregistrement de la table 'author' - echo 'traduit par : ' . $book->translator?->name; -} -``` - - -Accéder à la table parente --------------------------- - -Accéder à la table parente est simple. Il s'agit de relations comme *un livre a un auteur* ou *un livre peut avoir un traducteur*. L'enregistrement lié s'obtient par une propriété de l'objet ActiveRow, dont le nom correspond au nom de la colonne de clé étrangère sans le suffixe `_id` : - -```php -$book = $explorer->table('book')->get(1); -echo $book->author->name; // trouve l'auteur d'après la colonne author_id -echo $book->translator?->name; // trouve le traducteur d'après la colonne translator_id -``` - -Lors de l'accès à la propriété `$book->author`, Explorer cherche dans la table `book` une colonne dont le nom contient la chaîne `author` (c'est-à-dire `author_id`). D'après la valeur de cette colonne, il charge l'enregistrement correspondant de la table `author` et le renvoie comme `ActiveRow`. De la même façon, `$book->translator` utilise la colonne `translator_id`. Comme la colonne `translator_id` peut contenir `null`, nous utilisons dans le code l'opérateur nullsafe `?->`. - -Une approche alternative est offerte par la méthode `ref()`, qui accepte deux arguments - le nom de la table cible et le nom de la colonne de jointure - et renvoie une instance d'`ActiveRow` ou `null` : - -```php -echo $book->ref('author', 'author_id')->name; // relation vers l'auteur -echo $book->ref('author', 'translator_id')->name; // relation vers le traducteur -``` - -La méthode `ref()` est utile lorsque l'accès par propriété ne peut pas être employé, par exemple parce que la table contient une colonne du même nom (c'est-à-dire `author`). Dans les autres cas, l'accès par propriété est recommandé pour une meilleure lisibilité. - -Explorer optimise automatiquement les requêtes à la base. Lorsque nous parcourons les livres dans une boucle et accédons à leurs enregistrements liés (auteurs, traducteurs), Explorer ne génère pas une requête pour chaque livre séparément. Il n'exécute qu'**une seule requête SELECT par type de relation**, ce qui réduit nettement la charge de la base. Par exemple : - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo $book->title . ': '; - echo $book->author->name; - echo $book->translator?->name; -} -``` - -Ce code n'exécute que ces trois requêtes ultra-rapides vers la base : - -```sql -SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- IDs de la colonne author_id des livres sélectionnés -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- IDs de la colonne translator_id des livres sélectionnés -``` - -.[note] -La logique de recherche de la colonne de jointure est déterminée par l'implémentation des [Conventions |api:Nette\Database\Conventions]. Nous recommandons d'utiliser [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], qui analyse les clés étrangères et vous permet de travailler facilement avec les relations existantes entre les tables. - - -Accéder à la table enfant -------------------------- - -L'accès à la table enfant fonctionne dans l'autre sens. Nous demandons maintenant *quels livres cet auteur a-t-il écrits* ou *quels livres ce traducteur a-t-il traduits*. Pour ce type de requête, nous utilisons la méthode `related()`, qui renvoie une `Selection` contenant les enregistrements liés. Prenons un exemple : - -```php -$author = $explorer->table('author')->get(1); - -// Affiche tous les livres de l'auteur -foreach ($author->related('book.author_id') as $book) { - echo "A écrit : $book->title"; -} - -// Affiche tous les livres traduits par l'auteur -foreach ($author->related('book.translator_id') as $book) { - echo "A traduit : $book->title"; -} -``` - -La méthode `related()` accepte la description de la jointure soit en un seul argument avec la notation par point, soit en deux arguments distincts : - -```php -$author->related('book.translator_id'); // un seul argument -$author->related('book', 'translator_id'); // deux arguments -``` - -Explorer sait détecter automatiquement la bonne colonne de jointure d'après le nom de la table parente. Dans ce cas, il joint via la colonne `book.author_id`, car le nom de la table source est `author` : - -```php -$author->related('book'); // utilise book.author_id -``` - -Si plusieurs liens possibles existent, Explorer lèvera une [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -Nous pouvons bien sûr utiliser la méthode `related()` en parcourant plusieurs enregistrements dans une boucle, et Explorer optimisera là aussi automatiquement les requêtes : - -```php -$authors = $explorer->table('author'); -foreach ($authors as $author) { - echo $author->name . ' a écrit :'; - foreach ($author->related('book') as $book) { - echo $book->title; - } -} -``` - -Ce code ne génère que deux requêtes SQL ultra-rapides : - -```sql -SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- IDs des auteurs sélectionnés -``` - - -Relation plusieurs-à-plusieurs ------------------------------- - -Pour une relation plusieurs-à-plusieurs (M:N), une **table de jonction** est nécessaire (dans notre cas `book_tag`), contenant deux colonnes de clés étrangères (`book_id`, `tag_id`). Chacune de ces colonnes renvoie à la clé primaire de l'une des tables liées. Pour récupérer les données liées, nous obtenons d'abord les enregistrements de la table de jonction à l'aide de `related('book_tag')`, puis nous passons aux données cibles : - -```php -$book = $explorer->table('book')->get(1); -// affiche les noms des tags attribués au livre -foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name; // affiche le nom du tag via la table de jonction -} - -$tag = $explorer->table('tag')->get(1); -// ou dans l'autre sens : affiche les titres des livres portant ce tag -foreach ($tag->related('book_tag') as $bookTag) { - echo $bookTag->book->title; // affiche le titre du livre -} -``` - -Explorer optimise de nouveau les requêtes SQL sous une forme efficace : - -```sql -SELECT * FROM `book`; -SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- IDs des livres sélectionnés -SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- IDs des tags trouvés dans book_tag -``` - - -Requêter à travers les tables liées ------------------------------------ - -Dans les méthodes `where()`, `select()`, `order()` et `group()`, vous pouvez utiliser des notations particulières pour accéder aux colonnes d'autres tables. Explorer crée automatiquement les JOINs nécessaires. - -La **notation par point** (`table_parente.colonne`) s'utilise pour les relations 1:N du point de vue de la table enfant : - -```php -$books = $explorer->table('book'); - -// Trouve les livres dont le nom de l'auteur commence par 'Jon' -$books->where('author.name LIKE ?', 'Jon%'); - -// Trie les livres par nom d'auteur décroissant -$books->order('author.name DESC'); - -// Affiche le titre du livre et le nom de l'auteur -$books->select('book.title, author.name'); -``` - -La **notation par deux-points** (`:table_enfant.colonne`) s'utilise pour les relations 1:N du point de vue de la table parente : - -```php -$authors = $explorer->table('author'); - -// Trouve les auteurs ayant écrit un livre avec 'PHP' dans le titre -$authors->where(':book.title LIKE ?', '%PHP%'); - -// Compte le nombre de livres de chaque auteur -$authors->select('*, COUNT(:book.id) AS book_count') - ->group('author.id'); -``` - -Dans l'exemple ci-dessus avec la notation par deux-points (`:book.title`), la colonne de clé étrangère n'est pas précisée. Explorer détecte automatiquement la bonne colonne d'après le nom de la table parente. Dans ce cas, il joint via la colonne `book.author_id`, car le nom de la table source est `author`. Si plusieurs liens possibles existent, Explorer lèvera une [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -La colonne de jointure peut être indiquée explicitement entre parenthèses : - -```php -// Trouve les auteurs ayant traduit un livre avec 'PHP' dans le titre -$authors->where(':book(translator_id).title LIKE ?', '%PHP%'); -``` - -Les notations peuvent être chaînées pour accéder aux données à travers plusieurs tables : - -```php -// Trouve les auteurs de livres portant le tag 'PHP' -$authors->where(':book:book_tag.tag.name', 'PHP') - ->group('author.id'); -``` - - -Étendre les conditions du JOIN ------------------------------- - -La méthode `joinWhere()` étend les conditions indiquées lors de la jointure des tables en SQL, après le mot-clé `ON`. - -Disons que nous voulons trouver les livres traduits par un traducteur précis : - -```php -// Trouve les livres traduits par un traducteur nommé 'David' -$books = $explorer->table('book') - ->joinWhere('translator', 'translator.name', 'David'); -// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') -``` - -Dans la condition de `joinWhere()`, vous pouvez utiliser les mêmes constructions que dans la méthode `where()` : opérateurs, placeholders, tableaux de valeurs ou expressions SQL. - -Pour les requêtes plus complexes comportant plusieurs JOINs, vous pouvez définir des alias de tables : - -```php -$tags = $explorer->table('tag') - ->joinWhere(':book_tag.book.author', 'book_author.born < ?', 1950) - ->alias(':book_tag.book.author', 'book_author'); -// LEFT JOIN `book_tag` ON `tag`.`id` = `book_tag`.`tag_id` -// LEFT JOIN `book` ON `book_tag`.`book_id` = `book`.`id` -// LEFT JOIN `author` `book_author` ON `book`.`author_id` = `book_author`.`id` -// AND (`book_author`.`born` < 1950) -``` - -Notez que, tandis que la méthode `where()` ajoute des conditions à la clause `WHERE`, la méthode `joinWhere()` étend les conditions de la clause `ON` lors de la jointure des tables. diff --git a/database/fr/guide.texy b/database/fr/guide.texy deleted file mode 100644 index 77f062dcca..0000000000 --- a/database/fr/guide.texy +++ /dev/null @@ -1,218 +0,0 @@ -Nette Database -************** - -.[perex] -Nette Database est une couche de base de données puissante et élégante pour PHP, axée sur la simplicité et les fonctionnalités intelligentes. Elle propose deux façons de travailler avec votre base : l'[Explorer |explorer] pour développer rapidement, ou l'[approche SQL |SQL way] pour manipuler directement les requêtes. - -<div class="grid gap-3"> -<div> - - -[Approche SQL|sql-way] -====================== -- Requêtes paramétrées et sûres -- Contrôle précis de la structure des requêtes SQL -- Quand vous écrivez des requêtes complexes avec des fonctions avancées -- Optimisation des performances à l'aide de fonctions SQL spécifiques - -</div> - -<div> - - -[Explorer |explorer] -==================== -- Développer vite sans écrire de SQL -- Manipulation intuitive des relations entre les tables -- Profiter de l'optimisation automatique des requêtes -- Convient à un travail rapide et confortable avec la base - -</div> - -</div> - - -Installation -============ - -Téléchargez et installez la bibliothèque à l'aide de [Composer|best-practices:composer] : - -```shell -composer require nette/database -``` - - -Bases de données prises en charge -================================= - -Nette Database prend en charge les bases de données suivantes : - -|* Serveur de base de données |* Nom DSN |* Prise en charge Explorer -|-----------------------|--------------|-----------------------| -| MySQL (>= 5.1) | mysql | OUI | -| PostgreSQL (>= 9.0) | pgsql | OUI | -| SQLite 3 (>= 3.8) | sqlite | OUI | -| Oracle | oci | NON | -| MS SQL (PDO_SQLSRV) | sqlsrv | OUI | -| MS SQL (PDO_DBLIB) | mssql | NON | -| ODBC | odbc | NON | - - -Deux approches du travail avec la base de données -================================================= - -Nette Database vous laisse le choix : vous pouvez soit écrire directement les requêtes SQL (approche SQL), soit les laisser être générées automatiquement (Explorer). Voyons comment les deux approches traitent les mêmes tâches : - -[Approche SQL|sql-way] - requêtes SQL - -```php -// Insère un enregistrement -$database->query('INSERT INTO books', [ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// Récupère des enregistrements : les auteurs des livres -$result = $database->query(' - SELECT authors.*, COUNT(books.id) AS books_count - FROM authors - LEFT JOIN books ON authors.id = books.author_id - WHERE authors.active = 1 - GROUP BY authors.id -'); - -// Affichage (pas optimal, génère N requêtes supplémentaires) -foreach ($result as $author) { - $books = $database->query(' - SELECT * FROM books - WHERE author_id = ? - ORDER BY published_at DESC - ', $author->id); - - echo "L'auteur $author->name a écrit $author->books_count livres :\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -[Approche Explorer|explorer] - génération automatique du SQL - -```php -// Insère un enregistrement -$database->table('books')->insert([ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// Récupère des enregistrements : les auteurs des livres -$authors = $database->table('authors') - ->where('active', 1); - -// Affichage (génère automatiquement seulement 2 requêtes optimisées) -foreach ($authors as $author) { - $books = $author->related('books') - ->order('published_at DESC'); - - echo "L'auteur $author->name a écrit {$books->count()} livres :\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -L'approche Explorer génère et optimise les requêtes SQL automatiquement. Dans l'exemple ci-dessus, l'approche SQL génère N+1 requêtes (une pour les auteurs, puis une pour les livres de chaque auteur), tandis qu'Explorer optimise automatiquement les requêtes et n'en exécute que deux : une pour les auteurs et une pour tous leurs livres. - -Les deux approches peuvent être librement combinées dans votre application, selon les besoins. - - -Connexion et configuration -========================== - -Pour vous connecter à la base de données, il suffit de créer une instance de la classe [api:Nette\Database\Connection] : - -```php -$database = new Nette\Database\Connection($dsn, $user, $password); -``` - -Le paramètre `$dsn` (Data Source Name) est le même que celui [utilisé par PDO |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], par exemple `host=127.0.0.1;dbname=test`. En cas d'échec, il lève une `Nette\Database\ConnectionException`. - -Une méthode plus commode est cependant offerte par la [configuration de l'application |configuration], où il vous suffit d'ajouter une section `database`. Cela crée les objets nécessaires ainsi qu'un panneau de base de données dans la barre de [Tracy |tracy:]. - -```neon -database: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password -``` - -L'objet de connexion peut ensuite être [obtenu comme service depuis le conteneur DI |dependency-injection:passing-dependencies], par exemple : - -```php -class Model -{ - public function __construct( - // ou Nette\Database\Explorer - private Nette\Database\Connection $database, - ) { - } -} -``` - -Plus d'informations sur la [configuration de la base de données|configuration]. - - -Création manuelle de l'Explorer -------------------------------- - -Si vous n'utilisez pas le conteneur DI de Nette, vous pouvez créer manuellement une instance de `Nette\Database\Explorer` : - -```php -// connexion à la base de données -$connection = new Nette\Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); -// stockage du cache, implémente Nette\Caching\Storage, par exemple : -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); -// se charge de la réflexion de la structure de la base -$structure = new Nette\Database\Structure($connection, $storage); -// définit les règles de mapping des noms de tables, de colonnes et de clés étrangères -$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); -$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage); -``` - - -Gestion de la connexion -======================= - -Lors de la création d'un objet `Connection`, la connexion est établie automatiquement. Si vous voulez la différer, utilisez le mode lazy : activez-le dans la [configuration|configuration] en définissant `lazy`, ou ainsi : - -```php -$database = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => true]); -``` - -Pour gérer la connexion, utilisez les méthodes `connect()`, `disconnect()` et `reconnect()`. -- `connect()` crée une connexion si elle n'existe pas déjà et peut lever une `Nette\Database\ConnectionException`. -- `disconnect()` ferme la connexion courante à la base de données. -- `reconnect()` effectue une déconnexion suivie d'une reconnexion à la base. Cette méthode peut elle aussi lever une `Nette\Database\ConnectionException`. - -Vous pouvez en outre surveiller les événements liés à la connexion à l'aide de l'événement `onConnect`, qui est un tableau de callbacks appelés après l'établissement de la connexion à la base. - -```php -// s'exécute après la connexion à la base de données -$database->onConnect[] = function($database) { - echo "Connecté à la base de données"; -}; -``` - -L'événement `onQuery` fonctionne de la même façon : c'est un tableau de callbacks invoqués après chaque requête exécutée (et lorsqu'une requête échoue), utile pour la journalisation ou le profilage. - - -Barre de débogage Tracy -======================= - -Si vous utilisez [Tracy |tracy:], le panneau Database de la Debug Bar est activé automatiquement. Il affiche toutes les requêtes exécutées, leurs paramètres, leur temps d'exécution et l'endroit du code d'où elles ont été appelées. - -[* db-panel.webp *] diff --git a/database/fr/reflection.texy b/database/fr/reflection.texy deleted file mode 100644 index 9fbee322a8..0000000000 --- a/database/fr/reflection.texy +++ /dev/null @@ -1,127 +0,0 @@ -Réflexion de la structure -************************* - -.{data-version:3.2.1} -Nette Database fournit des outils d'introspection de la structure de la base de données à l'aide de la classe [api:Nette\Database\Reflection]. Elle permet d'obtenir des informations sur les tables, les colonnes, les index et les clés étrangères. Vous pouvez utiliser la réflexion pour générer des schémas, créer des applications souples travaillant avec la base de données, ou des outils de base de données généraux. - -L'objet de réflexion s'obtient depuis l'instance de connexion à la base de données : - -```php -$reflection = $database->getReflection(); -``` - - -Récupérer les tables --------------------- - -La propriété en lecture seule `$reflection->tables` contient un tableau associatif de toutes les tables de la base de données : - -```php -// Liste les noms de toutes les tables -foreach ($reflection->tables as $name => $table) { - echo $name . "\n"; -} -``` - -Deux méthodes supplémentaires sont disponibles : - -```php -// Vérifie l'existence d'une table -if ($reflection->hasTable('users')) { - echo "La table users existe"; -} - -// Renvoie l'objet de la table ; lève une exception si elle n'existe pas -$table = $reflection->getTable('users'); -``` - - -Informations sur la table -------------------------- - -Une table est représentée par l'objet [Table|api:Nette\Database\Reflection\Table], qui fournit les propriétés en lecture seule suivantes : - -- `$name: string` - nom de la table -- `$view: bool` - s'il s'agit d'une vue -- `$fullName: ?string` - nom complet de la table, schéma compris (s'il existe) -- `$columns: array<string, Column>` - tableau associatif des colonnes de la table -- `$indexes: Index[]` - tableau des index de la table -- `$primaryKey: ?Index` - clé primaire de la table, ou null -- `$foreignKeys: ForeignKey[]` - tableau des clés étrangères de la table -- `$comment: ?string` - commentaire de la table - - -Colonnes --------- - -La propriété `columns` de la table fournit un tableau associatif de colonnes, où la clé est le nom de la colonne et la valeur une instance de [Column|api:Nette\Database\Reflection\Column] avec ces propriétés : - -- `$name: string` - nom de la colonne -- `$table: ?Table` - référence à la table de la colonne -- `$nativeType: string` - type natif de la base de données -- `$size: ?int` - taille/longueur du type -- `$nullable: bool` - si la colonne peut contenir NULL -- `$default: mixed` - valeur par défaut de la colonne -- `$autoIncrement: bool` - si la colonne est auto-incrémentée -- `$primary: bool` - si elle fait partie de la clé primaire -- `$vendor: array` - métadonnées supplémentaires propres au système de base de données concerné -- `$comment: ?string` - commentaire de la colonne - -```php -foreach ($table->columns as $name => $column) { - echo "Colonne : $name\n"; - echo "Type : {$column->nativeType}\n"; - echo "Nullable : " . ($column->nullable ? 'Oui' : 'Non') . "\n"; -} -``` - - -Index ------ - -La propriété `indexes` de la table fournit un tableau d'index, où chaque index est une instance de [Index|api:Nette\Database\Reflection\Index] avec ces propriétés : - -- `$columns: Column[]` - tableau des colonnes composant l'index -- `$unique: bool` - si l'index est unique -- `$primary: bool` - s'il s'agit d'une clé primaire -- `$name: ?string` - nom de l'index - -La clé primaire de la table s'obtient à l'aide de la propriété `primaryKey`, qui renvoie soit un objet `Index`, soit `null` si la table n'a pas de clé primaire. - -```php -// Liste les index -foreach ($table->indexes as $index) { - $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); - echo "Index" . ($index->name ? " {$index->name}" : '') . ":\n"; - echo " Colonnes : $columns\n"; - echo " Unique : " . ($index->unique ? 'Oui' : 'Non') . "\n"; -} - -// Liste la clé primaire -if ($primaryKey = $table->primaryKey) { - $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); - echo "Clé primaire : $columns\n"; -} -``` - - -Clés étrangères ---------------- - -La propriété `foreignKeys` de la table fournit un tableau de clés étrangères, où chaque clé étrangère est une instance de [ForeignKey|api:Nette\Database\Reflection\ForeignKey] avec ces propriétés : - -- `$foreignTable: Table` - la table référencée -- `$localColumns: Column[]` - tableau des colonnes locales -- `$foreignColumns: Column[]` - tableau des colonnes référencées -- `$name: string` - nom de la clé étrangère - -```php -// Liste les clés étrangères -foreach ($table->foreignKeys as $fk) { - $localCols = implode(', ', array_map(fn($col) => $col->name, $fk->localColumns)); - $foreignCols = implode(', ', array_map(fn($col) => $col->name, $fk->foreignColumns)); - - echo "FK" . ($fk->name ? " {$fk->name}" : '') . ":\n"; - echo " $localCols -> {$fk->foreignTable->name}($foreignCols)\n"; -} -``` diff --git a/database/fr/security.texy b/database/fr/security.texy deleted file mode 100644 index c65a5ca9e5..0000000000 --- a/database/fr/security.texy +++ /dev/null @@ -1,185 +0,0 @@ -Risques de sécurité -******************* - -<div class=perex> - -Les bases de données contiennent souvent des données sensibles et permettent d'effectuer des opérations dangereuses. Pour travailler en sécurité avec Nette Database, il est essentiel de : - -- comprendre la différence entre une API sûre et une API non sûre -- utiliser des requêtes paramétrées -- valider correctement les données d'entrée - -</div> - - -Qu'est-ce que l'injection SQL ? -=============================== - -L'injection SQL est le risque de sécurité le plus grave lors du travail avec les bases de données. Elle survient lorsqu'une entrée utilisateur non assainie devient partie d'une requête SQL. Un attaquant peut y insérer ses propres commandes SQL et ainsi : -- obtenir un accès non autorisé aux données -- modifier ou supprimer des données dans la base -- contourner l'authentification - -```php -// ❌ CODE DANGEREUX - vulnérable à l'injection SQL -$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); - -// Un attaquant pourrait saisir une valeur comme : ' OR '1'='1 -// La requête résultante serait : SELECT * FROM users WHERE name = '' OR '1'='1' -// Ce qui renvoie tous les utilisateurs -``` - -Il en va de même pour Database Explorer : - -```php -// ❌ CODE DANGEREUX - vulnérable à l'injection SQL -$table->where('name = ' . $_GET['name']); -$table->where("name = '$_GET[name]'"); -``` - - -Requêtes paramétrées -==================== - -La défense fondamentale contre l'injection SQL, ce sont les requêtes paramétrées. Nette Database propose plusieurs façons de les utiliser. - -La plus simple est d'utiliser des **points d'interrogation comme placeholders** : - -```php -// ✅ Requête paramétrée sûre -$database->query('SELECT * FROM users WHERE name = ?', $name); - -// ✅ Condition sûre dans Explorer -$table->where('name = ?', $name); -``` - -Cela vaut pour toutes les autres méthodes de [Database Explorer|explorer] qui permettent d'insérer des expressions avec des points d'interrogation et des paramètres. - -Pour les commandes INSERT, UPDATE ou la clause WHERE, nous pouvons passer les valeurs dans un tableau : - -```php -// ✅ INSERT sûr -$database->query('INSERT INTO users', [ - 'name' => $name, - 'email' => $email, -]); - -// ✅ INSERT sûr dans Explorer -$table->insert([ - 'name' => $name, - 'email' => $email, -]); -``` - - -Validation des valeurs des paramètres -===================================== - -Les requêtes paramétrées sont la pierre angulaire d'un travail sûr avec la base de données. Les valeurs que nous y insérons doivent cependant passer par plusieurs niveaux de contrôle : - - -Contrôle du type ----------------- - -**Le plus important est de garantir le bon type de données des paramètres** : c'est une condition nécessaire à l'utilisation sûre de Nette Database. La base de données part du principe que toutes les données d'entrée ont le type de données correct, correspondant à la colonne concernée. - -Si par exemple `$name`, dans les exemples précédents, était de façon inattendue un tableau au lieu d'une chaîne, Nette Database essaierait d'insérer tous ses éléments dans la requête SQL, ce qui provoquerait une erreur. **N'utilisez donc jamais** des données non validées provenant de `$_GET`, `$_POST` ou `$_COOKIE` directement dans les requêtes de base de données. - - -Validation du format --------------------- - -Au deuxième niveau, nous contrôlons le format des données : par exemple si les chaînes sont bien encodées en UTF-8 et si leur longueur correspond à la définition de la colonne, ou si les valeurs numériques se situent dans la plage autorisée par le type de données de la colonne. - -Pour ce niveau de validation, nous pouvons en partie nous appuyer sur la base de données elle-même : beaucoup de bases refuseront les données invalides. Le comportement peut cependant varier ; certaines tronqueront silencieusement les chaînes trop longues ou rogneront les nombres hors plage. - - -Validation propre au domaine ----------------------------- - -Le troisième niveau concerne les contrôles logiques propres à votre application. Par exemple vérifier que les valeurs issues des listes déroulantes correspondent bien aux options proposées, que les nombres se situent dans la plage attendue (par exemple un âge de 0 à 150 ans), ou que les dépendances mutuelles entre valeurs ont un sens. - - -Méthodes de validation recommandées ------------------------------------ - -- Utilisez [Nette Forms|forms:], qui assure automatiquement la validation correcte de toutes les entrées. -- Utilisez les [presenters|application:] et indiquez les types de données des paramètres dans les méthodes `action*()` et `render*()`. -- Ou implémentez votre propre couche de validation à l'aide des outils PHP standards comme `filter_var()`. - - -Travailler en sécurité avec les colonnes -======================================== - -Dans la section précédente, nous avons montré comment valider correctement les valeurs des paramètres. Lorsque nous utilisons des tableaux dans les requêtes SQL, nous devons cependant accorder la même attention à leurs clés. - -```php -// ❌ CODE DANGEREUX - les clés du tableau ne sont pas assainies -$database->query('INSERT INTO users', $_POST); -``` - -Pour les commandes INSERT et UPDATE, c'est une faille de sécurité critique : un attaquant peut insérer ou modifier n'importe quelle colonne de la base. Il pourrait par exemple définir `is_admin = 1` ou insérer des données arbitraires dans des colonnes sensibles (ce qu'on appelle la Mass Assignment Vulnerability). - -Dans les conditions WHERE, c'est encore plus dangereux, car elles peuvent contenir des opérateurs : - -```php -// ❌ CODE DANGEREUX - les clés du tableau ne sont pas assainies -$_POST['salary >'] = 100000; -$database->query('SELECT * FROM users WHERE', $_POST); -// exécute la requête WHERE (`salary` > 100000) -``` - -Un attaquant peut ainsi découvrir méthodiquement les salaires des employés. Il peut commencer par exemple par une requête sur les salaires supérieurs à 100 000, puis inférieurs à 50 000, et en resserrant progressivement la fourchette, il peut révéler les salaires approximatifs de tous les employés. Ce type d'attaque s'appelle l'énumération SQL. - -Les méthodes `where()` et `whereOr()` sont même [bien plus souples |explorer#where()] et acceptent des expressions SQL, opérateurs et fonctions compris, dans les clés comme dans les valeurs. Cela donne à un attaquant la possibilité d'effectuer une injection SQL : - -```php -// ❌ CODE DANGEREUX - l'attaquant peut insérer son propre SQL -$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1']; -$table->where($_POST); -// exécute la requête WHERE (0) UNION SELECT name, salary FROM users WHERE (1) -``` - -Cette attaque termine la condition d'origine par `0)`, ajoute son propre `SELECT` à l'aide d'`UNION` pour obtenir des données sensibles de la table `users`, puis referme la requête de façon syntaxiquement correcte avec `WHERE (1)`. - - -Liste blanche de colonnes -------------------------- - -Pour travailler en sécurité avec les noms de colonnes, nous avons besoin d'un mécanisme garantissant que l'utilisateur ne peut travailler qu'avec les colonnes autorisées et ne peut pas en ajouter d'autres. Nous pourrions essayer de détecter et de bloquer les noms de colonnes dangereux (liste noire), mais cette approche n'est pas fiable : un attaquant trouvera toujours une nouvelle façon d'écrire un nom de colonne dangereux à laquelle nous n'aurions pas pensé. - -Il est donc bien plus sûr d'inverser la logique et de définir une liste explicite des colonnes autorisées (liste blanche) : - -```php -// Colonnes que l'utilisateur a le droit de modifier -$allowedColumns = ['name', 'email', 'active']; - -// Supprime de l'entrée toutes les colonnes non autorisées -$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); - -// ✅ Utilisation désormais sûre dans les requêtes, par exemple : -$database->query('INSERT INTO users', $filteredData); -$table->update($filteredData); -$table->where($filteredData); -``` - - -Identifiants dynamiques -======================= - -Pour les noms dynamiques de tables et de colonnes, utilisez le placeholder `?name`. Il assure l'échappement correct des identifiants selon la syntaxe de la base concernée (par exemple à l'aide de backticks dans MySQL) : - -```php -// ✅ Usage sûr d'identifiants de confiance -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name', $column, $table); -// Résultat dans MySQL : SELECT `name` FROM `users` -``` - -Important : n'utilisez le symbole `?name` que pour des valeurs de confiance définies dans le code de l'application. Pour les valeurs venant de l'utilisateur, utilisez de nouveau une [liste blanche |#Liste blanche de colonnes]. Sinon, vous vous exposez à des risques de sécurité : - -```php -// ❌ DANGEREUX - n'utilisez jamais l'entrée de l'utilisateur -$database->query('SELECT ?name FROM users', $_GET['column']); -``` diff --git a/database/fr/sql-way.texy b/database/fr/sql-way.texy deleted file mode 100644 index cc7469635f..0000000000 --- a/database/fr/sql-way.texy +++ /dev/null @@ -1,513 +0,0 @@ -Approche SQL -************ - -.[perex] -Nette Database propose deux façons de travailler : vous pouvez écrire vous-même les requêtes SQL (approche SQL), ou les faire générer automatiquement (voir [Explorer |explorer]). L'approche SQL vous donne le contrôle total sur les requêtes tout en garantissant qu'elles sont construites de façon sûre. - -.[note] -Les détails de la connexion à la base de données et de sa configuration se trouvent dans le chapitre [Connexion et configuration |guide#Connexion et configuration]. - - -Requêtes de base -================ - -La méthode `query()` sert à interroger la base de données. Elle renvoie un objet [ResultSet |api:Nette\Database\ResultSet], qui représente le résultat de la requête. Si la requête échoue, la méthode [lève une exception|exceptions]. Vous pouvez parcourir le résultat de la requête par une boucle `foreach`, ou utiliser l'une des [méthodes auxiliaires |#Récupérer les données]. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; -} -``` - -Pour insérer des valeurs dans les requêtes SQL en toute sécurité, utilisez des requêtes paramétrées. Nette Database rend cela extrêmement simple : il suffit d'ajouter une virgule et la valeur après la requête SQL : - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Avec plusieurs paramètres, vous avez deux possibilités. Vous pouvez soit entrelacer la requête SQL et les paramètres : - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age); -``` - -Soit écrire d'abord toute la requête SQL, puis ajouter tous les paramètres : - -```php -$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); -``` - - -Protection contre l'injection SQL -================================= - -Pourquoi est-il important d'utiliser des requêtes paramétrées ? Parce qu'elles vous protègent d'une attaque appelée injection SQL, où un attaquant pourrait injecter ses propres commandes SQL et ainsi accéder aux données de la base ou les endommager. - -.[warning] -**N'insérez jamais de variables directement dans une requête SQL !** Utilisez toujours des requêtes paramétrées, qui vous protègent de l'injection SQL. - -```php -// ❌ CODE DANGEREUX - vulnérable à l'injection SQL -$database->query("SELECT * FROM users WHERE name = '$name'"); - -// ✅ Requête paramétrée sûre -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Prenez connaissance des [risques de sécurité possibles |security]. - - -Techniques de requêtage -======================= - - -Conditions WHERE ----------------- - -Vous pouvez écrire les conditions `WHERE` sous forme de tableau associatif, où les clés sont les noms des colonnes et les valeurs les données à comparer. Nette Database choisit automatiquement l'opérateur SQL le plus adapté selon le type de la valeur. - -```php -$database->query('SELECT * FROM users WHERE', [ - 'name' => 'John', - 'active' => true, -]); -// WHERE `name` = 'John' AND `active` = 1 -``` - -Vous pouvez aussi indiquer explicitement l'opérateur de comparaison dans la clé : - -```php -$database->query('SELECT * FROM users WHERE', [ - 'age >' => 25, // utilise l'opérateur > - 'name LIKE' => '%John%', // utilise l'opérateur LIKE - 'email NOT LIKE' => '%example.com%', // utilise l'opérateur NOT LIKE -]); -// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' -``` - -Nette gère automatiquement les cas particuliers comme les valeurs `null` ou les tableaux. - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name' => 'Laptop', // utilise l'opérateur = - 'category_id' => [1, 2, 3], // utilise IN - 'description' => null, // utilise IS NULL -]); -// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL -``` - -Pour les conditions négatives, utilisez l'opérateur `NOT` : - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // utilise l'opérateur != - 'category_id NOT' => [1, 2, 3], // utilise NOT IN - 'description NOT' => null, // utilise IS NOT NULL - 'id NOT' => [], // ignoré -]); -// WHERE `name` != 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL -``` - -Par défaut, les conditions sont jointes par l'opérateur `AND`. Cela peut être changé à l'aide du [placeholder ?or |#Indications de construction SQL]. - - -Règles ORDER BY ---------------- - -La clause `ORDER BY` peut s'écrire à l'aide d'un tableau. Indiquez les colonnes dans les clés et utilisez une valeur booléenne pour préciser l'ordre croissant (`true`) ou décroissant (`false`) : - -```php -$database->query('SELECT id FROM author ORDER BY', [ - 'id' => true, // croissant - 'name' => false, // décroissant -]); -// SELECT id FROM author ORDER BY `id`, `name` DESC -``` - - -Insérer des données (INSERT) ----------------------------- - -La commande SQL `INSERT` sert à insérer des enregistrements. - -```php -$values = [ - 'name' => 'John Doe', - 'email' => 'john@example.com', -]; -$database->query('INSERT INTO users ?', $values); -$userId = $database->getInsertId(); -``` - -La méthode `getInsertId()` renvoie l'ID de la dernière ligne insérée. Pour certaines bases de données (par exemple PostgreSQL), il faut indiquer en paramètre le nom de la séquence depuis laquelle l'ID doit être généré, avec `$database->getInsertId($sequenceId)`. - -Vous pouvez aussi passer comme paramètres des [#Valeurs spéciales], par exemple des fichiers, des objets DateTime ou des types enum. - -Insertion de plusieurs enregistrements d'un coup : - -```php -$database->query('INSERT INTO users ?', [ - ['name' => 'User 1', 'email' => 'user1@mail.com'], - ['name' => 'User 2', 'email' => 'user2@mail.com'], -]); -``` - -Un INSERT multiple est bien plus rapide, car une seule requête est exécutée au lieu de nombreuses requêtes individuelles. - -**Note de sécurité :** n'utilisez jamais de données non validées comme `$values`. Prenez connaissance des [risques possibles |security#Travailler en sécurité avec les colonnes]. - - -Mettre à jour des données (UPDATE) ----------------------------------- - -La commande SQL `UPDATE` sert à mettre à jour des enregistrements. - -```php -// Met à jour un seul enregistrement -$values = [ - 'name' => 'John Smith', -]; -$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); -``` - -Le nombre de lignes affectées est renvoyé par `$result->getRowCount()`. - -Pour `UPDATE`, nous pouvons utiliser les opérateurs `+=` et `-=` : - -```php -$database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // incrémente login_count -], 1); -``` - -Exemple d'insertion, ou de mise à jour d'un enregistrement s'il existe déjà. Nous utilisons la technique `ON DUPLICATE KEY UPDATE` : - -```php -$values = [ - 'name' => $name, - 'year' => $year, -]; -$database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', - $values + ['id' => $id], - $values, -); -// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978) -// ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 -``` - -Remarquez que Nette Database reconnaît le contexte dans lequel un paramètre tableau est utilisé au sein de la commande SQL et construit le code SQL en conséquence. Du premier tableau, il a donc construit `(id, name, year) VALUES (123, 'Jim', 1978)`, tandis qu'il a converti le second en `name = 'Jim', year = 1978`. Nous en parlons plus en détail dans la section [#Indications de construction SQL]. - - -Supprimer des données (DELETE) ------------------------------- - -La commande SQL `DELETE` sert à supprimer des enregistrements. Exemple d'obtention du nombre de lignes supprimées : - -```php -$count = $database->query('DELETE FROM users WHERE id = ?', 1) - ->getRowCount(); -``` - - -Indications de construction SQL -------------------------------- - -Une indication est un placeholder particulier dans une requête SQL, qui précise comment la valeur du paramètre doit être convertie en expression SQL : - -| Indication | Description | Utilisée automatiquement pour -|-----------|-------------------------------------------------|----------------------------- -| `?name` | Sert à insérer des noms de tables ou de colonnes | - -| `?values` | Génère `(clé, ...) VALUES (valeur, ...)` | `INSERT ... ?`, `REPLACE ... ?` -| `?set` | Génère des affectations `clé = valeur, ...` | `SET ?`, `KEY UPDATE ?` -| `?and` | Joint les conditions d'un tableau par `AND` | `WHERE ?`, `HAVING ?` -| `?or` | Joint les conditions d'un tableau par `OR` | - -| `?order` | Génère la clause `ORDER BY` | `ORDER BY ?`, `GROUP BY ?` - -Le placeholder `?name` sert à insérer dynamiquement des noms de tables et de colonnes dans la requête. Nette Database se charge de la mise entre quotes correcte des identifiants selon les conventions de la base (par exemple entre backticks dans MySQL). - -```php -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); -// SELECT `name` FROM `users` WHERE id = 1 (dans MySQL) -``` - -**Attention :** n'utilisez le placeholder `?name` que pour des noms de tables et de colonnes validés. Sinon, vous vous exposez à des [failles de sécurité |security#Identifiants dynamiques]. - -Les autres indications n'ont généralement pas besoin d'être précisées, car Nette utilise une détection automatique intelligente lors de la construction de la requête SQL (voir la troisième colonne du tableau). Vous pouvez cependant vous en servir, par exemple lorsque vous voulez joindre les conditions par `OR` au lieu d'`AND` : - -```php -$database->query('SELECT * FROM users WHERE ?or', [ - 'name' => 'John', - 'email' => 'john@example.com', -]); -// SELECT * FROM users WHERE `name` = 'John' OR `email` = 'john@example.com' -``` - - -Valeurs spéciales ------------------ - -Outre les types scalaires courants (string, int, bool), vous pouvez aussi passer comme paramètres des valeurs particulières : - -- fichiers : `fopen('image.gif', 'r')` insère le contenu binaire du fichier -- date et heure : les objets `DateTimeInterface` sont convertis au format de la base -- types enum : les instances d'`enum` sont converties en leur valeur -- littéraux SQL : créés à l'aide de `Connection::literal('NOW()')`, ils sont insérés directement dans la requête - -```php -$database->query('INSERT INTO articles ?', [ - 'title' => 'My Article', - 'published_at' => new DateTimeImmutable, // ou new DateTime - 'content' => fopen('image.png', 'r'), - 'state' => Status::Draft, -]); -``` - -Pour les bases de données qui n'ont pas de prise en charge native du type `datetime` (comme SQLite et Oracle), les objets `DateTime` et `DateTimeImmutable` sont convertis vers une valeur définie dans la [configuration de la base de données|configuration] par l'entrée `formatDateTime` (la valeur par défaut est `U`, le timestamp Unix). - - -Littéraux SQL -------------- - -Dans certains cas, vous devez passer comme valeur du code SQL brut, qui ne doit pas être traité comme une chaîne ni échappé. Les objets de la classe `Nette\Database\SqlLiteral` servent à cela. Ils sont créés par la méthode `Connection::literal()`. - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - 'year >' => $database::literal('YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) -``` - -Ou bien : - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) -``` - -Les littéraux SQL peuvent contenir des paramètres : - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > ? AND year < ?', $min, $max), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) -``` - -Cela permet des combinaisons intéressantes : - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('?or', [ - 'active' => true, - 'role' => $role, - ]), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (`active` = 1 OR `role` = 'admin') -``` - - -Récupérer les données -===================== - - -Raccourcis pour les requêtes SELECT ------------------------------------ - -Pour simplifier la récupération des données, `Connection` offre plusieurs raccourcis qui combinent un appel à `query()` et un appel `fetch*()` qui suit. Ces méthodes acceptent les mêmes paramètres que `query()`, c'est-à-dire une requête SQL et des paramètres facultatifs. La description complète des méthodes `fetch*()` se trouve [plus bas |#fetch()]. - -| `fetch($sql, ...$params): ?Row` | Exécute la requête et renvoie la première ligne comme objet `Row`, ou `null`. -| `fetchAll($sql, ...$params): array` | Exécute la requête et renvoie toutes les lignes sous forme de tableau d'objets `Row`. -| `fetchPairs($sql, ...$params): array` | Exécute la requête et renvoie un tableau associatif (paires clé => valeur). -| `fetchField($sql, ...$params): mixed` | Exécute la requête et renvoie la valeur de la première colonne de la première ligne. -| `fetchList($sql, ...$params): ?array` | Exécute la requête et renvoie la première ligne comme tableau indexé, ou `null`. - -Exemple : - -```php -// fetchField() - renvoie la valeur de la première cellule -$count = $database->query('SELECT COUNT(*) FROM articles') - ->fetchField(); -``` - - -`foreach` - parcourir les lignes --------------------------------- - -Après l'exécution d'une requête, un objet [ResultSet|api:Nette\Database\ResultSet] est renvoyé, qui permet de parcourir les résultats de plusieurs façons. La plus simple pour exécuter une requête et récupérer les lignes est de les parcourir dans une boucle `foreach`. C'est la méthode la plus économe en mémoire, car elle récupère les données ligne par ligne et ne charge pas tout le jeu de résultats en mémoire d'un coup. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; - // ... -} -``` - -.[note] -Le `ResultSet` ne peut être parcouru qu'une seule fois. Si vous avez besoin de le parcourir plusieurs fois, vous devez d'abord charger les données dans un tableau, par exemple à l'aide de la méthode `fetchAll()`. - - -fetch(): ?Row .[method] ------------------------ - -Renvoie une ligne sous forme d'objet `Row`. S'il n'y a plus de lignes, renvoie `null`. Déplace le pointeur interne sur la ligne suivante. - -```php -$result = $database->query('SELECT * FROM users'); -$row = $result->fetch(); // charge la première ligne -if ($row) { - echo $row->name; -} -``` - - -fetchAll(): array .[method] ---------------------------- - -Renvoie toutes les lignes restantes du `ResultSet` sous forme de tableau d'objets `Row`. - -```php -$result = $database->query('SELECT * FROM users'); -$rows = $result->fetchAll(); // charge toutes les lignes -foreach ($rows as $row) { - echo $row->name; -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Renvoie le jeu de résultats sous forme de tableau associatif. Le premier argument indique la colonne à utiliser comme clés, le second la colonne à utiliser comme valeurs : - -```php -$result = $database->query('SELECT id, name FROM users'); -$names = $result->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Si seul le premier paramètre (`$key`) est fourni, c'est la ligne entière (objet `Row`) qui sera utilisée comme valeur : - -```php -$rows = $result->fetchPairs('id'); -// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...] -``` - -En cas de clés en double, c'est la valeur de la dernière ligne qui est utilisée. Utiliser `null` comme clé donne un tableau indexé numériquement (à partir de zéro), ce qui évite les collisions de clés : - -```php -$names = $result->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Vous pouvez aussi fournir un callback qui traite chaque ligne. Le callback peut renvoyer une seule valeur ou une paire clé-valeur. - -```php -$result = $database->query('SELECT * FROM users'); -$items = $result->fetchPairs(fn($row) => "$row->id - $row->name"); -// ['1 - John', '2 - Jane', ...] - -// Le callback peut aussi renvoyer un tableau formant une paire clé & valeur : -$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); -// ['John' => 46, 'Jane' => 21, ...] -``` - - -fetchField(): mixed .[method] ------------------------------ - -Renvoie la valeur de la première colonne de la ligne courante. S'il n'y a plus de lignes, renvoie `null`. Déplace le pointeur interne sur la ligne suivante. - -```php -$result = $database->query('SELECT name FROM users'); -$name = $result->fetchField(); // charge le name de la première ligne -``` - - -fetchList(): ?array .[method] ------------------------------ - -Renvoie la ligne sous forme de tableau indexé. S'il n'y a plus de lignes, renvoie `null`. Déplace le pointeur interne sur la ligne suivante. - -```php -$result = $database->query('SELECT name, email FROM users'); -$row = $result->fetchList(); // ['John', 'john@example.com'] -``` - - -getRowCount(): ?int .[method] ------------------------------ - -Renvoie le nombre de lignes affectées par la dernière requête `UPDATE` ou `DELETE`. Pour les requêtes `SELECT`, renvoie le nombre de lignes du jeu de résultats. Celui-ci n'est cependant pas toujours connu, auquel cas la méthode renvoie `null`. - - -getColumnCount(): ?int .[method] --------------------------------- - -Renvoie le nombre de colonnes du `ResultSet`. - - -Informations sur la requête -=========================== - -À des fins de débogage, nous pouvons obtenir des informations sur la dernière requête exécutée : - -```php -echo $database->getLastQueryString(); // affiche la requête SQL - -$result = $database->query('SELECT * FROM articles'); -echo $result->getQueryString(); // affiche la requête SQL -echo $result->getTime(); // affiche le temps d'exécution en secondes -``` - -Pour afficher le résultat sous forme de tableau HTML, vous pouvez utiliser : - -```php -$result = $database->query('SELECT * FROM articles'); -$result->dump(); -``` - -Le `ResultSet` fournit des informations sur les types des colonnes : - -```php -$result = $database->query('SELECT * FROM articles'); -$types = $result->getColumnTypes(); - -foreach ($types as $column => $type) { - echo "$column est de type $type"; // par ex. 'id est de type int' -} -``` - - -Journalisation des requêtes ---------------------------- - -Nous pouvons mettre en place notre propre journalisation des requêtes. L'événement `onQuery` est un tableau de callbacks appelés après chaque requête exécutée : - -```php -$database->onQuery[] = function ($database, $result) use ($logger) { - $logger->info('Requête : ' . $result->getQueryString()); - $logger->info('Temps : ' . $result->getTime()); - - if ($result->getRowCount() > 1000) { - $logger->warning('Jeu de résultats volumineux : ' . $result->getRowCount() . ' lignes'); - } -}; -``` diff --git a/database/fr/transactions.texy b/database/fr/transactions.texy deleted file mode 100644 index 62c0067f22..0000000000 --- a/database/fr/transactions.texy +++ /dev/null @@ -1,45 +0,0 @@ -Transactions -************ - -.[perex] -Les transactions garantissent que toutes les opérations qu'elles contiennent sont exécutées, ou bien aucune. Elles sont utiles pour préserver la cohérence des données lors d'opérations complexes. - -La façon la plus simple d'utiliser les transactions ressemble à ceci : - -```php -$database->beginTransaction(); -try { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); - $database->commit(); -} catch (\Exception $e) { - $database->rollBack(); - throw $e; -} -``` - -Vous pouvez obtenir le même résultat de façon bien plus élégante à l'aide de la méthode `transaction()`. Elle accepte un callback qui est exécuté à l'intérieur de la transaction. Si le callback s'exécute sans exception, la transaction est validée automatiquement. Si une exception survient, la transaction est annulée et l'exception est propagée plus loin. - -```php -$database->transaction(function ($database) use ($id) { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); -}); -``` - -Les appels à `transaction()` peuvent être imbriqués, ce qui permet de composer facilement des méthodes gérant chacune sa propre transaction. Seule la transaction la plus externe est réellement envoyée à la base sous forme de `BEGIN`/`COMMIT` ; les appels internes se contentent de suivre la profondeur d'imbrication. Appeler manuellement `beginTransaction()`, `commit()` ou `rollBack()` à l'intérieur d'un callback de `transaction()` lève une `LogicException`. - -La méthode `transaction()` peut aussi renvoyer des valeurs : - -```php -$count = $database->transaction(function ($database) { - $result = $database->query('UPDATE users SET active = ?', true); - return $result->getRowCount(); // renvoie le nombre de lignes mises à jour -}); -``` diff --git a/database/fr/type-conversion.texy b/database/fr/type-conversion.texy deleted file mode 100644 index 87c9b75f66..0000000000 --- a/database/fr/type-conversion.texy +++ /dev/null @@ -1,55 +0,0 @@ -Conversion de types -******************* - -.[perex] -Nette Database convertit automatiquement les valeurs renvoyées par la base de données vers les types PHP correspondants. - - -Date et heure -------------- - -Les valeurs temporelles sont converties en objets `Nette\Utils\DateTime`. Si vous voulez qu'elles soient converties en objets immuables `Nette\Database\DateTime`, activez l'option `newDateTime` dans la [configuration |configuration]. - -```php -$row = $database->fetch('SELECT created_at FROM articles'); -echo $row->created_at instanceof DateTime; // true -echo $row->created_at->format('j. n. Y'); -``` - -Dans le cas de MySQL, le type de données `TIME` est converti en objets `DateInterval`. - - -Valeurs booléennes ------------------- - -Les valeurs booléennes sont automatiquement converties en `true` ou `false`. Pour MySQL, `TINYINT(1)` est converti si nous activons `convertBoolean` dans la [configuration |configuration]. - -```php -$row = $database->fetch('SELECT is_published FROM articles'); -echo gettype($row->is_published); // 'boolean' -``` - - -Valeurs numériques ------------------- - -Les valeurs numériques sont converties en `int` ou en `float` selon le type de la colonne dans la base de données : - -```php -$row = $database->fetch('SELECT id, price FROM products'); -echo gettype($row->id); // integer -echo gettype($row->price); // float -``` - - -Normalisation personnalisée ---------------------------- - -À l'aide de la méthode `setRowNormalizer(?callable $normalizer)`, vous pouvez définir votre propre fonction de transformation des lignes venant de la base de données. C'est utile, par exemple, pour convertir automatiquement les types de données. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // la conversion des types a lieu ici - return $row; -}); -``` diff --git a/database/fr/upgrading.texy b/database/fr/upgrading.texy deleted file mode 100644 index fb78b20053..0000000000 --- a/database/fr/upgrading.texy +++ /dev/null @@ -1,36 +0,0 @@ -Mise à niveau -************* - - -Mise à niveau vers la version 3.2 -================================= - -La version minimale de PHP requise est 8.1. - -Le code a été soigneusement adapté à PHP 8.1. Toutes les nouvelles déclarations de type des méthodes et des propriétés ont été ajoutées. Les changements sont mineurs : - -- MySQL : la date nulle `0000-00-00` est renvoyée comme `null` -- MySQL : un decimal sans décimales est renvoyé comme int et non comme float -- le type `time` est renvoyé comme objet `DateTime` avec la date fixée au `0001-01-01` au lieu de la date du jour - - -Mise à niveau vers la version 3.1 -================================= - -- la classe `Nette\Database\Context` a été renommée en `Nette\Database\Explorer` par cohérence avec le nom [Database Explorer|explorer] -- les interfaces `Nette\Database\IRow` et `Nette\Database\IRowContainer` sont marquées comme obsolètes, car inutiles -- le driver `MySqlDriver` utilise des sous-requêtes -- le traducteur d'instructions SQL contrôle mieux les endroits où des tableaux peuvent être passés - - -Mise à niveau vers la version 3.0 -================================= - -Certaines méthodes, comme `fetch()` ou `fetchField()`, renvoient désormais `null` au lieu de `false` lorsqu'il n'y a pas de ligne suivante. - - -Mise à niveau vers la version 2.3 -================================= - -- le `MySqlDriver` utilise par défaut l'encodage `utf8mb4` pour MySQL >= 5.5.3 au lieu d'`utf8` -- `IReflection` a été scindée en deux interfaces jumelles, `IStructure` et `IConventions` diff --git a/database/it/@home.texy b/database/it/@home.texy deleted file mode 100644 index 0c342c6b51..0000000000 --- a/database/it/@home.texy +++ /dev/null @@ -1,18 +0,0 @@ -Database supportati -=================== - -Sono supportati questi server di database: - -|* Server di database |* Nome DSN |* Supporto in Core |* Supporto in Explorer -| MySQL (>= 5.1) | mysql | SÌ | SÌ -| PostgreSQL (>= 9.0) | pgsql | SÌ | SÌ -| Sqlite 3 (>= 3.8) | sqlite | SÌ | SÌ -| Oracle | oci | SÌ | - -| MS SQL (PDO_SQLSRV) | sqlsrv | SÌ | SÌ -| MS SQL (PDO_DBLIB) | mssql | SÌ | - -| ODBC | odbc | SÌ | - - - - -{{maintitle: Nette Database - eccellente livello di accesso al database per PHP}} -{{description: Nette Database semplifica in modo significativo l'ottenimento dei dati dal database senza scrivere query SQL. Esegue query efficienti e non trasferisce dati inutili.}} diff --git a/database/it/@left-menu.texy b/database/it/@left-menu.texy deleted file mode 100644 index ffec3672aa..0000000000 --- a/database/it/@left-menu.texy +++ /dev/null @@ -1,21 +0,0 @@ -Nette Database -************** -- [Primi passi |guide] -- [Approccio SQL|sql-way] -- [Explorer|explorer] -- [Transazioni|transactions] -- [Eccezioni|exceptions] -- [Reflection|reflection] -- [Conversione dei tipi |type-conversion] -- [Configurazione|configuration] -- [Rischi di sicurezza |security] -- [Aggiornamento|upgrading] - - -Letture consigliate -******************* -- [Documentazione di Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Best practice |best-practices:] -- [Risoluzione dei problemi |nette:troubleshooting] diff --git a/database/it/@meta.texy b/database/it/@meta.texy deleted file mode 100644 index 4647d0c8a2..0000000000 --- a/database/it/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentazione Nette}} diff --git a/database/it/configuration.texy b/database/it/configuration.texy deleted file mode 100644 index 976d2054fd..0000000000 --- a/database/it/configuration.texy +++ /dev/null @@ -1,110 +0,0 @@ -Configurazione del database -*************************** - -.[perex] -Panoramica delle opzioni di configurazione di Nette Database. - -Se non usate tutto il framework, ma solo questa libreria, leggete [come caricare la configurazione|bootstrap:]. - - -Connessione singola -------------------- - -Configurazione di una singola connessione al database: - -```neon -database: - # DSN, l'unica chiave obbligatoria - dsn: "sqlite:%appDir%/Model/demo.db" - user: ... - password: ... -``` - -Vengono creati i servizi `Nette\Database\Connection` e `Nette\Database\Explorer`, che di solito si passano tramite [autowiring |dependency-injection:autowiring] oppure facendo riferimento al [loro nome |#Servizi DI]. - -Altre impostazioni: - -```neon -database: - # mostrare il pannello del database nella Tracy Bar? - debugger: ... # (bool) predefinito è attivo se Tracy è attiva - - # mostrare l'EXPLAIN della query nella Tracy Bar? - explain: ... # (bool) predefinito true - - # abilitare l'autowiring per questa connessione? - autowired: ... # (bool) predefinito true per la prima connessione - - # convenzioni delle tabelle: discovered, static oppure il nome di una classe - conventions: discovered # (string) predefinito 'discovered' - - options: - # connettersi al database solo quando serve? - lazy: ... # (bool) predefinito false - - # classe del driver di database PHP - driverClass: # (string) - - # solo MySQL: imposta sql_mode - sqlmode: # (string) - - # solo MySQL: imposta SET NAMES - charset: # (string) predefinito 'utf8mb4' - - # solo MySQL: converte TINYINT(1) in bool - convertBoolean: # (bool) predefinito false - - # restituisce le colonne di data come oggetti immutabili (dalla versione 3.2.1) - newDateTime: # (bool) predefinito false - - # solo Oracle e SQLite: formato per salvare la data - formatDateTime: # (string) predefinito 'U' -``` - -La chiave `options` può contenere altre opzioni che trovate nella [documentazione dei driver PDO |https://www.php.net/manual/en/pdo.drivers.php], per esempio: - -```neon -database: - options: - PDO::MYSQL_ATTR_COMPRESS: true -``` - - -Connessioni multiple --------------------- - -Nella configurazione possiamo definire più connessioni al database dividendole in sezioni con un nome: - -```neon -database: - main: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password - - another: - dsn: 'sqlite::memory:' -``` - -L'autowiring è attivo solo per i servizi della prima sezione. Lo si può cambiare con `autowired: false` oppure `autowired: true`. - - -Servizi DI ----------- - -Al container DI vengono aggiunti questi servizi, dove `###` rappresenta il nome della connessione: - -| Nome | Tipo | Descrizione -|---------------------------|---------------------------------|--------------------------- -| `database.###.connection` | [api:Nette\Database\Connection] | connessione al database -| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |explorer] - - -Se definiamo una sola connessione, i nomi dei servizi saranno `database.default.connection` e `database.default.explorer`. Se definiamo più connessioni come nell'esempio sopra, i nomi corrisponderanno alle sezioni, cioè `database.main.connection`, `database.main.explorer` e poi `database.another.connection` e `database.another.explorer`. - -I servizi non autowired si passano esplicitamente facendo riferimento al loro nome: - -```neon -services: - - UserFacade(@database.another.connection) -``` diff --git a/database/it/exceptions.texy b/database/it/exceptions.texy deleted file mode 100644 index bc6531f582..0000000000 --- a/database/it/exceptions.texy +++ /dev/null @@ -1,37 +0,0 @@ -Eccezioni -********* - -Nette Database usa una gerarchia di eccezioni. La classe di base è `Nette\Database\DriverException`, che estende `PDOException` e offre funzionalità avanzate per lavorare con gli errori del database: - -- Il metodo `getDriverCode()` restituisce il codice di errore del driver del database. -- Il metodo `getSqlState()` restituisce il codice SQLSTATE. -- I metodi `getQueryString()` e `getParameters()` permettono di ottenere la query originale e i suoi parametri. - -Dalla classe `DriverException` derivano queste eccezioni specializzate: - -- `ConnectionException` - segnala il fallimento della connessione al server del database. - - `ConnectionLostException` .{data-version:3.2.9} - la connessione è caduta durante un'operazione (riavvio del server, guasto di rete, idle timeout); prima di continuare a usarla serve una riconnessione. -- `ConstraintViolationException` - classe base per le violazioni dei vincoli del database, dalla quale ereditano queste eccezioni: - - `ForeignKeyConstraintViolationException` - violazione di un vincolo di chiave esterna. - - `NotNullConstraintViolationException` - violazione di un vincolo NOT NULL. - - `UniqueConstraintViolationException` - violazione di un vincolo di unicità. - - `CheckConstraintViolationException` .{data-version:3.2.9} - violazione di un vincolo CHECK. -- `DeadlockException` .{data-version:3.2.9} - deadlock o conflitto di serializzazione rilevato dal server; la transazione è stata annullata e si può ritentare. -- `LockTimeoutException` .{data-version:3.2.9} - è stato superato il tempo di attesa di un lock; il comando è stato interrotto, ma la transazione circostante di solito resta aperta. - -L'esempio seguente mostra come catturare l'eccezione `UniqueConstraintViolationException`, che si verifica quando si cerca di inserire un utente con una email già presente nel database (supponendo che la colonna `email` abbia un indice univoco): - -```php -try { - $database->query('INSERT INTO users', [ - 'email' => 'john@example.com', - 'name' => 'John Doe', - 'password' => $hashedPassword, - ]); -} catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'Un utente con questa email esiste già.'; - -} catch (Nette\Database\DriverException $e) { - echo 'Durante la registrazione si è verificato un errore: ' . $e->getMessage(); -} -``` diff --git a/database/it/explorer.texy b/database/it/explorer.texy deleted file mode 100644 index 508447a718..0000000000 --- a/database/it/explorer.texy +++ /dev/null @@ -1,912 +0,0 @@ -Database Explorer -***************** - -<div class=perex> - -Explorer offre un modo intuitivo ed efficiente di lavorare con il database. Gestisce automaticamente le relazioni tra le tabelle e ottimizza le query, così potete concentrarvi sulla logica della vostra applicazione. Funziona subito, senza configurazione. Se avete bisogno del pieno controllo sulle query SQL, potete usare l'[approccio SQL |SQL way]. - -- Lavorare con i dati è naturale e facile da capire -- Genera query SQL ottimizzate che caricano solo i dati necessari -- Permette un accesso semplice ai dati collegati senza dover scrivere query JOIN -- Funziona subito, senza alcuna configurazione né generazione di entità - -</div> - - -Il lavoro con Explorer comincia chiamando il metodo `table()` sull'oggetto [api:Nette\Database\Explorer] (i dettagli su come impostare la connessione al database li trovate nel capitolo [Connessione e configurazione |guide#Connessione e configurazione]): - -```php -$books = $explorer->table('book'); // 'book' è il nome della tabella -``` - -Il metodo restituisce un oggetto [Selection |api:Nette\Database\Table\Selection], che rappresenta una query SQL. A questo oggetto si possono concatenare altri metodi per filtrare e ordinare i risultati. La query viene composta ed eseguita solo nel momento in cui si richiedono i dati, per esempio iterando con `foreach`. Ogni riga è rappresentata da un oggetto [ActiveRow |api:Nette\Database\Table\ActiveRow]: - -```php -foreach ($books as $book) { - echo $book->title; // stampa la colonna 'title' - echo $book->author_id; // stampa la colonna 'author_id' -} -``` - -Explorer semplifica enormemente il lavoro con le [relazioni tra le tabelle |#Relazioni tra le tabelle]. L'esempio seguente mostra con quanta facilità possiamo mostrare dati provenienti da tabelle collegate (libri e i loro autori). Notate che non serve scrivere alcuna query JOIN, ci pensa Nette a generarle: - -```php -$books = $explorer->table('book'); - -foreach ($books as $book) { - echo 'Libro: ' . $book->title; - echo 'Autore: ' . $book->author->name; // crea un JOIN alla tabella 'author' -} -``` - -Nette Database Explorer ottimizza le query per la massima efficienza. L'esempio sopra esegue solo due query SELECT, indipendentemente dal fatto che elaboriamo 10 o 10.000 libri. - -Explorer tiene inoltre traccia di quali colonne vengono usate nel codice e carica dal database solo quelle, risparmiando altre prestazioni. Questo comportamento è del tutto automatico e adattivo. Se in seguito modificate il codice per usare altre colonne, Explorer adatta automaticamente le query. Non dovete configurare nulla né pensare a quali colonne serviranno: lasciate fare a Nette. - - -Filtraggio e ordinamento -======================== - -La classe `Selection` offre i metodi per filtrare e ordinare la selezione dei dati. - -.[language-php] -| `where($condition, ...$params)` | Aggiunge una condizione WHERE. Più condizioni si uniscono con AND | -| `whereOr(array $conditions)` | Aggiunge un gruppo di condizioni WHERE unite con OR | -| `wherePrimary($value)` | Aggiunge una condizione WHERE sulla chiave primaria | -| `order($columns, ...$params)` | Imposta l'ordinamento con ORDER BY | -| `select($columns, ...$params)` | Indica quali colonne caricare | -| `limit($limit, $offset = null)` | Limita il numero di righe (LIMIT) e imposta eventualmente OFFSET | -| `page($page, $itemsPerPage, &$numOfPages = null)` | Imposta la paginazione | -| `group($columns, ...$params)` | Raggruppa le righe (GROUP BY) | -| `having($condition, ...$params)`| Aggiunge una condizione HAVING per filtrare le righe raggruppate | - -I metodi si possono concatenare (la cosiddetta [interfaccia fluent |nette:introduction-to-object-oriented-programming#Interfacce fluent]): `$table->where(...)->order(...)->limit(...)`. - -In questi metodi potete usare anche le notazioni speciali per accedere ai [dati delle tabelle collegate |#Interrogare attraverso le tabelle collegate]. - - -Escaping e identificatori -------------------------- - -I metodi eseguono automaticamente l'escaping dei parametri e quotano gli identificatori (nomi di tabelle e colonne), il che previene la SQL injection. Perché tutto funzioni correttamente bisogna rispettare alcune regole: - -- Scrivete le parole chiave, i nomi di funzioni, procedure ecc. in **maiuscolo**. -- Scrivete i nomi di colonne e tabelle in **minuscolo**. -- Passate sempre le stringhe tramite **parametri**. - -```php -where('name = ' . $name); // FALLA CRITICA: SQL injection -where('name LIKE "%search%"'); // SBAGLIATO: complica la quotatura automatica -where('name LIKE ?', '%search%'); // CORRETTO: valore passato come parametro - -where('name like ?', $name); // SBAGLIATO: genera: `name` `like` ? -where('name LIKE ?', $name); // CORRETTO: genera: `name` LIKE ? -where('LOWER(name) = ?', $value);// CORRETTO: LOWER(`name`) = ? -``` - - -where(string|array $condition, ...$parameters): static .[method] ----------------------------------------------------------------- - -Filtra i risultati con le condizioni WHERE. La sua forza sta nel gestire in modo intelligente i vari tipi di valore e nello scegliere automaticamente gli operatori SQL adatti. - -Uso di base: - -```php -$table->where('id', $value); // WHERE `id` = 123 -$table->where('id > ?', $value); // WHERE `id` > 123 -$table->where('id = ? OR name = ?', $id, $name); // WHERE `id` = 1 OR `name` = 'Jon Snow' -``` - -Grazie al riconoscimento automatico dell'operatore adatto non dovete occuparvi dei vari casi particolari, li risolve Nette per voi: - -```php -$table->where('id', 1); // WHERE `id` = 1 -$table->where('id', null); // WHERE `id` IS NULL -$table->where('id', [1, 2, 3]); // WHERE `id` IN (1, 2, 3) -// potete usare anche il segnaposto ? senza operatore: -$table->where('id ?', 1); // WHERE `id` = 1 -``` - -Il metodo gestisce correttamente anche le condizioni negative e gli array vuoti: - -```php -$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- non trova nulla -$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- trova tutto -$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- trova tutto -// $table->where('NOT id ?', $ids); // ATTENZIONE: questa sintassi non è supportata -``` - -Come parametro potete passare anche il risultato di un'altra query alla tabella, creando così una sottoquery: - -```php -// WHERE `id` IN (SELECT `id` FROM `tableName`) -$table->where('id', $explorer->table($tableName)); - -// WHERE `id` IN (SELECT `col` FROM `tableName`) -$table->where('id', $explorer->table($tableName)->select('col')); -``` - -Le condizioni si possono passare anche come array, i cui elementi vengono uniti con AND: - -```php -// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) -$table->where([ - 'price_final < price_original', - 'stock_count > min_stock', -]); -``` - -Nell'array potete usare le coppie chiave => valore e Nette sceglierà di nuovo automaticamente gli operatori corretti: - -```php -// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3)) -$table->where([ - 'status' => 'active', - 'id' => [1, 2, 3], -]); -``` - -Nell'array potete combinare espressioni SQL con segnaposto e più parametri. Il che è adatto alle condizioni complesse con operatori definiti con precisione: - -```php -// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) -$table->where([ - 'age > ?' => 18, - 'ROUND(score, ?) > ?' => [2, 75.5], // i due parametri si passano come array -]); -``` - -Più chiamate a `where()` uniscono automaticamente le condizioni con AND. - - -whereOr(array $parameters): static .[method] --------------------------------------------- - -Analogamente a `where()` aggiunge delle condizioni, ma le unisce con OR: - -```php -// WHERE (`status` = 'active') OR (`deleted` = 1) -$table->whereOr([ - 'status' => 'active', - 'deleted' => true, -]); -``` - -Anche qui si possono usare espressioni più complesse: - -```php -// WHERE (`price` > 1000) OR (`price_with_tax` > 1500) -$table->whereOr([ - 'price > ?' => 1000, - 'price_with_tax > ?' => 1500, -]); -``` - - -wherePrimary(mixed $key): static .[method] ------------------------------------------- - -Aggiunge una condizione sulla chiave primaria della tabella: - -```php -// WHERE `id` = 123 -$table->wherePrimary(123); - -// WHERE `id` IN (1, 2, 3) -$table->wherePrimary([1, 2, 3]); -``` - -Se la tabella ha una chiave primaria composta (per esempio `foo_id`, `bar_id`), passatela come array: - -```php -// WHERE `foo_id` = 1 AND `bar_id` = 5 -$table->wherePrimary(['foo_id' => 1, 'bar_id' => 5])->fetch(); - -// WHERE (`foo_id`, `bar_id`) IN ((1, 5), (2, 3)) -$table->wherePrimary([ - ['foo_id' => 1, 'bar_id' => 5], - ['foo_id' => 2, 'bar_id' => 3], -])->fetchAll(); -``` - - -order(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Determina l'ordine in cui vengono restituite le righe. Potete ordinare per una o più colonne, in ordine crescente o decrescente, oppure secondo un'espressione personalizzata: - -```php -$table->order('created'); // ORDER BY `created` -$table->order('created DESC'); // ORDER BY `created` DESC -$table->order('priority DESC, created'); // ORDER BY `priority` DESC, `created` -$table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC -``` - - -select(string $columns, ...$parameters): static .[method] ---------------------------------------------------------- - -Indica le colonne da restituire dal database. Per impostazione predefinita Nette Database Explorer restituisce solo le colonne effettivamente usate nel codice. Usate il metodo `select()` quando avete bisogno di ottenere espressioni specifiche: - -```php -// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` -$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); -``` - -Gli alias definiti con `AS` sono poi accessibili come proprietà dell'oggetto `ActiveRow`: - -```php -foreach ($table as $row) { - echo $row->formatted_date; // accesso all'alias -} -``` - - -limit(?int $limit, ?int $offset = null): static .[method] ---------------------------------------------------------- - -Limita il numero di righe restituite (LIMIT) e permette eventualmente di impostare uno scostamento: - -```php -$table->limit(10); // LIMIT 10 (restituisce le prime 10 righe) -$table->limit(10, 20); // LIMIT 10 OFFSET 20 -``` - -Per la paginazione è più adatto il metodo `page()`. - - -page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] -------------------------------------------------------------------------- - -Facilita la paginazione dei risultati. Accetta il numero di pagina (a partire da 1) e il numero di elementi per pagina. Come opzione potete passare il riferimento a una variabile in cui verrà salvato il numero totale di pagine: - -```php -$numOfPages = null; -$table->page(page: 3, itemsPerPage: 10, numOfPages: $numOfPages); -echo "Pagine totali: $numOfPages"; -``` - - -group(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Raggruppa le righe secondo le colonne indicate (GROUP BY). Si usa di solito insieme alle funzioni di aggregazione: - -```php -// conta il numero di prodotti in ogni categoria -$table->select('category_id, COUNT(*) AS count') - ->group('category_id'); -``` - - -having(string $having, ...$parameters): static .[method] --------------------------------------------------------- - -Imposta una condizione per filtrare le righe raggruppate (HAVING). Si può usare insieme al metodo `group()` e alle funzioni di aggregazione: - -```php -// trova le categorie che hanno più di 100 prodotti -$table->select('category_id, COUNT(*) AS count') - ->group('category_id') - ->having('count > ?', 100); -``` - - -Leggere i dati -============== - -Per leggere i dati dal database sono disponibili diversi metodi utili: - -.[language-php] -| `foreach ($table as $key => $row)` | Itera su tutte le righe, `$key` è il valore della chiave primaria, `$row` è un oggetto ActiveRow | -| `$row = $table->get($key)` | Restituisce una singola riga in base alla chiave primaria | -| `$row = $table->fetch()` | Restituisce la riga corrente e sposta il puntatore a quella successiva | -| `$array = $table->fetchPairs()` | Crea un array associativo dai risultati | -| `$array = $table->fetchAll()` | Restituisce tutte le righe come array | -| `count($table)` | Restituisce il numero di righe nell'oggetto Selection | - -L'oggetto [ActiveRow |api:Nette\Database\Table\ActiveRow] è di sola lettura. Questo significa che non potete cambiare i valori delle sue proprietà. Questa limitazione garantisce la coerenza dei dati ed evita effetti collaterali imprevisti. I dati vengono caricati dal database e ogni modifica va fatta in modo esplicito e controllato. - - -`foreach` - iterazione su tutte le righe ----------------------------------------- - -Il modo più semplice di eseguire una query e ottenere le righe è iterare con un ciclo `foreach`. Esegue automaticamente la query SQL. - -```php -$books = $explorer->table('book'); -foreach ($books as $key => $book) { - // $key è il valore della chiave primaria, $book è un ActiveRow - echo "$book->title ({$book->author->name})"; -} -``` - - -get($key): ?ActiveRow .[method] -------------------------------- - -Esegue la query SQL e restituisce la riga in base alla chiave primaria, oppure `null` se non esiste. - -```php -$book = $explorer->table('book')->get(123); // restituisce l'ActiveRow con ID 123 oppure null -if ($book) { - echo $book->title; -} -``` - - -fetch(): ?ActiveRow .[method] ------------------------------ - -Restituisce la riga corrente e sposta il puntatore interno a quella successiva. Se non esistono altre righe, restituisce `null`. - -```php -$books = $explorer->table('book'); -while ($book = $books->fetch()) { - $this->processBook($book); -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Restituisce i risultati come array associativo. Il primo argomento indica il nome della colonna da usare come chiave dell'array, il secondo il nome della colonna da usare come valore: - -```php -$authors = $explorer->table('author')->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Se viene indicato solo il primo parametro, il valore sarà l'intera riga, cioè l'oggetto `ActiveRow`: - -```php -$authors = $explorer->table('author')->fetchPairs('id'); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - -In caso di chiavi duplicate viene usato il valore dell'ultima riga. Usando `null` come chiave, l'array sarà indicizzato numericamente a partire da zero (in tal caso non si verificano collisioni): - -```php -$authors = $explorer->table('author')->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -In alternativa potete passare come parametro un callback che per ogni riga restituirà o un singolo valore, oppure una coppia chiave-valore. - -```php -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); -// ['Primo libro (John Novak)', ...] - -// il callback può anche restituire un array con la coppia chiave e valore: -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => [$row->title, $row->author->name]); -// ['Primo libro' => 'John Novak', ...] -``` - - -fetchAll(): array .[method] ---------------------------- - -Restituisce tutte le righe come array associativo di oggetti `ActiveRow`, dove le chiavi sono i valori delle chiavi primarie. - -```php -$allBooks = $explorer->table('book')->fetchAll(); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - - -count(): int .[method] ----------------------- - -Il metodo `count()` senza parametri restituisce il numero di righe nell'oggetto `Selection`: - -```php -$table->where('category', 1); -$count = $table->count(); -$count = count($table); // alternativa -``` - -Attenzione: `count()` con un parametro esegue la funzione di aggregazione COUNT nel database, vedi più sotto. - - -ActiveRow::toArray(): array .[method] -------------------------------------- - -Converte l'oggetto `ActiveRow` in un array associativo, dove le chiavi sono i nomi delle colonne e i valori i dati corrispondenti. - -```php -$book = $explorer->table('book')->get(1); -$bookArray = $book->toArray(); -// $bookArray sarà ['id' => 1, 'title' => '...', 'author_id' => ..., ...] -``` - - -Aggregazione -============ - -La classe `Selection` offre metodi per eseguire facilmente le funzioni di aggregazione (COUNT, SUM, MIN, MAX, AVG ecc.). - -.[language-php] -| `count($expr)` | Conta il numero di righe | -| `min($expr)` | Restituisce il valore minimo di una colonna | -| `max($expr)` | Restituisce il valore massimo di una colonna | -| `sum($expr)` | Restituisce la somma dei valori di una colonna | -| `aggregation($function)` | Permette una funzione di aggregazione qualsiasi, per esempio `AVG()` o `GROUP_CONCAT()` | - - -count(string $expr): int .[method] ----------------------------------- - -Esegue una query SQL con la funzione COUNT e restituisce il risultato. Il metodo serve a scoprire quante righe soddisfano una certa condizione: - -```php -$count = $table->count('*'); // SELECT COUNT(*) FROM `table` -$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` -``` - -Attenzione: [#count()] senza parametri restituisce solo il numero di righe nell'oggetto `Selection`. - - -min(string $expr) e max(string $expr) .[method] ------------------------------------------------ - -I metodi `min()` e `max()` restituiscono il valore minimo e massimo della colonna o dell'espressione indicata: - -```php -// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 -$maxPrice = $products->where('active', true) - ->max('price'); -``` - - -sum(string $expr): mixed .[method] ----------------------------------- - -Restituisce la somma dei valori della colonna o dell'espressione indicata: - -```php -// SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 -$totalPrice = $products->where('active', true) - ->sum('price * items_in_stock'); -``` - - -aggregation(string $function, ?string $groupFunction = null): mixed .[method] ------------------------------------------------------------------------------ - -Permette di eseguire una funzione di aggregazione qualsiasi. - -```php -// prezzo medio dei prodotti di una categoria -$avgPrice = $products->where('category_id', 1) - ->aggregation('AVG(price)'); - -// unisce i tag dei prodotti in un'unica stringa -$tags = $products->where('id', 1) - ->aggregation('GROUP_CONCAT(tag.name) AS tags') - ->fetch() - ->tags; -``` - -Se dobbiamo aggregare risultati che sono già frutto di una funzione di aggregazione e di un raggruppamento (per esempio `SUM(value)` su righe raggruppate), indichiamo come secondo argomento la funzione di aggregazione da applicare a questi risultati intermedi: - -```php -// calcola il prezzo totale dei prodotti in magazzino per le singole categorie e poi somma questi prezzi. -$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') - ->group('category_id') - ->aggregation('SUM(category_total)', 'SUM'); -``` - -In questo esempio calcoliamo prima il prezzo totale dei prodotti di ogni categoria (`SUM(price * stock) AS category_total`) e raggruppiamo i risultati per `category_id`. Poi usiamo `aggregation('SUM(category_total)', 'SUM')` per sommare questi totali intermedi `category_total`. Il secondo argomento `'SUM'` indica che ai risultati intermedi va applicata la funzione SUM. - - -Insert, Update e Delete -======================= - -Nette Database Explorer semplifica l'inserimento, l'aggiornamento e la cancellazione dei dati. Tutti i metodi citati lanciano in caso di errore una `Nette\Database\DriverException`. - - -Selection::insert(iterable $data) .[method] -------------------------------------------- - -Inserisce nuovi record nella tabella. - -**Inserimento di un singolo record:** - -Passate il nuovo record come array associativo o come oggetto iterabile (per esempio `ArrayHash`, usato nei [form |forms:]), dove le chiavi corrispondono ai nomi delle colonne della tabella. - -Se la tabella ha una chiave primaria definita, il metodo restituisce un oggetto `ActiveRow`, che viene ricaricato dal database per riflettere le eventuali modifiche fatte a livello di database (trigger, valori predefiniti delle colonne, calcolo delle colonne auto-increment). Questo garantisce la coerenza dei dati e l'oggetto contiene sempre i dati attuali dal database. Se la tabella non ha una chiave primaria, non esiste una riga identificabile e il metodo restituisce `null`. - -```php -$row = $explorer->table('users')->insert([ - 'name' => 'John Doe', - 'email' => 'john.doe@example.com', -]); -// $row è un'istanza di ActiveRow e contiene i dati completi della riga inserita, -// compreso l'ID generato automaticamente e le eventuali modifiche fatte dai trigger -echo $row->id; // stampa l'ID del nuovo utente inserito -echo $row->created_at; // stampa l'ora di creazione, se impostata da un trigger -``` - -**Inserimento di più record in una volta:** - -Il metodo `insert()` permette di inserire più record con un'unica query SQL. In tal caso restituisce il numero di righe inserite. - -```php -$insertedRows = $explorer->table('users')->insert([ - [ - 'name' => 'John', - 'year' => 1994, - ], - [ - 'name' => 'Jack', - 'year' => 1995, - ], -]); -// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) -// $insertedRows sarà 2 -``` - -Come parametro si può passare anche un oggetto `Selection` con una selezione di dati. - -```php -$newUsers = $explorer->table('potential_users') - ->where('approved', 1) - ->select('name, email'); - -$insertedRows = $explorer->table('users')->insert($newUsers); -``` - -**Inserimento di valori speciali:** - -Come valori possiamo passare anche file, oggetti `DateTime` o letterali SQL: - -```php -$explorer->table('users')->insert([ - 'name' => 'John', - 'created_at' => new DateTime, // converte nel formato del database - 'avatar' => fopen('image.jpg', 'rb'), // inserisce il contenuto binario del file - 'uuid' => $explorer::literal('UUID()'), // chiama la funzione UUID() -]); -``` - - -Selection::update(iterable $data): int .[method] ------------------------------------------------- - -Aggiorna le righe della tabella secondo il filtro indicato. Restituisce il numero di righe effettivamente modificate. - -Passate le colonne da modificare come array associativo o come oggetto iterabile (per esempio `ArrayHash`, usato nei [form |forms:]), dove le chiavi corrispondono ai nomi delle colonne della tabella: - -```php -$affected = $explorer->table('users') - ->where('id', 10) - ->update([ - 'name' => 'John Smith', - 'year' => 1994, - ]); -// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 -``` - -Per modificare i valori numerici potete usare gli operatori `+=` e `-=`: - -```php -$explorer->table('users') - ->where('id', 10) - ->update([ - 'points+=' => 1, // aumenta di 1 il valore della colonna 'points' - 'coins-=' => 1, // diminuisce di 1 il valore della colonna 'coins' - ]); -// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 -``` - - -Selection::delete(): int .[method] ----------------------------------- - -Cancella le righe dalla tabella secondo il filtro indicato. Restituisce il numero di righe cancellate. - -```php -$count = $explorer->table('users') - ->where('id', 10) - ->delete(); -// DELETE FROM `users` WHERE `id` = 10 -``` - -.[caution] -Quando chiamate `update()` o `delete()`, non dimenticate di indicare con `where()` le righe da modificare o cancellare. Se non usate `where()`, l'operazione verrà eseguita sull'intera tabella! - - -ActiveRow::update(iterable $data): bool .[method] -------------------------------------------------- - -Aggiorna i dati nella riga del database rappresentata dall'oggetto `ActiveRow`. Accetta un iterabile con i dati da aggiornare (le chiavi sono i nomi delle colonne). Per modificare i valori numerici potete usare gli operatori `+=` e `-=`: - -Dopo l'aggiornamento l'`ActiveRow` viene automaticamente ricaricato dal database per riflettere le eventuali modifiche fatte a livello di database (per esempio dai trigger). Il metodo restituisce `true` solo se è avvenuto un cambiamento reale dei dati. - -```php -$article = $explorer->table('article')->get(1); -$article->update([ - 'views += 1', // incrementa il numero di visualizzazioni -]); -echo $article->views; // stampa il numero attuale di visualizzazioni -``` - -Questo metodo aggiorna una sola riga concreta del database. Per l'aggiornamento massivo di più righe usate il metodo [#Selection::update()]. - - -ActiveRow::delete(): int .[method] ----------------------------------- - -Cancella dal database la riga rappresentata dall'oggetto `ActiveRow`. Restituisce il numero di righe cancellate, che dovrebbe essere 1. - -```php -$book = $explorer->table('book')->get(1); -$book->delete(); // cancella il libro con ID 1 -``` - -Questo metodo cancella una sola riga concreta del database. Per la cancellazione massiva di più righe usate il metodo [#Selection::delete()]. - - -Relazioni tra le tabelle -======================== - -Nei database relazionali i dati sono divisi in più tabelle e collegati tra loro con le chiavi esterne. Nette Database Explorer offre un modo rivoluzionario di lavorare con queste relazioni: senza scrivere query JOIN e senza dover configurare o generare nulla. - -Per mostrare il lavoro con le relazioni useremo come esempio un database di libri ([lo trovate su GitHub |https://github.com/nette-examples/books]). Nel database abbiamo le tabelle: - -- `author` - scrittori e traduttori (colonne `id`, `name`, `web`, `born`) -- `book` - libri (colonne `id`, `author_id`, `translator_id`, `title`, `sequel_id`) -- `tag` - tag (colonne `id`, `name`) -- `book_tag` - tabella di collegamento tra libri e tag (colonne `book_id`, `tag_id`) - -[* db-schema-1-.webp *] *** Struttura del database usata negli esempi .<> - -Nel nostro database di libri di esempio troviamo diversi tipi di relazione (anche se il modello è semplificato rispetto alla realtà): - -- **Uno a molti (1:N)** - Ogni libro **ha un** autore; un autore può scrivere **più** libri. -- **Zero a molti (0:N)** - Un libro **può avere** un traduttore; un traduttore può tradurre **più** libri. -- **Zero a uno (0:1)** - Un libro **può avere** un seguito. -- **Molti a molti (M:N)** - Un libro **può avere più** tag e un tag può essere assegnato a **più** libri. - -In queste relazioni c'è sempre una **tabella genitore** e una **tabella figlia**. Per esempio nella relazione tra autori e libri la tabella `author` è quella genitore e `book` quella figlia: potete immaginarvelo come se il libro "appartenesse" sempre a un autore. Questo si riflette anche nella struttura del database: la tabella figlia `book` contiene la chiave esterna `author_id`, che punta alla tabella genitore `author`. - -Se dobbiamo elencare i libri con i nomi dei loro autori, abbiamo due possibilità. O otteniamo i dati con un'unica query SQL usando JOIN: - -```sql -SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; -``` - -Oppure otteniamo i dati in due passaggi (prima i libri, poi i loro autori) e li mettiamo insieme in PHP: - -```sql -SELECT * FROM book; -SELECT * FROM author WHERE id IN (1, 2, 3); -- ID degli autori dei libri selezionati -``` - -Il secondo approccio è in realtà **più efficiente**, anche se può sorprendere. I dati vengono caricati una sola volta e si possono sfruttare meglio nella cache. Ed è proprio così che funziona Nette Database Explorer: fa tutto sotto il cofano e vi offre un'API elegante: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo 'titolo: ' . $book->title; - echo 'scritto da: ' . $book->author->name; // $book->author è un record della tabella 'author' - echo 'tradotto da: ' . $book->translator?->name; -} -``` - - -Accesso alla tabella genitore ------------------------------ - -L'accesso alla tabella genitore è semplice. Sono relazioni del tipo *un libro ha un autore* oppure *un libro può avere un traduttore*. Il record collegato si ottiene tramite una proprietà dell'oggetto ActiveRow, il cui nome corrisponde al nome della colonna con la chiave esterna senza il suffisso `_id`: - -```php -$book = $explorer->table('book')->get(1); -echo $book->author->name; // trova l'autore in base alla colonna author_id -echo $book->translator?->name; // trova il traduttore in base alla colonna translator_id -``` - -Quando si accede alla proprietà `$book->author`, Explorer cerca nella tabella `book` una colonna il cui nome contenga la stringa `author` (cioè `author_id`). In base al valore di questa colonna carica il record corrispondente dalla tabella `author` e lo restituisce come `ActiveRow`. Analogamente `$book->translator` usa la colonna `translator_id`. Poiché la colonna `translator_id` può contenere `null`, nel codice usiamo l'operatore nullsafe `?->`. - -Un approccio alternativo lo offre il metodo `ref()`, che accetta due argomenti (il nome della tabella di destinazione e il nome della colonna di collegamento) e restituisce un'istanza di `ActiveRow` oppure `null`: - -```php -echo $book->ref('author', 'author_id')->name; // relazione con l'autore -echo $book->ref('author', 'translator_id')->name; // relazione con il traduttore -``` - -Il metodo `ref()` torna utile quando non si può usare l'accesso tramite proprietà, per esempio perché la tabella contiene una colonna con lo stesso nome (cioè `author`). Negli altri casi si consiglia l'accesso tramite proprietà, per una migliore leggibilità. - -Explorer ottimizza automaticamente le query al database. Quando percorriamo i libri in un ciclo e accediamo ai loro record collegati (autori, traduttori), Explorer non genera una query per ogni libro. Esegue invece solo **una query SELECT per ogni tipo di relazione**, riducendo in modo significativo il carico sul database. Per esempio: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo $book->title . ': '; - echo $book->author->name; - echo $book->translator?->name; -} -``` - -Questo codice esegue solo queste tre velocissime query al database: - -```sql -SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- ID dalla colonna author_id dei libri selezionati -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- ID dalla colonna translator_id dei libri selezionati -``` - -.[note] -La logica con cui viene individuata la colonna di collegamento è determinata dall'implementazione di [Conventions |api:Nette\Database\Conventions]. Consigliamo di usare [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], che analizza le chiavi esterne e permette di lavorare facilmente con le relazioni esistenti tra le tabelle. - - -Accesso alla tabella figlia ---------------------------- - -L'accesso alla tabella figlia funziona nella direzione opposta. Ora ci chiediamo *quali libri ha scritto questo autore* oppure *quali libri ha tradotto questo traduttore*. Per questo tipo di query usiamo il metodo `related()`, che restituisce una `Selection` con i record collegati. Vediamo un esempio: - -```php -$author = $explorer->table('author')->get(1); - -// stampa tutti i libri dell'autore -foreach ($author->related('book.author_id') as $book) { - echo "Ha scritto: $book->title"; -} - -// stampa tutti i libri tradotti dall'autore -foreach ($author->related('book.translator_id') as $book) { - echo "Ha tradotto: $book->title"; -} -``` - -Il metodo `related()` accetta la descrizione del collegamento come un unico argomento con la notazione a punto, oppure come due argomenti separati: - -```php -$author->related('book.translator_id'); // un argomento -$author->related('book', 'translator_id'); // due argomenti -``` - -Explorer sa individuare automaticamente la colonna di collegamento corretta in base al nome della tabella genitore. In questo caso collega tramite la colonna `book.author_id`, perché il nome della tabella di partenza è `author`: - -```php -$author->related('book'); // usa book.author_id -``` - -Se esistessero più collegamenti possibili, Explorer lancia una [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -Il metodo `related()` lo possiamo naturalmente usare anche iterando su più record in un ciclo, e anche in questo caso Explorer ottimizza automaticamente le query: - -```php -$authors = $explorer->table('author'); -foreach ($authors as $author) { - echo $author->name . ' ha scritto:'; - foreach ($author->related('book') as $book) { - echo $book->title; - } -} -``` - -Questo codice genera solo due velocissime query SQL: - -```sql -SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- ID degli autori selezionati -``` - - -Relazione molti a molti ------------------------ - -Per la relazione molti a molti (M:N) serve una **tabella di collegamento** (nel nostro caso `book_tag`), che contiene due colonne con chiavi esterne (`book_id`, `tag_id`). Ognuna di queste colonne punta alla chiave primaria di una delle tabelle collegate. Per ottenere i dati collegati prendiamo prima i record dalla tabella di collegamento con `related('book_tag')` e da lì proseguiamo verso i dati di destinazione: - -```php -$book = $explorer->table('book')->get(1); -// stampa i nomi dei tag assegnati al libro -foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name; // stampa il nome del tag tramite la tabella di collegamento -} - -$tag = $explorer->table('tag')->get(1); -// oppure al contrario: stampa i nomi dei libri contrassegnati con questo tag -foreach ($tag->related('book_tag') as $bookTag) { - echo $bookTag->book->title; // stampa il titolo del libro -} -``` - -Explorer ottimizza di nuovo le query SQL in una forma efficiente: - -```sql -SELECT * FROM `book`; -SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- ID dei libri selezionati -SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- ID dei tag trovati in book_tag -``` - - -Interrogare attraverso le tabelle collegate -------------------------------------------- - -Nei metodi `where()`, `select()`, `order()` e `group()` potete usare notazioni speciali per accedere alle colonne di altre tabelle. Explorer crea automaticamente i JOIN necessari. - -La **notazione a punto** (`tabella_genitore.colonna`) si usa per le relazioni 1:N dal punto di vista della tabella figlia: - -```php -$books = $explorer->table('book'); - -// trova i libri il cui nome dell'autore inizia con 'Jon' -$books->where('author.name LIKE ?', 'Jon%'); - -// ordina i libri per nome dell'autore in ordine decrescente -$books->order('author.name DESC'); - -// stampa il titolo del libro e il nome dell'autore -$books->select('book.title, author.name'); -``` - -La **notazione con i due punti** (`:tabella_figlia.colonna`) si usa per le relazioni 1:N dal punto di vista della tabella genitore: - -```php -$authors = $explorer->table('author'); - -// trova gli autori che hanno scritto un libro con 'PHP' nel titolo -$authors->where(':book.title LIKE ?', '%PHP%'); - -// conta il numero di libri di ogni autore -$authors->select('*, COUNT(:book.id) AS book_count') - ->group('author.id'); -``` - -Nell'esempio sopra con la notazione a due punti (`:book.title`) non è indicata la colonna con la chiave esterna. Explorer individua automaticamente la colonna corretta in base al nome della tabella genitore. In questo caso collega tramite la colonna `book.author_id`, perché il nome della tabella di partenza è `author`. Se esistessero più collegamenti possibili, Explorer lancia una [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -La colonna di collegamento si può indicare esplicitamente tra parentesi: - -```php -// trova gli autori che hanno tradotto un libro con 'PHP' nel titolo -$authors->where(':book(translator_id).title LIKE ?', '%PHP%'); -``` - -Le notazioni si possono concatenare per accedere ai dati attraverso più tabelle: - -```php -// trova gli autori dei libri contrassegnati con il tag 'PHP' -$authors->where(':book:book_tag.tag.name', 'PHP') - ->group('author.id'); -``` - - -Estendere le condizioni per JOIN --------------------------------- - -Il metodo `joinWhere()` estende le condizioni indicate quando si collegano le tabelle in SQL dopo la parola chiave `ON`. - -Diciamo che vogliamo trovare i libri tradotti da un certo traduttore: - -```php -// trova i libri tradotti dal traduttore di nome 'David' -$books = $explorer->table('book') - ->joinWhere('translator', 'translator.name', 'David'); -// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') -``` - -Nella condizione di `joinWhere()` potete usare le stesse costruzioni del metodo `where()`: operatori, segnaposto, array di valori o espressioni SQL. - -Per query più complesse con più JOIN potete definire degli alias per le tabelle: - -```php -$tags = $explorer->table('tag') - ->joinWhere(':book_tag.book.author', 'book_author.born < ?', 1950) - ->alias(':book_tag.book.author', 'book_author'); -// LEFT JOIN `book_tag` ON `tag`.`id` = `book_tag`.`tag_id` -// LEFT JOIN `book` ON `book_tag`.`book_id` = `book`.`id` -// LEFT JOIN `author` `book_author` ON `book`.`author_id` = `book_author`.`id` -// AND (`book_author`.`born` < 1950) -``` - -Notate che mentre il metodo `where()` aggiunge condizioni alla clausola `WHERE`, il metodo `joinWhere()` estende le condizioni nella clausola `ON` quando si collegano le tabelle. diff --git a/database/it/guide.texy b/database/it/guide.texy deleted file mode 100644 index 83fe3e156c..0000000000 --- a/database/it/guide.texy +++ /dev/null @@ -1,218 +0,0 @@ -Nette Database -************** - -.[perex] -Nette Database è un livello di accesso al database per PHP potente ed elegante, concentrato sulla semplicità e su funzionalità intelligenti. Offre due modi di lavorare con il database: l'[Explorer |explorer] per uno sviluppo rapido delle applicazioni, oppure l'[approccio SQL |SQL way] per il controllo diretto delle query. - -<div class="grid gap-3"> -<div> - - -[Approccio SQL|sql-way] -======================= -- Query sicure e parametrizzate -- Controllo preciso sulla struttura della query SQL -- Quando scrivete query complesse con funzioni avanzate -- Ottimizzate le prestazioni con funzioni SQL specifiche - -</div> - -<div> - - -[Explorer |explorer] -==================== -- Sviluppate rapidamente senza scrivere SQL -- Gestione intuitiva delle relazioni tra le tabelle -- Approfittate dell'ottimizzazione automatica delle query -- Adatto a un lavoro rapido e comodo con il database - -</div> - -</div> - - -Installazione -============= - -La libreria si scarica e si installa con [Composer|best-practices:composer]: - -```shell -composer require nette/database -``` - - -Database supportati -=================== - -Nette Database supporta questi database: - -|* Server di database |* Nome DSN |* Supporto Explorer -|-----------------------|--------------|-----------------------| -| MySQL (>= 5.1) | mysql | SÌ | -| PostgreSQL (>= 9.0) | pgsql | SÌ | -| SQLite 3 (>= 3.8) | sqlite | SÌ | -| Oracle | oci | NO | -| MS SQL (PDO_SQLSRV) | sqlsrv | SÌ | -| MS SQL (PDO_DBLIB) | mssql | NO | -| ODBC | odbc | NO | - - -Due approcci al lavoro con il database -====================================== - -Nette Database vi lascia scegliere: potete scrivere le query SQL direttamente (approccio SQL), oppure lasciare che vengano generate automaticamente (Explorer). Vediamo come i due approcci risolvono gli stessi compiti: - -[Approccio SQL|sql-way] - query SQL - -```php -// inserimento di un record -$database->query('INSERT INTO books', [ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// ottenimento dei record: autori dei libri -$result = $database->query(' - SELECT authors.*, COUNT(books.id) AS books_count - FROM authors - LEFT JOIN books ON authors.id = books.author_id - WHERE authors.active = 1 - GROUP BY authors.id -'); - -// visualizzazione (non ottimale, genera N query aggiuntive) -foreach ($result as $author) { - $books = $database->query(' - SELECT * FROM books - WHERE author_id = ? - ORDER BY published_at DESC - ', $author->id); - - echo "L'autore $author->name ha scritto $author->books_count libri:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -[Approccio Explorer|explorer] - generazione automatica dell'SQL - -```php -// inserimento di un record -$database->table('books')->insert([ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// ottenimento dei record: autori dei libri -$authors = $database->table('authors') - ->where('active', 1); - -// visualizzazione (genera automaticamente solo 2 query ottimizzate) -foreach ($authors as $author) { - $books = $author->related('books') - ->order('published_at DESC'); - - echo "L'autore $author->name ha scritto {$books->count()} libri:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -L'approccio Explorer genera e ottimizza le query SQL automaticamente. Nell'esempio sopra l'approccio SQL genera N+1 query (una per gli autori e poi una per i libri di ogni autore), mentre Explorer ottimizza automaticamente le query ed esegue solo due: una per gli autori e una per tutti i loro libri. - -I due approcci si possono combinare liberamente nella vostra applicazione secondo le necessità. - - -Connessione e configurazione -============================ - -Per connettervi al database basta creare un'istanza della classe [api:Nette\Database\Connection]: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password); -``` - -Il parametro `$dsn` (Data Source Name) è lo stesso [usato da PDO |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], per esempio `host=127.0.0.1;dbname=test`. In caso di fallimento lancia una `Nette\Database\ConnectionException`. - -Un modo più comodo lo offre però la [configurazione dell'applicazione |configuration], dove basta aggiungere la sezione `database`. Vengono così creati gli oggetti necessari e anche il pannello del database nella barra di [Tracy |tracy:]. - -```neon -database: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password -``` - -L'oggetto della connessione si può poi [ottenere come servizio dal container DI |dependency-injection:passing-dependencies], per esempio: - -```php -class Model -{ - public function __construct( - // oppure Nette\Database\Explorer - private Nette\Database\Connection $database, - ) { - } -} -``` - -Maggiori informazioni sulla [configurazione del database|configuration]. - - -Creazione manuale dell'Explorer -------------------------------- - -Se non usate il container DI di Nette, potete creare a mano un'istanza di `Nette\Database\Explorer`: - -```php -// connessione al database -$connection = new Nette\Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); -// storage della cache, implementa Nette\Caching\Storage, per esempio: -$storage = new Nette\Caching\Storages\FileStorage('/percorso/verso/temp/dir'); -// si occupa della reflection della struttura del database -$structure = new Nette\Database\Structure($connection, $storage); -// definisce le regole per mappare nomi di tabelle, colonne e chiavi esterne -$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); -$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage); -``` - - -Gestione della connessione -========================== - -Quando si crea l'oggetto `Connection`, la connessione viene stabilita automaticamente. Se volete rimandare la connessione, usate la modalità lazy: attivatela nella [configurazione|configuration] impostando `lazy`, oppure così: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => true]); -``` - -Per gestire la connessione usate i metodi `connect()`, `disconnect()` e `reconnect()`. -- `connect()` crea la connessione se non esiste già e può lanciare una `Nette\Database\ConnectionException`. -- `disconnect()` chiude la connessione corrente al database. -- `reconnect()` esegue la disconnessione e la successiva riconnessione al database. Anche questo metodo può lanciare una `Nette\Database\ConnectionException`. - -Potete inoltre seguire gli eventi legati alla connessione con l'evento `onConnect`, che è un array di callback richiamati dopo che la connessione al database è stata stabilita. - -```php -// viene eseguito dopo la connessione al database -$database->onConnect[] = function($database) { - echo "Connesso al database"; -}; -``` - -In modo analogo funziona l'evento `onQuery`: è un array di callback richiamati dopo ogni query eseguita (e quando una query fallisce), utile per il logging o il profiling. - - -Tracy Debug Bar -=============== - -Se usate [Tracy |tracy:], il pannello Database nella Debug Bar si attiva automaticamente. Mostra tutte le query eseguite, i loro parametri, il tempo di esecuzione e il punto del codice da cui sono state richiamate. - -[* db-panel.webp *] diff --git a/database/it/reflection.texy b/database/it/reflection.texy deleted file mode 100644 index e3a6036b31..0000000000 --- a/database/it/reflection.texy +++ /dev/null @@ -1,127 +0,0 @@ -Reflection della struttura -************************** - -.{data-version:3.2.1} -Nette Database mette a disposizione strumenti per ispezionare la struttura del database con la classe [api:Nette\Database\Reflection]. Permette di ottenere informazioni su tabelle, colonne, indici e chiavi esterne. La reflection si può sfruttare per generare schemi, creare applicazioni flessibili che lavorano con il database oppure strumenti generici per i database. - -L'oggetto reflection si ottiene dall'istanza della connessione al database: - -```php -$reflection = $database->getReflection(); -``` - - -Ottenere le tabelle -------------------- - -La proprietà readonly `$reflection->tables` contiene un array associativo di tutte le tabelle del database: - -```php -// elenco dei nomi di tutte le tabelle -foreach ($reflection->tables as $name => $table) { - echo $name . "\n"; -} -``` - -Sono disponibili altri due metodi: - -```php -// verifica dell'esistenza di una tabella -if ($reflection->hasTable('users')) { - echo "La tabella users esiste"; -} - -// restituisce l'oggetto della tabella; se non esiste lancia un'eccezione -$table = $reflection->getTable('users'); -``` - - -Informazioni sulla tabella --------------------------- - -La tabella è rappresentata dall'oggetto [Table|api:Nette\Database\Reflection\Table], che offre queste proprietà readonly: - -- `$name: string` - nome della tabella -- `$view: bool` - se si tratta di una vista -- `$fullName: ?string` - nome completo della tabella compreso lo schema (se esiste) -- `$columns: array<string, Column>` - array associativo delle colonne della tabella -- `$indexes: Index[]` - array degli indici della tabella -- `$primaryKey: ?Index` - chiave primaria della tabella oppure null -- `$foreignKeys: ForeignKey[]` - array delle chiavi esterne della tabella -- `$comment: ?string` - commento della tabella - - -Colonne -------- - -La proprietà `columns` della tabella offre un array associativo di colonne, dove la chiave è il nome della colonna e il valore è un'istanza di [Column|api:Nette\Database\Reflection\Column] con queste proprietà: - -- `$name: string` - nome della colonna -- `$table: ?Table` - riferimento alla tabella della colonna -- `$nativeType: string` - tipo nativo del database -- `$size: ?int` - dimensione/lunghezza del tipo -- `$nullable: bool` - se la colonna può contenere NULL -- `$default: mixed` - valore predefinito della colonna -- `$autoIncrement: bool` - se la colonna è auto-increment -- `$primary: bool` - se fa parte della chiave primaria -- `$vendor: array` - metadati aggiuntivi specifici del sistema di database -- `$comment: ?string` - commento della colonna - -```php -foreach ($table->columns as $name => $column) { - echo "Colonna: $name\n"; - echo "Tipo: {$column->nativeType}\n"; - echo "Nullable: " . ($column->nullable ? 'Sì' : 'No') . "\n"; -} -``` - - -Indici ------- - -La proprietà `indexes` della tabella offre un array di indici, dove ogni indice è un'istanza di [Index|api:Nette\Database\Reflection\Index] con queste proprietà: - -- `$columns: Column[]` - array delle colonne che compongono l'indice -- `$unique: bool` - se l'indice è univoco -- `$primary: bool` - se si tratta della chiave primaria -- `$name: ?string` - nome dell'indice - -La chiave primaria della tabella si ottiene con la proprietà `primaryKey`, che restituisce un oggetto `Index` oppure `null` se la tabella non ha una chiave primaria. - -```php -// elenco degli indici -foreach ($table->indexes as $index) { - $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); - echo "Indice" . ($index->name ? " {$index->name}" : '') . ":\n"; - echo " Colonne: $columns\n"; - echo " Univoco: " . ($index->unique ? 'Sì' : 'No') . "\n"; -} - -// elenco della chiave primaria -if ($primaryKey = $table->primaryKey) { - $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); - echo "Chiave primaria: $columns\n"; -} -``` - - -Chiavi esterne --------------- - -La proprietà `foreignKeys` della tabella offre un array di chiavi esterne, dove ogni chiave esterna è un'istanza di [ForeignKey|api:Nette\Database\Reflection\ForeignKey] con queste proprietà: - -- `$foreignTable: Table` - la tabella referenziata -- `$localColumns: Column[]` - array delle colonne locali -- `$foreignColumns: Column[]` - array delle colonne referenziate -- `$name: string` - nome della chiave esterna - -```php -// elenco delle chiavi esterne -foreach ($table->foreignKeys as $fk) { - $localCols = implode(', ', array_map(fn($col) => $col->name, $fk->localColumns)); - $foreignCols = implode(', ', array_map(fn($col) => $col->name, $fk->foreignColumns)); - - echo "FK" . ($fk->name ? " {$fk->name}" : '') . ":\n"; - echo " $localCols -> {$fk->foreignTable->name}($foreignCols)\n"; -} -``` diff --git a/database/it/security.texy b/database/it/security.texy deleted file mode 100644 index 6fb5dc6902..0000000000 --- a/database/it/security.texy +++ /dev/null @@ -1,185 +0,0 @@ -Rischi di sicurezza -******************* - -<div class=perex> - -I database contengono spesso dati sensibili e permettono di eseguire operazioni pericolose. Per lavorare in sicurezza con Nette Database è fondamentale: - -- Capire la differenza tra API sicure e insicure -- Usare query parametrizzate -- Validare correttamente i dati in ingresso - -</div> - - -Che cos'è la SQL injection? -=========================== - -La SQL injection è il rischio di sicurezza più grave quando si lavora con i database. Nasce quando un input dell'utente non trattato diventa parte di una query SQL. L'attaccante può inserire comandi SQL propri e così: -- Ottenere un accesso non autorizzato ai dati -- Modificare o cancellare dati nel database -- Aggirare l'autenticazione - -```php -// ❌ CODICE PERICOLOSO - vulnerabile a SQL injection -$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); - -// l'attaccante può inserire per esempio il valore: ' OR '1'='1 -// e la query risultante sarà: SELECT * FROM users WHERE name = '' OR '1'='1' -// il che restituisce tutti gli utenti -``` - -Lo stesso vale per Database Explorer: - -```php -// ❌ CODICE PERICOLOSO - vulnerabile a SQL injection -$table->where('name = ' . $_GET['name']); -$table->where("name = '$_GET[name]'"); -``` - - -Query parametrizzate -==================== - -La difesa fondamentale contro la SQL injection sono le query parametrizzate. Nette Database offre diversi modi per usarle. - -Il più semplice è usare i **segnaposto con punto interrogativo**: - -```php -// ✅ query parametrizzata sicura -$database->query('SELECT * FROM users WHERE name = ?', $name); - -// ✅ condizione sicura in Explorer -$table->where('name = ?', $name); -``` - -Lo stesso vale per tutti gli altri metodi di [Database Explorer|explorer] che permettono di inserire espressioni con segnaposto e parametri. - -Per i comandi INSERT, UPDATE o per la clausola WHERE possiamo passare i valori in un array: - -```php -// ✅ INSERT sicuro -$database->query('INSERT INTO users', [ - 'name' => $name, - 'email' => $email, -]); - -// ✅ INSERT sicuro in Explorer -$table->insert([ - 'name' => $name, - 'email' => $email, -]); -``` - - -Validazione dei valori dei parametri -==================================== - -Le query parametrizzate sono la pietra angolare del lavoro sicuro con il database. I valori che vi inseriamo devono però superare più livelli di controllo: - - -Controllo dei tipi ------------------- - -**La cosa più importante è garantire il tipo di dato corretto dei parametri**: è una condizione necessaria per un uso sicuro di Nette Database. Il database presuppone che tutti i dati in ingresso abbiano il tipo di dato corretto, corrispondente alla colonna in questione. - -Se per esempio negli esempi precedenti `$name` fosse inaspettatamente un array invece di una stringa, Nette Database proverebbe a inserire tutti i suoi elementi nella query SQL, il che porterebbe a un errore. Perciò **non usate mai** dati non validati da `$_GET`, `$_POST` o `$_COOKIE` direttamente nelle query al database. - - -Validazione del formato ------------------------ - -Al secondo livello controlliamo il formato dei dati: per esempio se le stringhe sono nella codifica UTF-8 e la loro lunghezza corrisponde alla definizione della colonna, oppure se i valori numerici rientrano nell'intervallo consentito per il tipo di dato della colonna. - -A questo livello di validazione possiamo in parte affidarci al database stesso: molti database rifiutano i dati non validi. Il comportamento però può variare, alcuni possono troncare silenziosamente le stringhe lunghe o tagliare i numeri fuori intervallo. - - -Validazione specifica del dominio ---------------------------------- - -Il terzo livello riguarda i controlli logici specifici della vostra applicazione. Per esempio verificare che i valori dei select box corrispondano alle opzioni offerte, che i numeri rientrino nell'intervallo atteso (per esempio l'età 0-150 anni) oppure che le dipendenze reciproche tra i valori abbiano senso. - - -Modi consigliati di validare ----------------------------- - -- Usate i [form di Nette|forms:], che garantiscono automaticamente la corretta validazione di tutti gli input. -- Usate i [presenter|application:] e indicate i tipi di dato dei parametri nei metodi `action*()` e `render*()`. -- Oppure realizzate un vostro livello di validazione usando gli strumenti standard di PHP come `filter_var()`. - - -Lavoro sicuro con le colonne -============================ - -Nella sezione precedente abbiamo mostrato come validare correttamente i valori dei parametri. Quando però usiamo gli array nelle query SQL, dobbiamo prestare la stessa attenzione anche alle loro chiavi. - -```php -// ❌ CODICE PERICOLOSO - le chiavi dell'array non sono trattate -$database->query('INSERT INTO users', $_POST); -``` - -Nel caso dei comandi INSERT e UPDATE si tratta di una grave falla di sicurezza: l'attaccante può inserire o modificare una colonna qualsiasi del database. Potrebbe per esempio impostare `is_admin = 1` oppure inserire dati arbitrari in colonne sensibili (la cosiddetta Mass Assignment Vulnerability). - -Nelle condizioni WHERE è ancora più pericoloso, perché possono contenere degli operatori: - -```php -// ❌ CODICE PERICOLOSO - le chiavi dell'array non sono trattate -$_POST['salary >'] = 100000; -$database->query('SELECT * FROM users WHERE', $_POST); -// esegue la query WHERE (`salary` > 100000) -``` - -Con questo approccio l'attaccante può scoprire sistematicamente gli stipendi dei dipendenti. Può cominciare per esempio con una query sugli stipendi sopra 100.000, poi sotto 50.000 e restringendo via via l'intervallo può rivelare gli stipendi approssimativi di tutti i dipendenti. Questo tipo di attacco si chiama SQL enumeration. - -I metodi `where()` e `whereOr()` sono poi [molto più flessibili |explorer#where()] e supportano nelle chiavi e nei valori espressioni SQL, compresi operatori e funzioni. Questo dà all'attaccante la possibilità di eseguire una SQL injection: - -```php -// ❌ CODICE PERICOLOSO - l'attaccante può inserire SQL proprio -$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1']; -$table->where($_POST); -// esegue la query WHERE (0) UNION SELECT name, salary FROM users WHERE (1) -``` - -Questo attacco chiude la condizione originale con `0)`, aggiunge con `UNION` un proprio `SELECT` per ottenere dati sensibili dalla tabella `users` e con `WHERE (1)` chiude la query rendendola sintatticamente corretta. - - -Whitelist delle colonne ------------------------ - -Per lavorare in sicurezza con i nomi delle colonne ci serve un meccanismo che garantisca che l'utente possa lavorare solo con le colonne consentite e non possa aggiungerne di proprie. Potremmo provare a rilevare e bloccare i nomi di colonna pericolosi (blacklist), ma è un approccio inaffidabile: l'attaccante può sempre inventare un nuovo modo di scrivere un nome di colonna pericoloso che non avevamo previsto. - -È perciò molto più sicuro ribaltare la logica e definire un elenco esplicito di colonne consentite (whitelist): - -```php -// colonne che l'utente può modificare -$allowedColumns = ['name', 'email', 'active']; - -// rimuoviamo dall'input tutte le colonne non autorizzate -$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); - -// ✅ ora si può usare in sicurezza nelle query, per esempio: -$database->query('INSERT INTO users', $filteredData); -$table->update($filteredData); -$table->where($filteredData); -``` - - -Identificatori dinamici -======================= - -Per i nomi dinamici di tabelle e colonne usate il segnaposto `?name`. Garantisce il corretto escaping degli identificatori secondo la sintassi del database in uso (per esempio con i backtick in MySQL): - -```php -// ✅ uso sicuro di identificatori affidabili -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name', $column, $table); -// risultato in MySQL: SELECT `name` FROM `users` -``` - -Importante: usate il simbolo `?name` solo per valori affidabili definiti nel codice dell'applicazione. Per i valori provenienti dall'utente usate di nuovo una [whitelist |#Whitelist delle colonne]. Altrimenti vi esponete a rischi di sicurezza: - -```php -// ❌ PERICOLOSO - non usate mai l'input dell'utente -$database->query('SELECT ?name FROM users', $_GET['column']); -``` diff --git a/database/it/sql-way.texy b/database/it/sql-way.texy deleted file mode 100644 index 11c2c6bbcc..0000000000 --- a/database/it/sql-way.texy +++ /dev/null @@ -1,513 +0,0 @@ -Approccio SQL -************* - -.[perex] -Nette Database offre due modi di lavorare: potete scrivere le query SQL da soli (approccio SQL), oppure farle generare automaticamente (vedi [Explorer |explorer]). L'approccio SQL vi dà il pieno controllo sulle query e allo stesso tempo garantisce che vengano costruite in sicurezza. - -.[note] -I dettagli sulla connessione e la configurazione del database li trovate nel capitolo [Connessione e configurazione |guide#Connessione e configurazione]. - - -Interrogazione di base -====================== - -Per interrogare il database serve il metodo `query()`. Restituisce un oggetto [ResultSet |api:Nette\Database\ResultSet], che rappresenta il risultato della query. Se la query fallisce, il metodo [lancia un'eccezione|exceptions]. Il risultato della query si può percorrere con un ciclo `foreach` oppure usare uno dei [metodi di supporto |#Ottenere i dati]. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; -} -``` - -Per inserire in sicurezza i valori nelle query SQL usate le query parametrizzate. Nette Database lo rende estremamente semplice: basta aggiungere dopo la query SQL una virgola e il valore: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Con più parametri avete due possibilità. Potete alternare la query SQL e i parametri: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age); -``` - -Oppure scrivere prima tutta la query SQL e poi accodare tutti i parametri: - -```php -$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); -``` - - -Protezione contro la SQL injection -================================== - -Perché è importante usare query parametrizzate? Perché vi proteggono dall'attacco chiamato SQL injection, con cui un attaccante potrebbe inserire comandi SQL propri e ottenere così l'accesso ai dati del database o danneggiarli. - -.[warning] -**Non inserite mai le variabili direttamente nella query SQL!** Usate sempre query parametrizzate, che vi proteggono dalla SQL injection. - -```php -// ❌ CODICE PERICOLOSO - vulnerabile a SQL injection -$database->query("SELECT * FROM users WHERE name = '$name'"); - -// ✅ query parametrizzata sicura -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Prendete confidenza con i [possibili rischi di sicurezza |security]. - - -Tecniche di interrogazione -========================== - - -Condizioni WHERE ----------------- - -Le condizioni `WHERE` si possono scrivere come array associativo, dove le chiavi sono i nomi delle colonne e i valori i dati da confrontare. Nette Database sceglie automaticamente l'operatore SQL più adatto in base al tipo del valore. - -```php -$database->query('SELECT * FROM users WHERE', [ - 'name' => 'John', - 'active' => true, -]); -// WHERE `name` = 'John' AND `active` = 1 -``` - -Nella chiave potete anche indicare esplicitamente l'operatore di confronto: - -```php -$database->query('SELECT * FROM users WHERE', [ - 'age >' => 25, // usa l'operatore > - 'name LIKE' => '%John%', // usa l'operatore LIKE - 'email NOT LIKE' => '%example.com%', // usa l'operatore NOT LIKE -]); -// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' -``` - -Nette gestisce automaticamente i casi particolari come i valori `null` o gli array. - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name' => 'Laptop', // usa l'operatore = - 'category_id' => [1, 2, 3], // usa IN - 'description' => null, // usa IS NULL -]); -// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL -``` - -Per le condizioni negative usate l'operatore `NOT`: - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // usa l'operatore != - 'category_id NOT' => [1, 2, 3], // usa NOT IN - 'description NOT' => null, // usa IS NOT NULL - 'id NOT' => [], // viene saltato -]); -// WHERE `name` != 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL -``` - -Per impostazione predefinita le condizioni sono unite dall'operatore `AND`. Lo si può cambiare con il [segnaposto ?or |#Suggerimenti per la costruzione dell'SQL]. - - -Regole ORDER BY ---------------- - -La clausola `ORDER BY` si può scrivere con un array. Nelle chiavi indicate le colonne e il valore booleano determina l'ordine crescente (`true`) o decrescente (`false`): - -```php -$database->query('SELECT id FROM author ORDER BY', [ - 'id' => true, // crescente - 'name' => false, // decrescente -]); -// SELECT id FROM author ORDER BY `id`, `name` DESC -``` - - -Inserimento dei dati (INSERT) ------------------------------ - -Per inserire i record serve il comando SQL `INSERT`. - -```php -$values = [ - 'name' => 'John Doe', - 'email' => 'john@example.com', -]; -$database->query('INSERT INTO users ?', $values); -$userId = $database->getInsertId(); -``` - -Il metodo `getInsertId()` restituisce l'ID dell'ultima riga inserita. In alcuni database (per esempio PostgreSQL) bisogna indicare come parametro il nome della sequenza da cui generare l'ID, con `$database->getInsertId($sequenceId)`. - -Come parametri si possono passare anche [#Valori speciali], per esempio file, oggetti DateTime o tipi enum. - -Inserimento di più record in una volta: - -```php -$database->query('INSERT INTO users ?', [ - ['name' => 'User 1', 'email' => 'user1@mail.com'], - ['name' => 'User 2', 'email' => 'user2@mail.com'], -]); -``` - -L'INSERT multiplo è molto più veloce, perché viene eseguita una sola query al database invece di tante singole. - -**Nota di sicurezza:** non usate mai dati non validati come `$values`. Prendete confidenza con i [possibili rischi |security#Lavoro sicuro con le colonne]. - - -Aggiornamento dei dati (UPDATE) -------------------------------- - -Per aggiornare i record serve il comando SQL `UPDATE`. - -```php -// aggiornamento di un singolo record -$values = [ - 'name' => 'John Smith', -]; -$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); -``` - -Il numero di righe interessate lo restituisce `$result->getRowCount()`. - -Per l'`UPDATE` possiamo usare gli operatori `+=` e `-=`: - -```php -$database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // incrementa login_count -], 1); -``` - -Esempio di inserimento o aggiornamento di un record se esiste già. Usiamo la tecnica `ON DUPLICATE KEY UPDATE`: - -```php -$values = [ - 'name' => $name, - 'year' => $year, -]; -$database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', - $values + ['id' => $id], - $values, -); -// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978) -// ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 -``` - -Notate che Nette Database riconosce il contesto in cui il parametro array viene usato nel comando SQL e costruisce di conseguenza il codice SQL. Dal primo array ha quindi costruito `(id, name, year) VALUES (123, 'Jim', 1978)`, mentre il secondo lo ha convertito nella forma `name = 'Jim', year = 1978`. Ne parliamo più in dettaglio nella sezione [#Suggerimenti per la costruzione dell'SQL]. - - -Cancellazione dei dati (DELETE) -------------------------------- - -Per cancellare i record serve il comando SQL `DELETE`. Esempio con l'ottenimento del numero di righe cancellate: - -```php -$count = $database->query('DELETE FROM users WHERE id = ?', 1) - ->getRowCount(); -``` - - -Suggerimenti per la costruzione dell'SQL ----------------------------------------- - -Un suggerimento è un segnaposto particolare nella query SQL che determina come il valore del parametro debba essere convertito in un'espressione SQL: - -| Suggerimento | Descrizione | Usato automaticamente per -|-----------|-------------------------------------------------|----------------------------- -| `?name` | Serve a inserire nomi di tabelle o colonne | - -| `?values` | Genera `(chiave, ...) VALUES (valore, ...)` | `INSERT ... ?`, `REPLACE ... ?` -| `?set` | Genera le assegnazioni `chiave = valore, ...` | `SET ?`, `KEY UPDATE ?` -| `?and` | Unisce le condizioni dell'array con `AND` | `WHERE ?`, `HAVING ?` -| `?or` | Unisce le condizioni dell'array con `OR` | - -| `?order` | Genera la clausola `ORDER BY` | `ORDER BY ?`, `GROUP BY ?` - -Il segnaposto `?name` serve a inserire dinamicamente nella query i nomi di tabelle e colonne. Nette Database si occupa di quotare correttamente gli identificatori secondo le convenzioni del database (per esempio racchiudendoli tra backtick in MySQL). - -```php -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); -// SELECT `name` FROM `users` WHERE id = 1 (in MySQL) -``` - -**Attenzione:** usate il segnaposto `?name` solo per nomi di tabelle e colonne validati. Altrimenti vi esponete a [vulnerabilità di sicurezza |security#Identificatori dinamici]. - -Gli altri suggerimenti di solito non serve indicarli, perché Nette usa un'autodetezione intelligente quando costruisce la query SQL (vedi la terza colonna della tabella). Potete però usarli per esempio nel caso in cui vogliate unire le condizioni con `OR` invece che con `AND`: - -```php -$database->query('SELECT * FROM users WHERE ?or', [ - 'name' => 'John', - 'email' => 'john@example.com', -]); -// SELECT * FROM users WHERE `name` = 'John' OR `email` = 'john@example.com' -``` - - -Valori speciali ---------------- - -Oltre ai consueti tipi scalari (string, int, bool) potete passare come parametri anche valori speciali: - -- file: `fopen('image.gif', 'r')` inserisce il contenuto binario del file -- data e ora: gli oggetti `DateTimeInterface` vengono convertiti nel formato del database -- tipi enum: le istanze di `enum` vengono convertite nel loro valore -- letterali SQL: creati con `Connection::literal('NOW()')` vengono inseriti direttamente nella query - -```php -$database->query('INSERT INTO articles ?', [ - 'title' => 'Il mio articolo', - 'published_at' => new DateTimeImmutable, // oppure new DateTime - 'content' => fopen('image.png', 'r'), - 'state' => Status::Draft, -]); -``` - -Nei database che non hanno supporto nativo per il tipo di dato `datetime` (come SQLite e Oracle), gli oggetti `DateTime` e `DateTimeImmutable` vengono convertiti nel valore indicato nella [configurazione del database|configuration] dalla voce `formatDateTime` (il valore predefinito è `U`, cioè il timestamp Unix). - - -Letterali SQL -------------- - -In alcuni casi bisogna passare come valore del codice SQL grezzo, che non va trattato come stringa né sottoposto a escaping. A questo servono gli oggetti della classe `Nette\Database\SqlLiteral`. Si creano con il metodo `Connection::literal()`. - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - 'year >' => $database::literal('YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) -``` - -Oppure: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) -``` - -I letterali SQL possono contenere parametri: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > ? AND year < ?', $min, $max), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) -``` - -Il che permette combinazioni interessanti: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('?or', [ - 'active' => true, - 'role' => $role, - ]), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (`active` = 1 OR `role` = 'admin') -``` - - -Ottenere i dati -=============== - - -Scorciatoie per le query SELECT -------------------------------- - -Per semplificare l'ottenimento dei dati, `Connection` offre alcune scorciatoie che combinano la chiamata a `query()` con la successiva chiamata a `fetch*()`. Questi metodi accettano gli stessi parametri di `query()`, cioè la query SQL ed eventuali parametri. La descrizione completa dei metodi `fetch*()` la trovate [più sotto |#fetch()]. - -| `fetch($sql, ...$params): ?Row` | Esegue la query e restituisce la prima riga come oggetto `Row` oppure `null`. -| `fetchAll($sql, ...$params): array` | Esegue la query e restituisce tutte le righe come array di oggetti `Row`. -| `fetchPairs($sql, ...$params): array` | Esegue la query e restituisce un array associativo (coppie chiave => valore). -| `fetchField($sql, ...$params): mixed` | Esegue la query e restituisce il valore della prima colonna della prima riga. -| `fetchList($sql, ...$params): ?array` | Esegue la query e restituisce la prima riga come array indicizzato oppure `null`. - -Esempio: - -```php -// fetchField() - restituisce il valore della prima cella -$count = $database->query('SELECT COUNT(*) FROM articles') - ->fetchField(); -``` - - -`foreach` - iterazione sulle righe ----------------------------------- - -Dopo l'esecuzione della query viene restituito un oggetto [ResultSet|api:Nette\Database\ResultSet], che permette di percorrere i risultati in vari modi. Il modo più semplice di eseguire una query e ottenere le righe è iterare con un ciclo `foreach`. Questo metodo è il più parsimonioso in termini di memoria, perché carica i dati riga per riga e non tiene in memoria tutto il risultato in una volta. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; - // ... -} -``` - -.[note] -Il `ResultSet` si può percorrere una sola volta. Se avete bisogno di iterare ripetutamente, dovete prima caricare i dati in un array, per esempio con il metodo `fetchAll()`. - - -fetch(): ?Row .[method] ------------------------ - -Restituisce una riga come oggetto `Row`. Se non esistono altre righe, restituisce `null`. Sposta il puntatore interno alla riga successiva. - -```php -$result = $database->query('SELECT * FROM users'); -$row = $result->fetch(); // carica la prima riga -if ($row) { - echo $row->name; -} -``` - - -fetchAll(): array .[method] ---------------------------- - -Restituisce tutte le righe rimanenti del `ResultSet` come array di oggetti `Row`. - -```php -$result = $database->query('SELECT * FROM users'); -$rows = $result->fetchAll(); // carica tutte le righe -foreach ($rows as $row) { - echo $row->name; -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Restituisce i risultati come array associativo. Il primo argomento indica la colonna da usare come chiave, il secondo la colonna da usare come valore: - -```php -$result = $database->query('SELECT id, name FROM users'); -$names = $result->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Se viene indicato solo il primo parametro (`$key`), come valore verrà usata l'intera riga (l'oggetto `Row`): - -```php -$rows = $result->fetchPairs('id'); -// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...] -``` - -In caso di chiavi duplicate viene usato il valore dell'ultima riga. Usando `null` come chiave si ottiene un array indicizzato numericamente (a partire da zero), il che evita le collisioni di chiavi: - -```php -$names = $result->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -In alternativa potete indicare un callback che elabora ogni riga. Il callback può restituire un singolo valore oppure una coppia chiave-valore. - -```php -$result = $database->query('SELECT * FROM users'); -$items = $result->fetchPairs(fn($row) => "$row->id - $row->name"); -// ['1 - John', '2 - Jane', ...] - -// il callback può anche restituire un array con la coppia chiave e valore: -$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); -// ['John' => 46, 'Jane' => 21, ...] -``` - - -fetchField(): mixed .[method] ------------------------------ - -Restituisce il valore della prima colonna della riga corrente. Se non esistono altre righe, restituisce `null`. Sposta il puntatore interno alla riga successiva. - -```php -$result = $database->query('SELECT name FROM users'); -$name = $result->fetchField(); // carica il nome dalla prima riga -``` - - -fetchList(): ?array .[method] ------------------------------ - -Restituisce la riga come array indicizzato. Se non esistono altre righe, restituisce `null`. Sposta il puntatore interno alla riga successiva. - -```php -$result = $database->query('SELECT name, email FROM users'); -$row = $result->fetchList(); // ['John', 'john@example.com'] -``` - - -getRowCount(): ?int .[method] ------------------------------ - -Restituisce il numero di righe interessate dall'ultima query `UPDATE` o `DELETE`. Per le query `SELECT` restituisce il numero di righe del risultato. Questo però non è sempre noto, e in tal caso il metodo restituisce `null`. - - -getColumnCount(): ?int .[method] --------------------------------- - -Restituisce il numero di colonne del `ResultSet`. - - -Informazioni sulle query -======================== - -Per il debugging possiamo ottenere informazioni sull'ultima query eseguita: - -```php -echo $database->getLastQueryString(); // stampa la query SQL - -$result = $database->query('SELECT * FROM articles'); -echo $result->getQueryString(); // stampa la query SQL -echo $result->getTime(); // stampa il tempo di esecuzione in secondi -``` - -Per mostrare il risultato come tabella HTML potete usare: - -```php -$result = $database->query('SELECT * FROM articles'); -$result->dump(); -``` - -Il `ResultSet` offre informazioni sui tipi delle colonne: - -```php -$result = $database->query('SELECT * FROM articles'); -$types = $result->getColumnTypes(); - -foreach ($types as $column => $type) { - echo "$column è di tipo $type"; // per esempio 'id è di tipo int' -} -``` - - -Logging delle query -------------------- - -Possiamo realizzare un logging personalizzato delle query. L'evento `onQuery` è un array di callback richiamati dopo ogni query eseguita: - -```php -$database->onQuery[] = function ($database, $result) use ($logger) { - $logger->info('Query: ' . $result->getQueryString()); - $logger->info('Tempo: ' . $result->getTime()); - - if ($result->getRowCount() > 1000) { - $logger->warning('Risultato di grandi dimensioni: ' . $result->getRowCount() . ' righe'); - } -}; -``` diff --git a/database/it/transactions.texy b/database/it/transactions.texy deleted file mode 100644 index b0755210c8..0000000000 --- a/database/it/transactions.texy +++ /dev/null @@ -1,45 +0,0 @@ -Transazioni -*********** - -.[perex] -Le transazioni garantiscono che vengano eseguite o tutte le operazioni della transazione, oppure nessuna. Sono utili per mantenere la coerenza dei dati durante operazioni complesse. - -Il modo più semplice di usare le transazioni è questo: - -```php -$database->beginTransaction(); -try { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); - $database->commit(); -} catch (\Exception $e) { - $database->rollBack(); - throw $e; -} -``` - -Lo stesso risultato lo ottenete in modo molto più elegante con il metodo `transaction()`. Accetta un callback che viene eseguito all'interno della transazione. Se il callback termina senza eccezioni, la transazione viene confermata automaticamente. Se si verifica un'eccezione, la transazione viene annullata e l'eccezione viene propagata oltre. - -```php -$database->transaction(function ($database) use ($id) { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); -}); -``` - -Le chiamate a `transaction()` si possono annidare, il che rende facile comporre metodi che gestiscono ciascuno la propria transazione. Al database viene effettivamente inviata come `BEGIN`/`COMMIT` solo la transazione più esterna; le chiamate interne si limitano a tenere traccia della profondità di annidamento. Chiamare a mano `beginTransaction()`, `commit()` o `rollBack()` dentro il callback di `transaction()` provoca una `LogicException`. - -Il metodo `transaction()` può anche restituire valori: - -```php -$count = $database->transaction(function ($database) { - $result = $database->query('UPDATE users SET active = ?', true); - return $result->getRowCount(); // restituisce il numero di righe modificate -}); -``` diff --git a/database/it/type-conversion.texy b/database/it/type-conversion.texy deleted file mode 100644 index 970c33d09e..0000000000 --- a/database/it/type-conversion.texy +++ /dev/null @@ -1,55 +0,0 @@ -Conversione dei tipi -******************** - -.[perex] -Nette Database converte automaticamente i valori restituiti dal database nei corrispondenti tipi PHP. - - -Data e ora ----------- - -I valori temporali vengono convertiti in oggetti `Nette\Utils\DateTime`. Se volete che i valori temporali siano convertiti in oggetti immutabili `Nette\Database\DateTime`, impostate nella [configurazione |configuration] l'opzione `newDateTime` a true. - -```php -$row = $database->fetch('SELECT created_at FROM articles'); -echo $row->created_at instanceof DateTime; // true -echo $row->created_at->format('j. n. Y'); -``` - -Nel caso di MySQL il tipo di dato `TIME` viene convertito in oggetti `DateInterval`. - - -Valori booleani ---------------- - -I valori booleani vengono convertiti automaticamente in `true` o `false`. In MySQL viene convertito `TINYINT(1)` se nella [configurazione |configuration] impostiamo `convertBoolean`. - -```php -$row = $database->fetch('SELECT is_published FROM articles'); -echo gettype($row->is_published); // 'boolean' -``` - - -Valori numerici ---------------- - -I valori numerici vengono convertiti in `int` o `float` secondo il tipo della colonna nel database: - -```php -$row = $database->fetch('SELECT id, price FROM products'); -echo gettype($row->id); // integer -echo gettype($row->price); // float -``` - - -Normalizzazione personalizzata ------------------------------- - -Con il metodo `setRowNormalizer(?callable $normalizer)` potete impostare una funzione personalizzata per trasformare le righe provenienti dal database. Torna utile per esempio per la conversione automatica dei tipi di dato. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // qui avviene la conversione dei tipi - return $row; -}); -``` diff --git a/database/it/upgrading.texy b/database/it/upgrading.texy deleted file mode 100644 index 6ee4337c59..0000000000 --- a/database/it/upgrading.texy +++ /dev/null @@ -1,36 +0,0 @@ -Aggiornamento -************* - - -Aggiornamento alla versione 3.2 -=============================== - -La versione minima richiesta di PHP è la 8.1. - -Il codice è stato accuratamente adattato a PHP 8.1. Sono state aggiunte tutte le nuove dichiarazioni di tipo per i metodi e le proprietà. Le modifiche sono minime: - -- MySQL: la data nulla `0000-00-00` viene restituita come `null` -- MySQL: un decimal senza cifre decimali viene restituito come int invece che come float -- il tipo `time` viene restituito come oggetto `DateTime` con la data impostata a `0001-01-01` invece che alla data odierna - - -Aggiornamento alla versione 3.1 -=============================== - -- la classe `Nette\Database\Context` è stata rinominata in `Nette\Database\Explorer` per coerenza con il nome [Database Explorer|explorer] -- le interfacce `Nette\Database\IRow` e `Nette\Database\IRowContainer` sono contrassegnate come deprecate perché superflue -- il driver `MySqlDriver` usa le sottoquery -- il traduttore dei comandi SQL controlla meglio dove si possono passare gli array - - -Aggiornamento alla versione 3.0 -=============================== - -Alcuni metodi, come `fetch()` o `fetchField()`, restituiscono ora `null` invece di `false` quando non esiste una riga successiva. - - -Aggiornamento alla versione 2.3 -=============================== - -- il `MySqlDriver` usa per MySQL >= 5.5.3 la codifica `utf8mb4` come predefinita invece di `utf8` -- `IReflection` è stata divisa nelle due interfacce gemelle `IStructure` e `IConventions` diff --git a/database/ja/@home.texy b/database/ja/@home.texy deleted file mode 100644 index 0b8efd210e..0000000000 --- a/database/ja/@home.texy +++ /dev/null @@ -1,18 +0,0 @@ -対応しているデータベース -============ - -次のデータベースサーバーに対応しています。 - -|* データベースサーバー |* DSN の名前 |* Core の対応 |* Explorer の対応 -| MySQL (>= 5.1) | mysql | あり | あり -| PostgreSQL (>= 9.0) | pgsql | あり | あり -| Sqlite 3 (>= 3.8) | sqlite | あり | あり -| Oracle | oci | あり | - -| MS SQL (PDO_SQLSRV) | sqlsrv | あり | あり -| MS SQL (PDO_DBLIB) | mssql | あり | - -| ODBC | odbc | あり | - - - - -{{maintitle: Nette Database - PHP のための素晴らしいデータベース層}} -{{description: Nette Database は SQL のクエリを書かずにデータベースからデータを取り出す作業を大幅に簡単にします。効率のよいクエリを実行し、要らないデータは転送しません。}} diff --git a/database/ja/@left-menu.texy b/database/ja/@left-menu.texy deleted file mode 100644 index a0b66b94d9..0000000000 --- a/database/ja/@left-menu.texy +++ /dev/null @@ -1,21 +0,0 @@ -Nette Database -************** -- [はじめに |guide] -- [SQL アプローチ|sql-way] -- [Explorer|explorer] -- [トランザクション|transactions] -- [例外|exceptions] -- [リフレクション|reflection] -- [型変換 |type-conversion] -- [設定|configuration] -- [セキュリティリスク |security] -- [アップグレード|upgrading] - - -関連情報 -**** -- [Nette ドキュメント |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [ベストプラクティス |best-practices:] -- [トラブルシューティング |nette:troubleshooting] diff --git a/database/ja/@meta.texy b/database/ja/@meta.texy deleted file mode 100644 index 43b85f3cac..0000000000 --- a/database/ja/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette ドキュメント}} diff --git a/database/ja/configuration.texy b/database/ja/configuration.texy deleted file mode 100644 index c20a9b2c69..0000000000 --- a/database/ja/configuration.texy +++ /dev/null @@ -1,110 +0,0 @@ -データベースの設定 -********* - -.[perex] -Nette Database の設定オプションの一覧です。 - -フレームワーク全体ではなくこのライブラリだけを使っているなら、[設定の読み込み方|bootstrap:]をご覧ください。 - - -ひとつの接続 ------- - -ひとつのデータベース接続を設定します。 - -```neon -database: - # DSN。唯一の必須のキーです - dsn: "sqlite:%appDir%/Model/demo.db" - user: ... - password: ... -``` - -これで `Nette\Database\Connection` と `Nette\Database\Explorer` のサービスが作られ、ふつうは[オートワイヤリング |dependency-injection:autowiring]で、あるいは[その名前 |#DI のサービス]を指定して渡されます。 - -そのほかの設定です。 - -```neon -database: - # Tracy Bar にデータベースのパネルを表示しますか - debugger: ... # (bool) Tracy が動いていれば既定でオン - - # Tracy Bar にクエリの EXPLAIN を表示しますか - explain: ... # (bool) 既定は true - - # この接続でオートワイヤリングを有効にしますか - autowired: ... # (bool) 最初の接続では既定で true - - # テーブルの規約: discovered、static、またはクラス名 - conventions: discovered # (string) 既定は 'discovered' - - options: - # 必要になったときだけデータベースに接続しますか - lazy: ... # (bool) 既定は false - - # PHP のデータベースドライバのクラス - driverClass: # (string) - - # MySQL のみ: sql_mode を設定します - sqlmode: # (string) - - # MySQL のみ: SET NAMES を設定します - charset: # (string) 既定は 'utf8mb4' - - # MySQL のみ: TINYINT(1) を bool に変換します - convertBoolean: # (bool) 既定は false - - # 日付の列を変更できないオブジェクトとして返します(バージョン 3.2.1 以降) - newDateTime: # (bool) 既定は false - - # Oracle と SQLite のみ: 日付を保存する書式 - formatDateTime: # (string) 既定は 'U' -``` - -`options` キーには [PDO ドライバのドキュメント |https://www.php.net/manual/en/pdo.drivers.php]にあるほかのオプションも書けます。たとえば次のようにです。 - -```neon -database: - options: - PDO::MYSQL_ATTR_COMPRESS: true -``` - - -複数の接続 ------ - -設定では、名前の付いた区画に分けて複数のデータベース接続を定義できます。 - -```neon -database: - main: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password - - another: - dsn: 'sqlite::memory:' -``` - -オートワイヤリングが有効になるのは最初の区画のサービスだけです。これは `autowired: false` や `autowired: true` で変えられます。 - - -DI のサービス --------- - -DI コンテナには次のサービスが足されます。`###` は接続の名前を表します。 - -| 名前 | 型 | 説明 -|---------------------------|---------------------------------|--------------------------- -| `database.###.connection` | [api:Nette\Database\Connection] | データベース接続 -| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |explorer] - - -接続をひとつだけ定義した場合、サービスの名前は `database.default.connection` と `database.default.explorer` になります。上の例のように複数の接続を定義した場合、名前は区画に対応して `database.main.connection`、`database.main.explorer`、さらに `database.another.connection`、`database.another.explorer` になります。 - -オートワイヤリングされないサービスは、その名前を指定してはっきり渡します。 - -```neon -services: - - UserFacade(@database.another.connection) -``` diff --git a/database/ja/exceptions.texy b/database/ja/exceptions.texy deleted file mode 100644 index b8f06f6ea9..0000000000 --- a/database/ja/exceptions.texy +++ /dev/null @@ -1,37 +0,0 @@ -例外 -*** - -Nette Database は例外の階層を使います。基底クラスは `Nette\Database\DriverException` で、`PDOException` を継承し、データベースのエラーを扱うための機能を強化します。 - -- `getDriverCode()` メソッドはデータベースドライバのエラーコードを返します。 -- `getSqlState()` メソッドは SQLSTATE のコードを返します。 -- `getQueryString()` と `getParameters()` メソッドで、もとのクエリとそのパラメータを取り出せます。 - -`DriverException` クラスは、次の特化した例外に継承されます。 - -- `ConnectionException` - データベースサーバーへの接続に失敗したことを示します。 - - `ConnectionLostException` .{data-version:3.2.9} - 処理の途中で接続が切れました(サーバーの再起動、ネットワークの障害、idle timeout)。続けて使う前に接続し直す必要があります。 -- `ConstraintViolationException` - データベースの制約違反の基底クラスで、次の例外がこれを継承します。 - - `ForeignKeyConstraintViolationException` - 外部キー制約の違反。 - - `NotNullConstraintViolationException` - NOT NULL 制約の違反。 - - `UniqueConstraintViolationException` - 一意性の制約の違反。 - - `CheckConstraintViolationException` .{data-version:3.2.9} - CHECK 制約の違反。 -- `DeadlockException` .{data-version:3.2.9} - サーバーが検出した deadlock または直列化の衝突。トランザクションはロールバックされ、やり直せます。 -- `LockTimeoutException` .{data-version:3.2.9} - ロック待ちの制限時間を超えました。その文は中止されましたが、それを囲むトランザクションはふつう開いたままです。 - -次の例は `UniqueConstraintViolationException` の捕まえ方を示します。これはデータベースにすでにあるメールアドレスでユーザーを挿入しようとしたときに起きます(`email` 列に一意インデックスがあるものとします)。 - -```php -try { - $database->query('INSERT INTO users', [ - 'email' => 'john@example.com', - 'name' => 'John Doe', - 'password' => $hashedPassword, - ]); -} catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'このメールアドレスのユーザーはすでに存在します。'; - -} catch (Nette\Database\DriverException $e) { - echo '登録の途中でエラーが起きました: ' . $e->getMessage(); -} -``` diff --git a/database/ja/explorer.texy b/database/ja/explorer.texy deleted file mode 100644 index 369e2c668c..0000000000 --- a/database/ja/explorer.texy +++ /dev/null @@ -1,912 +0,0 @@ -Database Explorer -***************** - -<div class=perex> - -Explorer はデータベースを直感的に、しかも効率よく扱う方法を提供します。テーブルどうしの関係を自動的に扱い、クエリを最適化するので、あなたはアプリケーションの論理に集中できます。設定なしですぐに動きます。SQL のクエリを完全に思いどおりにしたいなら、[SQL アプローチ |SQL way]を使えます。 - -- データの扱いが自然で分かりやすい -- 必要なデータだけを取ってくる最適化された SQL のクエリを生成する -- JOIN のクエリを書かずに関連するデータへ簡単にアクセスできる -- 設定もエンティティの生成もなしにすぐ動く - -</div> - - -Explorer を使う仕事は、[api:Nette\Database\Explorer]オブジェクトの `table()` メソッドを呼ぶところから始まります(データベース接続の設定については [接続と設定 |guide#接続と設定]をご覧ください)。 - -```php -$books = $explorer->table('book'); // 'book' はテーブルの名前です -``` - -このメソッドは SQL のクエリを表す [Selection |api:Nette\Database\Table\Selection]オブジェクトを返します。このオブジェクトにはさらにメソッドをつないで、結果を絞り込んだり並べ替えたりできます。クエリが組み立てられて実行されるのは、たとえば `foreach` で回すなど、データが実際に求められたときだけです。それぞれの行は [ActiveRow |api:Nette\Database\Table\ActiveRow]オブジェクトが表します。 - -```php -foreach ($books as $book) { - echo $book->title; // 'title' 列を出力します - echo $book->author_id; // 'author_id' 列を出力します -} -``` - -Explorer は[テーブルどうしの関係 |#テーブルどうしの関係]を扱う仕事を大いに簡単にします。次の例は、関連するテーブル(本とその著者)のデータをどれだけ簡単に出力できるかを示します。JOIN のクエリを書く必要がないことに注目してください。Nette がそれを生成してくれます。 - -```php -$books = $explorer->table('book'); - -foreach ($books as $book) { - echo 'Book: ' . $book->title; - echo 'Author: ' . $book->author->name; // 'author' テーブルへの JOIN を作ります -} -``` - -Nette Database Explorer は効率が最大になるようにクエリを最適化します。上の例は、本を 10 冊扱おうと 10,000 冊扱おうと、SELECT のクエリを 2 つしか実行しません。 - -さらに Explorer は、コードでどの列が使われているかを追い、データベースからはそれだけを取ってきて、いっそう性能を節約します。この振る舞いは完全に自動で、状況に合わせて変わります。あとからコードを変えて別の列を使うようにすれば、Explorer はクエリを自動的に合わせます。何かを設定したり、どの列が要るかを考えたりする必要はありません。それは Nette に任せてください。 - - -絞り込みと並べ替え -========= - -`Selection` クラスは、選び出すデータを絞り込んだり並べ替えたりするメソッドを提供します。 - -.[language-php] -| `where($condition, ...$params)` | WHERE の条件を足します。複数の条件は AND でつながれます | -| `whereOr(array $conditions)` | OR でつながれる WHERE の条件のまとまりを足します | -| `wherePrimary($value)` | 主キーをもとにした WHERE の条件を足します | -| `order($columns, ...$params)` | ORDER BY による並べ替えを設定します | -| `select($columns, ...$params)` | どの列を取ってくるかを指定します | -| `limit($limit, $offset = null)` | 行の数を制限し(LIMIT)、必要なら OFFSET も設定します | -| `page($page, $itemsPerPage, &$numOfPages = null)` | ページ分けを設定します | -| `group($columns, ...$params)` | 行をまとめます(GROUP BY) | -| `having($condition, ...$params)`| まとめた行を絞り込む HAVING の条件を足します | - -メソッドはつなげて書けます(いわゆる [fluent インターフェース |nette:introduction-to-object-oriented-programming#fluent インターフェース])。`$table->where(...)->order(...)->limit(...)` のようにです。 - -これらのメソッドでは、[関連するテーブルのデータ |#関連するテーブルを通した問い合わせ]にアクセスする特別な書き方も使えます。 - - -エスケープと識別子 ---------- - -これらのメソッドはパラメータを自動的にエスケープし、識別子(テーブル名と列名)を引用符で囲むので、SQL インジェクションを防ぎます。正しく動くようにするには、いくつかの決まりを守る必要があります。 - -- キーワード、関数名、プロシージャ名などは**大文字**で書きます。 -- 列名とテーブル名は**小文字**で書きます。 -- 文字列はいつも**パラメータ**として渡します。 - -```php -where('name = ' . $name); // 致命的な弱点: SQL インジェクション -where('name LIKE "%search%"'); // 誤り: 自動的な引用を難しくします -where('name LIKE ?', '%search%'); // 正しい: 値をパラメータとして渡します - -where('name like ?', $name); // 誤り: `name` `like` ? を生成します -where('name LIKE ?', $name); // 正しい: `name` LIKE ? を生成します -where('LOWER(name) = ?', $value);// 正しい: LOWER(`name`) = ? -``` - - -where(string|array $condition, ...$parameters): static .[method] ----------------------------------------------------------------- - -WHERE の条件で結果を絞り込みます。その強みは、さまざまな型の値を賢く扱い、ふさわしい SQL の演算子を自動的に選ぶところにあります。 - -基本の使い方です。 - -```php -$table->where('id', $value); // WHERE `id` = 123 -$table->where('id > ?', $value); // WHERE `id` > 123 -$table->where('id = ? OR name = ?', $id, $name); // WHERE `id` = 1 OR `name` = 'Jon Snow' -``` - -ふさわしい演算子が自動的に見分けられるので、さまざまな特別な場合を自分で扱う必要はありません。Nette が片付けてくれます。 - -```php -$table->where('id', 1); // WHERE `id` = 1 -$table->where('id', null); // WHERE `id` IS NULL -$table->where('id', [1, 2, 3]); // WHERE `id` IN (1, 2, 3) -// 演算子なしのプレースホルダ ? も使えます: -$table->where('id ?', 1); // WHERE `id` = 1 -``` - -このメソッドは否定の条件と空の配列も正しく扱います。 - -```php -$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- 何も見つかりません -$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- すべてが見つかります -$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- すべてが見つかります -// $table->where('NOT id ?', $ids); // 注意: この書き方には対応していません -``` - -パラメータとして別のテーブルのクエリの結果を渡して、副問い合わせを作ることもできます。 - -```php -// WHERE `id` IN (SELECT `id` FROM `tableName`) -$table->where('id', $explorer->table($tableName)); - -// WHERE `id` IN (SELECT `col` FROM `tableName`) -$table->where('id', $explorer->table($tableName)->select('col')); -``` - -条件は配列でも渡せます。その要素は AND でつながれます。 - -```php -// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) -$table->where([ - 'price_final < price_original', - 'stock_count > min_stock', -]); -``` - -配列では キー => 値 の組も使え、Nette はやはり正しい演算子を自動的に選びます。 - -```php -// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3)) -$table->where([ - 'status' => 'active', - 'id' => [1, 2, 3], -]); -``` - -配列の中では、SQL の式をプレースホルダや複数のパラメータと組み合わせられます。演算子をきっちり定めたい込み入った条件に向いています。 - -```php -// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) -$table->where([ - 'age > ?' => 18, - 'ROUND(score, ?) > ?' => [2, 75.5], // 2 つのパラメータを配列で渡します -]); -``` - -`where()` を何度も呼ぶと、条件は自動的に AND でつながれます。 - - -whereOr(array $parameters): static .[method] --------------------------------------------- - -`where()` と同じように条件を足しますが、OR でつなぎます。 - -```php -// WHERE (`status` = 'active') OR (`deleted` = 1) -$table->whereOr([ - 'status' => 'active', - 'deleted' => true, -]); -``` - -ここでもより込み入った式を使えます。 - -```php -// WHERE (`price` > 1000) OR (`price_with_tax` > 1500) -$table->whereOr([ - 'price > ?' => 1000, - 'price_with_tax > ?' => 1500, -]); -``` - - -wherePrimary(mixed $key): static .[method] ------------------------------------------- - -テーブルの主キーに対する条件を足します。 - -```php -// WHERE `id` = 123 -$table->wherePrimary(123); - -// WHERE `id` IN (1, 2, 3) -$table->wherePrimary([1, 2, 3]); -``` - -テーブルが複合主キー(たとえば `foo_id`、`bar_id`)を持つなら、配列で渡します。 - -```php -// WHERE `foo_id` = 1 AND `bar_id` = 5 -$table->wherePrimary(['foo_id' => 1, 'bar_id' => 5])->fetch(); - -// WHERE (`foo_id`, `bar_id`) IN ((1, 5), (2, 3)) -$table->wherePrimary([ - ['foo_id' => 1, 'bar_id' => 5], - ['foo_id' => 2, 'bar_id' => 3], -])->fetchAll(); -``` - - -order(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -行が返される順序を指定します。ひとつまたは複数の列で、昇順または降順に、あるいは独自の式に従って並べ替えられます。 - -```php -$table->order('created'); // ORDER BY `created` -$table->order('created DESC'); // ORDER BY `created` DESC -$table->order('priority DESC, created'); // ORDER BY `priority` DESC, `created` -$table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC -``` - - -select(string $columns, ...$parameters): static .[method] ---------------------------------------------------------- - -データベースから返される列を指定します。既定では、Nette Database Explorer はコードで実際に使われている列だけを返します。特定の式を取り出す必要があるときに `select()` メソッドを使います。 - -```php -// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` -$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); -``` - -`AS` で定義した別名は、そのあと `ActiveRow` オブジェクトのプロパティとして使えます。 - -```php -foreach ($table as $row) { - echo $row->formatted_date; // 別名にアクセスします -} -``` - - -limit(?int $limit, ?int $offset = null): static .[method] ---------------------------------------------------------- - -返される行の数を制限し(LIMIT)、必要なら開始位置も設定できます。 - -```php -$table->limit(10); // LIMIT 10(最初の 10 行を返します) -$table->limit(10, 20); // LIMIT 10 OFFSET 20 -``` - -ページ分けには `page()` メソッドのほうが向いています。 - - -page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] -------------------------------------------------------------------------- - -結果のページ分けを楽にします。ページ番号(1 から始まります)と 1 ページあたりの項目の数を受け取ります。必要なら、ページの総数が入る変数への参照も渡せます。 - -```php -$numOfPages = null; -$table->page(page: 3, itemsPerPage: 10, numOfPages: $numOfPages); -echo "Total pages: $numOfPages"; -``` - - -group(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -指定した列に従って行をまとめます(GROUP BY)。ふつうは集約関数と組み合わせて使います。 - -```php -// カテゴリごとの商品の数を数えます -$table->select('category_id, COUNT(*) AS count') - ->group('category_id'); -``` - - -having(string $having, ...$parameters): static .[method] --------------------------------------------------------- - -まとめた行を絞り込む条件を設定します(HAVING)。`group()` メソッドや集約関数と組み合わせて使えます。 - -```php -// 商品が 100 を超えるカテゴリを見つけます -$table->select('category_id, COUNT(*) AS count') - ->group('category_id') - ->having('count > ?', 100); -``` - - -データの読み出し -======== - -データベースからデータを読み出すのに、役に立つメソッドがいくつかあります。 - -.[language-php] -| `foreach ($table as $key => $row)` | すべての行を回します。`$key` は主キーの値、`$row` は ActiveRow オブジェクトです | -| `$row = $table->get($key)` | 主キーでひとつの行を返します | -| `$row = $table->fetch()` | 今の行を返し、ポインタを次へ進めます | -| `$array = $table->fetchPairs()` | 結果から連想配列を作ります | -| `$array = $table->fetchAll()` | すべての行を配列として返します | -| `count($table)` | Selection オブジェクトの行の数を返します | - -[ActiveRow |api:Nette\Database\Table\ActiveRow]オブジェクトは読み取り専用です。つまりそのプロパティの値は変えられません。この制限はデータの整合性を守り、思いがけない副作用を防ぎます。データはデータベースから読み込まれるものであり、変更ははっきりと、しかも制御された形で行うべきだからです。 - - -`foreach` - すべての行を回す --------------------- - -クエリを実行して行を取り出すいちばん簡単な方法は、`foreach` のループで回すことです。SQL のクエリは自動的に実行されます。 - -```php -$books = $explorer->table('book'); -foreach ($books as $key => $book) { - // $key は主キーの値、$book は ActiveRow です - echo "$book->title ({$book->author->name})"; -} -``` - - -get($key): ?ActiveRow .[method] -------------------------------- - -SQL のクエリを実行し、主キーで行を返します。存在しなければ `null` を返します。 - -```php -$book = $explorer->table('book')->get(123); // ID 123 の ActiveRow か null を返します -if ($book) { - echo $book->title; -} -``` - - -fetch(): ?ActiveRow .[method] ------------------------------ - -今の行を返し、内部のポインタを次へ進めます。行がもうなければ `null` を返します。 - -```php -$books = $explorer->table('book'); -while ($book = $books->fetch()) { - $this->processBook($book); -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -結果を連想配列として返します。第 1 引数は配列のキーとして使う列の名前、第 2 引数は値として使う列の名前を指定します。 - -```php -$authors = $explorer->table('author')->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -第 1 パラメータだけを渡すと、値は行全体、つまり `ActiveRow` オブジェクトになります。 - -```php -$authors = $explorer->table('author')->fetchPairs('id'); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - -キーが重なった場合は最後の行の値が使われます。キーに `null` を使うと、配列はゼロから始まる添字になります(そうすれば衝突は起きません)。 - -```php -$authors = $explorer->table('author')->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -代わりにパラメータとしてコールバックを渡せます。それは行ごとにひとつの値か、キーと値の組を返します。 - -```php -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); -// ['First Book (John Novak)', ...] - -// コールバックはキーと値の組の配列を返すこともできます: -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => [$row->title, $row->author->name]); -// ['First Book' => 'John Novak', ...] -``` - - -fetchAll(): array .[method] ---------------------------- - -すべての行を `ActiveRow` オブジェクトの連想配列として返します。キーは主キーの値です。 - -```php -$allBooks = $explorer->table('book')->fetchAll(); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - - -count(): int .[method] ----------------------- - -パラメータなしの `count()` メソッドは `Selection` オブジェクトの行の数を返します。 - -```php -$table->where('category', 1); -$count = $table->count(); -$count = count($table); // 別の書き方 -``` - -注意: パラメータ付きの `count()` はデータベースで COUNT の集約関数を実行します。下をご覧ください。 - - -ActiveRow::toArray(): array .[method] -------------------------------------- - -`ActiveRow` オブジェクトを連想配列に変えます。キーは列の名前、値は対応するデータです。 - -```php -$book = $explorer->table('book')->get(1); -$bookArray = $book->toArray(); -// $bookArray は ['id' => 1, 'title' => '...', 'author_id' => ..., ...] になります -``` - - -集約 -=== - -`Selection` クラスは、集約関数(COUNT、SUM、MIN、MAX、AVG など)を簡単に実行するメソッドを提供します。 - -.[language-php] -| `count($expr)` | 行の数を数えます | -| `min($expr)` | 列の最小値を返します | -| `max($expr)` | 列の最大値を返します | -| `sum($expr)` | 列の値の合計を返します | -| `aggregation($function)` | `AVG()` や `GROUP_CONCAT()` など、任意の集約関数を使えます | - - -count(string $expr): int .[method] ----------------------------------- - -COUNT 関数を使う SQL のクエリを実行し、その結果を返します。ある条件に合う行がいくつあるかを調べるのに使います。 - -```php -$count = $table->count('*'); // SELECT COUNT(*) FROM `table` -$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` -``` - -注意: パラメータなしの [#count()]は `Selection` オブジェクトの行の数を返すだけです。 - - -min(string $expr) と max(string $expr) .[method] ------------------------------------------------ - -`min()` と `max()` メソッドは、指定した列や式の最小値と最大値を返します。 - -```php -// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 -$maxPrice = $products->where('active', true) - ->max('price'); -``` - - -sum(string $expr): mixed .[method] ----------------------------------- - -指定した列や式の値の合計を返します。 - -```php -// SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 -$totalPrice = $products->where('active', true) - ->sum('price * items_in_stock'); -``` - - -aggregation(string $function, ?string $groupFunction = null): mixed .[method] ------------------------------------------------------------------------------ - -任意の集約関数を実行できます。 - -```php -// カテゴリの商品の平均価格 -$avgPrice = $products->where('category_id', 1) - ->aggregation('AVG(price)'); - -// 商品のタグをひとつの文字列につなぎます -$tags = $products->where('id', 1) - ->aggregation('GROUP_CONCAT(tag.name) AS tags') - ->fetch() - ->tags; -``` - -すでに何らかの集約関数とまとめの結果になっているもの(たとえばまとめた行に対する `SUM(value)`)をさらに集約する必要があるなら、その中間の結果に当てる集約関数を第 2 引数で指定します。 - -```php -// カテゴリごとに在庫の商品の合計金額を計算し、それらの金額を足し合わせます。 -$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') - ->group('category_id') - ->aggregation('SUM(category_total)', 'SUM'); -``` - -この例では、まずカテゴリごとに商品の合計金額を計算し(`SUM(price * stock) AS category_total`)、結果を `category_id` でまとめます。そのあと `aggregation('SUM(category_total)', 'SUM')` でこれらの中間の合計 `category_total` を足し合わせます。第 2 引数の `'SUM'` は、中間の結果に SUM 関数を当てることを指定しています。 - - -挿入、更新、削除 -======== - -Nette Database Explorer はデータの挿入、更新、削除を簡単にします。ここで挙げるメソッドはすべて、エラーの場合に `Nette\Database\DriverException` を投げます。 - - -Selection::insert(iterable $data) .[method] -------------------------------------------- - -テーブルに新しいレコードを挿入します。 - -**ひとつのレコードを挿入する:** - -新しいレコードを連想配列か、反復できるオブジェクト([フォーム |forms:]で使われる `ArrayHash` など)として渡します。キーはテーブルの列の名前に対応します。 - -テーブルに主キーが定義されていれば、このメソッドは `ActiveRow` オブジェクトを返します。それはデータベースの側で起きた変更(トリガー、列の既定値、自動採番の計算)を映すために、データベースから読み直されます。これでデータの整合性が保たれ、オブジェクトはいつもデータベースの今のデータを持ちます。テーブルに主キーがなければ、行を特定できないのでこのメソッドは `null` を返します。 - -```php -$row = $explorer->table('users')->insert([ - 'name' => 'John Doe', - 'email' => 'john.doe@example.com', -]); -// $row は ActiveRow のインスタンスで、挿入された行の完全なデータを持ちます。 -// 自動的に生成された ID や、トリガーによる変更も含みます -echo $row->id; // 新しく挿入されたユーザーの ID を出力します -echo $row->created_at; // トリガーで設定されていれば作成時刻を出力します -``` - -**複数のレコードを一度に挿入する:** - -`insert()` メソッドは、ひとつの SQL のクエリで複数のレコードを挿入できます。その場合は挿入された行の数を返します。 - -```php -$insertedRows = $explorer->table('users')->insert([ - [ - 'name' => 'John', - 'year' => 1994, - ], - [ - 'name' => 'Jack', - 'year' => 1995, - ], -]); -// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) -// $insertedRows は 2 になります -``` - -パラメータとして、データを選び出した `Selection` オブジェクトも渡せます。 - -```php -$newUsers = $explorer->table('potential_users') - ->where('approved', 1) - ->select('name, email'); - -$insertedRows = $explorer->table('users')->insert($newUsers); -``` - -**特別な値を挿入する:** - -値としてファイル、`DateTime` オブジェクト、SQL のリテラルも渡せます。 - -```php -$explorer->table('users')->insert([ - 'name' => 'John', - 'created_at' => new DateTime, // データベースの書式に変換します - 'avatar' => fopen('image.jpg', 'rb'), // ファイルの中身をバイナリとして入れます - 'uuid' => $explorer::literal('UUID()'), // UUID() 関数を呼びます -]); -``` - - -Selection::update(iterable $data): int .[method] ------------------------------------------------- - -指定した絞り込みに従ってテーブルの行を更新します。実際に変わった行の数を返します。 - -変える列を連想配列か、反復できるオブジェクト([フォーム |forms:]で使われる `ArrayHash` など)として渡します。キーはテーブルの列の名前に対応します。 - -```php -$affected = $explorer->table('users') - ->where('id', 10) - ->update([ - 'name' => 'John Smith', - 'year' => 1994, - ]); -// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 -``` - -数値を変えるには `+=` と `-=` の演算子を使えます。 - -```php -$explorer->table('users') - ->where('id', 10) - ->update([ - 'points+=' => 1, // 'points' 列の値を 1 増やします - 'coins-=' => 1, // 'coins' 列の値を 1 減らします - ]); -// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 -``` - - -Selection::delete(): int .[method] ----------------------------------- - -指定した絞り込みに従ってテーブルの行を削除します。削除された行の数を返します。 - -```php -$count = $explorer->table('users') - ->where('id', 10) - ->delete(); -// DELETE FROM `users` WHERE `id` = 10 -``` - -.[caution] -`update()` や `delete()` を呼ぶときは、変更/削除する行を `where()` で指定するのを忘れないでください。`where()` を使わないと、その操作はテーブル全体に対して行われます。 - - -ActiveRow::update(iterable $data): bool .[method] -------------------------------------------------- - -`ActiveRow` オブジェクトが表すデータベースの行のデータを更新します。更新するデータを持つ反復できるものを受け取ります(キーは列の名前です)。数値を変えるには `+=` と `-=` の演算子を使えます。 - -更新のあと、`ActiveRow` はデータベースの側で起きた変更(トリガーなど)を映すために自動的に読み直されます。このメソッドは、実際にデータが変わったときにだけ `true` を返します。 - -```php -$article = $explorer->table('article')->get(1); -$article->update([ - 'views += 1', // 閲覧数を増やします -]); -echo $article->views; // 今の閲覧数を出力します -``` - -このメソッドはデータベースの特定の 1 行だけを更新します。複数の行をまとめて更新するには [#Selection::update()]メソッドを使ってください。 - - -ActiveRow::delete(): int .[method] ----------------------------------- - -`ActiveRow` オブジェクトが表す行をデータベースから削除します。削除された行の数を返します。それは 1 のはずです。 - -```php -$book = $explorer->table('book')->get(1); -$book->delete(); // ID 1 の本を削除します -``` - -このメソッドはデータベースの特定の 1 行だけを削除します。複数の行をまとめて削除するには [#Selection::delete()]メソッドを使ってください。 - - -テーブルどうしの関係 -========== - -リレーショナルデータベースでは、データは複数のテーブルに分けられ、外部キーで結び付けられます。Nette Database Explorer は、この関係を扱う画期的な方法を提供します。JOIN のクエリを書く必要も、何かを設定したり生成したりする必要もありません。 - -関係の扱いを説明するために、本のデータベースの例を使います([GitHub で見られます |https://github.com/nette-examples/books])。データベースには次のテーブルがあります。 - -- `author` - 著者と翻訳者(列は `id`、`name`、`web`、`born`) -- `book` - 本(列は `id`、`author_id`、`translator_id`、`title`、`sequel_id`) -- `tag` - タグ(列は `id`、`name`) -- `book_tag` - 本とタグをつなぐ中間テーブル(列は `book_id`、`tag_id`) - -[* db-schema-1-.webp *] *** 例で使うデータベースの構造 .<> - -この本のデータベースの例には、いくつかの種類の関係があります(現実に比べると簡略にしてあります)。 - -- **1 対多(1:N)** - それぞれの本には著者が**ひとり**います。著者は**複数**の本を書けます。 -- **0 対多(0:N)** - 本には翻訳者が**いることがあります**。翻訳者は**複数**の本を訳せます。 -- **0 対 1(0:1)** - 本には続編が**あることがあります**。 -- **多対多(M:N)** - 本には**いくつかの**タグを付けられ、タグは**いくつもの**本に付けられます。 - -こうした関係にはいつも**親テーブル**と**子テーブル**があります。たとえば著者と本の関係では、`author` テーブルが親で `book` テーブルが子です。本はいつも著者に「属している」と考えると分かりやすいでしょう。これはデータベースの構造にも表れています。子テーブル `book` は、親テーブル `author` を参照する外部キー `author_id` を持ちます。 - -本を著者の名前とともに並べたいなら、やり方は 2 つあります。JOIN を使ってひとつの SQL のクエリでデータを取ってくるか、 - -```sql -SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; -``` - -あるいは 2 段階でデータを取ってきて、まず本、次にその著者を取り、そのあと PHP で組み合わせるかです。 - -```sql -SELECT * FROM book; -SELECT * FROM author WHERE id IN (1, 2, 3); -- 選ばれた本の著者の ID -``` - -意外に思えるかもしれませんが、2 つめのやり方のほうが実は**効率的**です。データは一度しか取られず、キャッシュもよりよく活かせます。Nette Database Explorer はまさにこのように働きます。すべてを裏で片付けて、あなたには優雅な API を差し出します。 - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo 'title: ' . $book->title; - echo 'written by: ' . $book->author->name; // $book->author は 'author' テーブルのレコードです - echo 'translated by: ' . $book->translator?->name; -} -``` - - -親テーブルへのアクセス ------------ - -親テーブルへのアクセスは分かりやすいものです。*本には著者がいる*、*本には翻訳者がいることがある* といった関係です。関連するレコードは ActiveRow オブジェクトのプロパティで取り出せます。その名前は、外部キーの列の名前から `_id` の接尾辞を取ったものに対応します。 - -```php -$book = $explorer->table('book')->get(1); -echo $book->author->name; // author_id 列をもとに著者を見つけます -echo $book->translator?->name; // translator_id 列をもとに翻訳者を見つけます -``` - -`$book->author` プロパティにアクセスすると、Explorer は `book` テーブルの中から、名前に `author` という文字列を含む列(つまり `author_id`)を探します。その列の値をもとに `author` テーブルの対応するレコードを読み込み、`ActiveRow` として返します。同じように `$book->translator` は `translator_id` 列を使います。`translator_id` 列は `null` を持てるので、コードでは nullsafe 演算子 `?->` を使っています。 - -別のやり方として `ref()` メソッドがあります。これは 2 つの引数、つまり対象のテーブルの名前とつなぐ列の名前を受け取り、`ActiveRow` のインスタンスか `null` を返します。 - -```php -echo $book->ref('author', 'author_id')->name; // 著者への関係 -echo $book->ref('author', 'translator_id')->name; // 翻訳者への関係 -``` - -`ref()` メソッドは、たとえばテーブルに同じ名前の列(つまり `author`)があってプロパティによるアクセスが使えない場合に役立ちます。そのほかの場合は、読みやすさのためにプロパティによるアクセスをおすすめします。 - -Explorer はデータベースのクエリを自動的に最適化します。ループで本を回しながら関連するレコード(著者、翻訳者)にアクセスしても、Explorer は本ごとにクエリを作ったりしません。代わりに**関係の種類ごとに SELECT のクエリをひとつだけ**実行し、データベースの負荷を大きく減らします。たとえば次のようにです。 - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo $book->title . ': '; - echo $book->author->name; - echo $book->translator?->name; -} -``` - -このコードはデータベースに対して、次の 3 つのごく速いクエリだけを実行します。 - -```sql -SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- 選ばれた本の author_id 列の ID -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- 選ばれた本の translator_id 列の ID -``` - -.[note] -つなぐ列を見つける論理は [Conventions |api:Nette\Database\Conventions]の実装が決めます。外部キーを解析し、テーブルどうしの既存の関係を簡単に扱える [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions]をおすすめします。 - - -子テーブルへのアクセス ------------ - -子テーブルへのアクセスは逆向きに働きます。今度は *この著者はどの本を書いたか*、*この翻訳者はどの本を訳したか* と尋ねます。この種の問い合わせには `related()` メソッドを使います。これは関連するレコードを持つ `Selection` を返します。例を見てみましょう。 - -```php -$author = $explorer->table('author')->get(1); - -// その著者のすべての本を出力します -foreach ($author->related('book.author_id') as $book) { - echo "Wrote: $book->title"; -} - -// その著者が訳したすべての本を出力します -foreach ($author->related('book.translator_id') as $book) { - echo "Translated: $book->title"; -} -``` - -`related()` メソッドは、つなぎ方の指定をドットの書き方でひとつの引数として、あるいは 2 つの別々の引数として受け取ります。 - -```php -$author->related('book.translator_id'); // 引数ひとつ -$author->related('book', 'translator_id'); // 引数 2 つ -``` - -Explorer は親テーブルの名前をもとに、正しいつなぎの列を自動的に見つけられます。この場合、もとのテーブルの名前が `author` なので `book.author_id` 列でつなぎます。 - -```php -$author->related('book'); // book.author_id を使います -``` - -つなぎ方の候補が複数あると、Explorer は [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]を投げます。 - -もちろん `related()` メソッドは、ループで複数のレコードを回しながらも使えます。その場合も Explorer はクエリを自動的に最適化します。 - -```php -$authors = $explorer->table('author'); -foreach ($authors as $author) { - echo $author->name . ' wrote:'; - foreach ($author->related('book') as $book) { - echo $book->title; - } -} -``` - -このコードはごく速い SQL のクエリを 2 つだけ生みます。 - -```sql -SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- 選ばれた著者の ID -``` - - -多対多の関係 ------- - -多対多(M:N)の関係には、2 つの外部キーの列(`book_id`、`tag_id`)を持つ**中間テーブル**(ここでは `book_tag`)が要ります。これらの列はそれぞれ、結び付けられるテーブルの一方の主キーを指します。関連するデータを取り出すには、まず `related('book_tag')` で中間テーブルのレコードを取り、そこから目当てのデータへ進みます。 - -```php -$book = $explorer->table('book')->get(1); -// その本に付けられたタグの名前を出力します -foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name; // 中間テーブル経由でタグの名前を出力します -} - -$tag = $explorer->table('tag')->get(1); -// あるいは逆向きに: そのタグが付いた本の名前を出力します -foreach ($tag->related('book_tag') as $bookTag) { - echo $bookTag->book->title; // 本のタイトルを出力します -} -``` - -Explorer はここでも SQL のクエリを効率のよい形に最適化します。 - -```sql -SELECT * FROM `book`; -SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- 選ばれた本の ID -SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- book_tag で見つかったタグの ID -``` - - -関連するテーブルを通した問い合わせ ------------------ - -`where()`、`select()`、`order()`、`group()` メソッドでは、ほかのテーブルの列にアクセスする特別な書き方を使えます。Explorer は必要な JOIN を自動的に作ります。 - -**ドットの書き方**(`親テーブル.列`)は、子テーブルから見た 1:N の関係に使います。 - -```php -$books = $explorer->table('book'); - -// 著者の名前が 'Jon' で始まる本を見つけます -$books->where('author.name LIKE ?', 'Jon%'); - -// 本を著者の名前の降順に並べます -$books->order('author.name DESC'); - -// 本のタイトルと著者の名前を出力します -$books->select('book.title, author.name'); -``` - -**コロンの書き方**(`:子テーブル.列`)は、親テーブルから見た 1:N の関係に使います。 - -```php -$authors = $explorer->table('author'); - -// タイトルに 'PHP' を含む本を書いた著者を見つけます -$authors->where(':book.title LIKE ?', '%PHP%'); - -// 著者ごとの本の数を数えます -$authors->select('*, COUNT(:book.id) AS book_count') - ->group('author.id'); -``` - -上のコロンの書き方の例(`:book.title`)では、外部キーの列を指定していません。Explorer は親テーブルの名前をもとに正しい列を自動的に見つけます。この場合、もとのテーブルの名前が `author` なので `book.author_id` 列でつなぎます。つなぎ方の候補が複数あると、Explorer は [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]を投げます。 - -つなぐ列はかっこの中ではっきり指定できます。 - -```php -// タイトルに 'PHP' を含む本を訳した著者を見つけます -$authors->where(':book(translator_id).title LIKE ?', '%PHP%'); -``` - -書き方はつなげられるので、複数のテーブルをまたいでデータにアクセスできます。 - -```php -// 'PHP' のタグが付いた本の著者を見つけます -$authors->where(':book:book_tag.tag.name', 'PHP') - ->group('author.id'); -``` - - -JOIN の条件を広げる ------------- - -`joinWhere()` メソッドは、SQL でテーブルをつなぐときの `ON` キーワードのうしろの条件を広げます。 - -ある翻訳者が訳した本を見つけたいとしましょう。 - -```php -// 'David' という名前の翻訳者が訳した本を見つけます -$books = $explorer->table('book') - ->joinWhere('translator', 'translator.name', 'David'); -// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') -``` - -`joinWhere()` の条件では、`where()` メソッドと同じ書き方、つまり演算子、プレースホルダ、値の配列、SQL の式を使えます。 - -JOIN が複数ある込み入ったクエリでは、テーブルの別名を定義できます。 - -```php -$tags = $explorer->table('tag') - ->joinWhere(':book_tag.book.author', 'book_author.born < ?', 1950) - ->alias(':book_tag.book.author', 'book_author'); -// LEFT JOIN `book_tag` ON `tag`.`id` = `book_tag`.`tag_id` -// LEFT JOIN `book` ON `book_tag`.`book_id` = `book`.`id` -// LEFT JOIN `author` `book_author` ON `book`.`author_id` = `book_author`.`id` -// AND (`book_author`.`born` < 1950) -``` - -`where()` メソッドが `WHERE` の句に条件を足すのに対し、`joinWhere()` メソッドはテーブルをつなぐときの `ON` の句の条件を広げることに注意してください。 diff --git a/database/ja/guide.texy b/database/ja/guide.texy deleted file mode 100644 index 09c66eb5b7..0000000000 --- a/database/ja/guide.texy +++ /dev/null @@ -1,218 +0,0 @@ -Nette Database -************** - -.[perex] -Nette Database は PHP のための強力で優雅なデータベース層で、単純さと気の利いた機能を大切にしています。データベースを扱う方法を 2 つ用意しています。素早くアプリケーションを開発するための [Explorer |explorer]と、クエリを直接組み立てる [SQL アプローチ |SQL way]です。 - -<div class="grid gap-3"> -<div> - - -[SQL アプローチ|sql-way] -=================== -- 安全でパラメータ化されたクエリ -- SQL のクエリの構造を細かく制御 -- 進んだ機能を使う込み入ったクエリを書くとき -- 特定の SQL の関数を使って性能を最適化 - -</div> - -<div> - - -[Explorer |explorer] -==================== -- SQL を書かずに素早く開発 -- テーブルどうしの関係を直感的に扱う -- クエリの自動的な最適化の恩恵を受ける -- 速く快適にデータベースを扱うのに向く - -</div> - -</div> - - -インストール -====== - -ライブラリは [Composer|best-practices:composer]でダウンロードしてインストールします。 - -```shell -composer require nette/database -``` - - -対応しているデータベース -============ - -Nette Database は次のデータベースに対応しています。 - -|* データベースサーバー |* DSN の名前 |* Explorer の対応 -|-----------------------|--------------|-----------------------| -| MySQL (>= 5.1) | mysql | あり | -| PostgreSQL (>= 9.0) | pgsql | あり | -| SQLite 3 (>= 3.8) | sqlite | あり | -| Oracle | oci | なし | -| MS SQL (PDO_SQLSRV) | sqlsrv | あり | -| MS SQL (PDO_DBLIB) | mssql | なし | -| ODBC | odbc | なし | - - -データベースを扱う 2 つのやり方 -================= - -Nette Database は選択肢を与えます。SQL のクエリを直接書く(SQL アプローチ)か、自動的に生成させる(Explorer)かです。同じ仕事を両方のやり方でどう片付けるか見てみましょう。 - -[SQL アプローチ|sql-way] - SQL のクエリ - -```php -// レコードを挿入します -$database->query('INSERT INTO books', [ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// レコードを取り出します: 本の著者 -$result = $database->query(' - SELECT authors.*, COUNT(books.id) AS books_count - FROM authors - LEFT JOIN books ON authors.id = books.author_id - WHERE authors.active = 1 - GROUP BY authors.id -'); - -// 表示します(最適ではなく、N 個の追加のクエリを生みます) -foreach ($result as $author) { - $books = $database->query(' - SELECT * FROM books - WHERE author_id = ? - ORDER BY published_at DESC - ', $author->id); - - echo "Author $author->name has written $author->books_count books:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -[Explorer のやり方|explorer] - SQL の自動生成 - -```php -// レコードを挿入します -$database->table('books')->insert([ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// レコードを取り出します: 本の著者 -$authors = $database->table('authors') - ->where('active', 1); - -// 表示します(自動的に最適化された 2 つのクエリだけを生みます) -foreach ($authors as $author) { - $books = $author->related('books') - ->order('published_at DESC'); - - echo "Author $author->name has written {$books->count()} books:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -Explorer のやり方は SQL のクエリを自動的に生成して最適化します。上の例では、SQL アプローチが N+1 個のクエリ(著者に 1 つ、そして著者ごとに本のクエリが 1 つずつ)を生むのに対し、Explorer はクエリを自動的に最適化して 2 つだけ、つまり著者に 1 つと、そのすべての本に 1 つを実行します。 - -どちらのやり方も、必要に応じてアプリケーションの中で自由に組み合わせられます。 - - -接続と設定 -===== - -データベースに接続するには、[api:Nette\Database\Connection]クラスのインスタンスを作るだけです。 - -```php -$database = new Nette\Database\Connection($dsn, $user, $password); -``` - -`$dsn`(Data Source Name)パラメータは [PDO が使うもの |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters]と同じで、たとえば `host=127.0.0.1;dbname=test` です。失敗すると `Nette\Database\ConnectionException` を投げます。 - -とはいえ、もっと便利な方法を[アプリケーションの設定 |configuration]が用意しています。そこに `database` の区画を足すだけです。これで必要なオブジェクトが作られ、[Tracy |tracy:]のバーにデータベースのパネルも現れます。 - -```neon -database: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password -``` - -そのあと接続のオブジェクトは [DI コンテナからサービスとして受け取れます |dependency-injection:passing-dependencies]。たとえば次のようにです。 - -```php -class Model -{ - public function __construct( - // または Nette\Database\Explorer - private Nette\Database\Connection $database, - ) { - } -} -``` - -詳しくは[データベースの設定|configuration]をご覧ください。 - - -Explorer を手で作る --------------- - -Nette の DI コンテナを使っていないなら、`Nette\Database\Explorer` のインスタンスを手で作れます。 - -```php -// データベース接続 -$connection = new Nette\Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); -// キャッシュの保管場所。Nette\Caching\Storage を実装します。たとえば: -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); -// データベースの構造のリフレクションを受け持ちます -$structure = new Nette\Database\Structure($connection, $storage); -// テーブル名、列、外部キーの対応づけの規則を定義します -$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); -$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage); -``` - - -接続の管理 -===== - -`Connection` オブジェクトが作られると、接続は自動的に確立されます。接続を遅らせたいなら lazy モードを使います。[設定|configuration]で `lazy` を設定するか、次のようにします。 - -```php -$database = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => true]); -``` - -接続を管理するには `connect()`、`disconnect()`、`reconnect()` メソッドを使います。 -- `connect()` はまだ接続がなければ接続を作り、`Nette\Database\ConnectionException` を投げることがあります。 -- `disconnect()` は今のデータベース接続を切ります。 -- `reconnect()` は接続を切ってから、データベースに接続し直します。このメソッドも `Nette\Database\ConnectionException` を投げることがあります。 - -さらに `onConnect` イベントで接続にまつわる出来事を見張れます。これはデータベースに接続したあとに呼ばれるコールバックの配列です。 - -```php -// データベースに接続したあとに実行されます -$database->onConnect[] = function($database) { - echo "Connected to the database"; -}; -``` - -`onQuery` イベントも同じように働きます。これは実行されたクエリごとに(そしてクエリが失敗したときにも)呼ばれるコールバックの配列で、ログや性能の計測に役立ちます。 - - -Tracy のデバッグバー -============= - -[Tracy |tracy:]を使っていれば、デバッグバーの Database のパネルが自動的に有効になります。そこには実行されたすべてのクエリ、そのパラメータ、実行にかかった時間、そしてそれが呼ばれたコードの場所が表示されます。 - -[* db-panel.webp *] diff --git a/database/ja/reflection.texy b/database/ja/reflection.texy deleted file mode 100644 index c188254a50..0000000000 --- a/database/ja/reflection.texy +++ /dev/null @@ -1,127 +0,0 @@ -構造のリフレクション -********** - -.{data-version:3.2.1} -Nette Database は [api:Nette\Database\Reflection]クラスで、データベースの構造を調べる道具を提供します。テーブル、列、インデックス、外部キーの情報を取り出せます。リフレクションはスキーマの生成、データベースを扱う柔軟なアプリケーション、汎用のデータベース向けの道具などに使えます。 - -リフレクションのオブジェクトはデータベース接続のインスタンスから取り出します。 - -```php -$reflection = $database->getReflection(); -``` - - -テーブルの取得 -------- - -読み取り専用のプロパティ `$reflection->tables` には、データベースのすべてのテーブルの連想配列が入っています。 - -```php -// すべてのテーブルの名前を並べます -foreach ($reflection->tables as $name => $table) { - echo $name . "\n"; -} -``` - -さらに 2 つのメソッドが使えます。 - -```php -// テーブルが存在するかを調べます -if ($reflection->hasTable('users')) { - echo "Table users exists"; -} - -// テーブルのオブジェクトを返します。存在しなければ例外を投げます -$table = $reflection->getTable('users'); -``` - - -テーブルの情報 -------- - -テーブルは [Table|api:Nette\Database\Reflection\Table]オブジェクトが表し、次の読み取り専用のプロパティを持ちます。 - -- `$name: string` - テーブルの名前 -- `$view: bool` - ビューかどうか -- `$fullName: ?string` - スキーマを含むテーブルの完全な名前(あれば) -- `$columns: array<string, Column>` - テーブルの列の連想配列 -- `$indexes: Index[]` - テーブルのインデックスの配列 -- `$primaryKey: ?Index` - テーブルの主キー、または null -- `$foreignKeys: ForeignKey[]` - テーブルの外部キーの配列 -- `$comment: ?string` - テーブルのコメント - - -列 ---- - -テーブルの `columns` プロパティは列の連想配列を返します。キーは列の名前で、値は次のプロパティを持つ [Column|api:Nette\Database\Reflection\Column]のインスタンスです。 - -- `$name: string` - 列の名前 -- `$table: ?Table` - その列のテーブルへの参照 -- `$nativeType: string` - データベース本来の型 -- `$size: ?int` - 型の大きさ/長さ -- `$nullable: bool` - 列が NULL を持てるかどうか -- `$default: mixed` - 列の既定値 -- `$autoIncrement: bool` - 列が自動採番かどうか -- `$primary: bool` - 主キーの一部かどうか -- `$vendor: array` - そのデータベースシステムに固有の追加のメタデータ -- `$comment: ?string` - 列のコメント - -```php -foreach ($table->columns as $name => $column) { - echo "Column: $name\n"; - echo "Type: {$column->nativeType}\n"; - echo "Nullable: " . ($column->nullable ? 'Yes' : 'No') . "\n"; -} -``` - - -インデックス ------- - -テーブルの `indexes` プロパティはインデックスの配列を返します。それぞれのインデックスは次のプロパティを持つ [Index|api:Nette\Database\Reflection\Index]のインスタンスです。 - -- `$columns: Column[]` - インデックスを構成する列の配列 -- `$unique: bool` - インデックスが一意かどうか -- `$primary: bool` - 主キーかどうか -- `$name: ?string` - インデックスの名前 - -テーブルの主キーは `primaryKey` プロパティで取り出せます。これは `Index` オブジェクトか、テーブルに主キーがなければ `null` を返します。 - -```php -// インデックスを並べます -foreach ($table->indexes as $index) { - $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); - echo "Index" . ($index->name ? " {$index->name}" : '') . ":\n"; - echo " Columns: $columns\n"; - echo " Unique: " . ($index->unique ? 'Yes' : 'No') . "\n"; -} - -// 主キーを並べます -if ($primaryKey = $table->primaryKey) { - $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); - echo "Primary Key: $columns\n"; -} -``` - - -外部キー ----- - -テーブルの `foreignKeys` プロパティは外部キーの配列を返します。それぞれの外部キーは次のプロパティを持つ [ForeignKey|api:Nette\Database\Reflection\ForeignKey]のインスタンスです。 - -- `$foreignTable: Table` - 参照先のテーブル -- `$localColumns: Column[]` - 手元の列の配列 -- `$foreignColumns: Column[]` - 参照先の列の配列 -- `$name: string` - 外部キーの名前 - -```php -// 外部キーを並べます -foreach ($table->foreignKeys as $fk) { - $localCols = implode(', ', array_map(fn($col) => $col->name, $fk->localColumns)); - $foreignCols = implode(', ', array_map(fn($col) => $col->name, $fk->foreignColumns)); - - echo "FK" . ($fk->name ? " {$fk->name}" : '') . ":\n"; - echo " $localCols -> {$fk->foreignTable->name}($foreignCols)\n"; -} -``` diff --git a/database/ja/security.texy b/database/ja/security.texy deleted file mode 100644 index 72d96480af..0000000000 --- a/database/ja/security.texy +++ /dev/null @@ -1,185 +0,0 @@ -セキュリティリスク -********* - -<div class=perex> - -データベースには機微なデータが入っていることが多く、危険な操作も行えます。Nette Database を安全に使うには、次のことが欠かせません。 - -- 安全な API と危険な API の違いを理解する -- パラメータ化されたクエリを使う -- 入力データをきちんと検証する - -</div> - - -SQL インジェクションとは何か -================ - -SQL インジェクションはデータベースを扱ううえでもっとも深刻なセキュリティリスクです。これは検査されていないユーザーの入力が SQL のクエリの一部になったときに起こります。攻撃者は自分の SQL のコマンドを差し込んで、次のことができてしまいます。 -- データへの許されないアクセスを得る -- データベースのデータを書き換えたり消したりする -- 認証をすり抜ける - -```php -// ❌ 危険なコード - SQL インジェクションに対して脆弱です -$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); - -// 攻撃者は次のような値を入れるかもしれません: ' OR '1'='1 -// できあがるクエリは: SELECT * FROM users WHERE name = '' OR '1'='1' -// これはすべてのユーザーを返します -``` - -同じことが Database Explorer にも当てはまります。 - -```php -// ❌ 危険なコード - SQL インジェクションに対して脆弱です -$table->where('name = ' . $_GET['name']); -$table->where("name = '$_GET[name]'"); -``` - - -パラメータ化されたクエリ -============ - -SQL インジェクションに対する基本の守りはパラメータ化されたクエリです。Nette Database はその使い方をいくつか用意しています。 - -もっとも単純なのは**疑問符のプレースホルダ**を使うことです。 - -```php -// ✅ 安全なパラメータ化されたクエリ -$database->query('SELECT * FROM users WHERE name = ?', $name); - -// ✅ Explorer での安全な条件 -$table->where('name = ?', $name); -``` - -これは、疑問符のプレースホルダとパラメータで式を入れられる [Database Explorer|explorer]のほかのすべてのメソッドにも当てはまります。 - -INSERT、UPDATE のコマンドや WHERE 句では、値を配列で渡せます。 - -```php -// ✅ 安全な INSERT -$database->query('INSERT INTO users', [ - 'name' => $name, - 'email' => $email, -]); - -// ✅ Explorer での安全な INSERT -$table->insert([ - 'name' => $name, - 'email' => $email, -]); -``` - - -パラメータの値の検証 -========== - -パラメータ化されたクエリは、安全にデータベースを扱うための土台です。とはいえ、そこに入れる値はいくつかの段階の検査を経なければなりません。 - - -型の検査 ----- - -**もっとも大事なのはパラメータのデータ型が正しいことを確かめること**です。これは Nette Database を安全に使うために欠かせない条件です。データベースは、入力データがすべてその列に対応する正しいデータ型を持っていると前提にしています。 - -たとえば前の例の `$name` が思いがけず文字列ではなく配列だったとすると、Nette Database はその要素をすべて SQL のクエリに入れようとして、エラーになります。ですから `$_GET`、`$_POST`、`$_COOKIE` からの検証していないデータを、データベースのクエリで直接**決して使わないでください**。 - - -書式の検証 ------ - -2 つめの段階ではデータの書式を確かめます。たとえば文字列が UTF-8 の文字コードで、長さが列の定義に合っているか、数値がその列のデータ型で許される範囲に収まっているか、といったことです。 - -この段階の検証はデータベース自身にある程度は任せられます。多くのデータベースは正しくないデータを拒みます。とはいえ振る舞いはさまざまで、長い文字列を黙って切り詰めたり、範囲の外の数を丸めたりするものもあります。 - - -ドメインに固有の検証 ----------- - -3 つめの段階は、あなたのアプリケーションに固有の論理的な検査です。たとえば選択肢の値が提示されたものと一致するか、数が思ったとおりの範囲にあるか(年齢 0〜150 歳など)、値どうしの依存関係が筋の通ったものかを確かめます。 - - -おすすめの検証の方法 ----------- - -- すべての入力の適切な検証を自動的に受け持つ [Nette Forms|forms:]を使います。 -- [プレゼンター|application:]を使い、`action*()` と `render*()` メソッドのパラメータにデータ型を指定します。 -- あるいは `filter_var()` のような PHP の標準の道具で独自の検証の層を作ります。 - - -列を安全に扱う -======= - -前の節では、パラメータの値をきちんと検証する方法を見ました。しかし SQL のクエリで配列を使うときは、そのキーにも同じだけ気を配らなければなりません。 - -```php -// ❌ 危険なコード - 配列のキーが検査されていません -$database->query('INSERT INTO users', $_POST); -``` - -INSERT と UPDATE のコマンドでは、これは致命的なセキュリティの欠陥です。攻撃者はデータベースのどの列でも挿入したり書き換えたりできてしまいます。たとえば `is_admin = 1` を設定したり、機微な列に好きなデータを入れたりできます(いわゆる Mass Assignment Vulnerability)。 - -WHERE の条件ではさらに危険です。演算子を含められるからです。 - -```php -// ❌ 危険なコード - 配列のキーが検査されていません -$_POST['salary >'] = 100000; -$database->query('SELECT * FROM users WHERE', $_POST); -// クエリ WHERE (`salary` > 100000) が実行されます -``` - -攻撃者はこのやり方で従業員の給与を順に探り当てられます。たとえばまず 100,000 より上の給与を問い合わせ、次に 50,000 より下を問い合わせ、範囲を少しずつ狭めていくことで、全員のおおよその給与を暴けます。この種の攻撃を SQL 列挙と呼びます。 - -`where()` と `whereOr()` メソッドは[さらにずっと柔軟で |explorer#where()]、キーと値の中で演算子や関数を含む SQL の式に対応しています。これは攻撃者に SQL インジェクションの余地を与えます。 - -```php -// ❌ 危険なコード - 攻撃者が自分の SQL を差し込めます -$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1']; -$table->where($_POST); -// クエリ WHERE (0) UNION SELECT name, salary FROM users WHERE (1) が実行されます -``` - -この攻撃は `0)` でもとの条件を終わらせ、`UNION` で自分の `SELECT` を継ぎ足して `users` テーブルの機微なデータを得て、`WHERE (1)` で構文的に正しいクエリとして閉じています。 - - -列のホワイトリスト ---------- - -列の名前を安全に扱うには、ユーザーが許された列だけを扱えて、自分で列を足せないようにするしくみが要ります。危険な列の名前を見つけて遮る(ブラックリスト)こともできますが、この手はあてになりません。攻撃者はこちらが思いつかなかった、危険な列の名前の新しい書き方をいつでも編み出せるからです。 - -ですから発想を逆にして、許される列のはっきりした一覧(ホワイトリスト)を定義するほうがずっと安全です。 - -```php -// ユーザーが変えてよい列 -$allowedColumns = ['name', 'email', 'active']; - -// 入力から許されていない列をすべて取り除きます -$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); - -// ✅ これでクエリで安全に使えます。たとえば次のようにです: -$database->query('INSERT INTO users', $filteredData); -$table->update($filteredData); -$table->where($filteredData); -``` - - -動的な識別子 -====== - -テーブルや列の名前を動的に扱うには `?name` のプレースホルダを使います。これはそのデータベースの構文に従って識別子をきちんとエスケープします(MySQL ならバッククォートを使います)。 - -```php -// ✅ 信頼できる識別子の安全な使い方 -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name', $column, $table); -// MySQL での結果: SELECT `name` FROM `users` -``` - -大事な点として、`?name` の記号はアプリケーションのコードで定義した信頼できる値にだけ使ってください。ユーザーから来る値には、やはり[ホワイトリスト |#列のホワイトリスト]を使います。さもないとセキュリティリスクにさらされます。 - -```php -// ❌ 危険 - ユーザーの入力を決して使わないでください -$database->query('SELECT ?name FROM users', $_GET['column']); -``` diff --git a/database/ja/sql-way.texy b/database/ja/sql-way.texy deleted file mode 100644 index 13bf64ffd6..0000000000 --- a/database/ja/sql-way.texy +++ /dev/null @@ -1,513 +0,0 @@ -SQL アプローチ -********* - -.[perex] -Nette Database は 2 つのやり方を用意しています。SQL のクエリを自分で書くか(SQL アプローチ)、自動的に生成させるか([Explorer |explorer]をご覧ください)です。SQL アプローチはクエリを完全に思いどおりにしつつ、それが安全に組み立てられることを保証します。 - -.[note] -データベースへの接続と設定の詳しい話は [接続と設定 |guide#接続と設定]の章にあります。 - - -基本のクエリ -====== - -データベースへの問い合わせには `query()` メソッドを使います。これはクエリの結果を表す [ResultSet |api:Nette\Database\ResultSet]オブジェクトを返します。クエリが失敗すると、このメソッドは[例外を投げます|exceptions]。クエリの結果は `foreach` のループで回せますし、[補助のメソッド |#データの取り出し]のどれかを使えます。 - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; -} -``` - -SQL のクエリに値を安全に入れるには、パラメータ化されたクエリを使います。Nette Database ではこれがきわめて簡単で、SQL のクエリのうしろにコンマと値を足すだけです。 - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -パラメータが複数あるときは 2 通りの書き方があります。SQL のクエリとパラメータを交互に並べられます。 - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age); -``` - -あるいは先に SQL のクエリ全体を書いて、そのあとにすべてのパラメータを並べられます。 - -```php -$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); -``` - - -SQL インジェクションからの保護 -================= - -なぜパラメータ化されたクエリを使うことが大事なのでしょうか。それは SQL インジェクションと呼ばれる攻撃から守ってくれるからです。この攻撃では、攻撃者が自分の SQL のコマンドを差し込んで、データベースのデータにアクセスしたり壊したりできてしまいます。 - -.[warning] -**変数を SQL のクエリに直接入れては決していけません。** いつもパラメータ化されたクエリを使ってください。それが SQL インジェクションから守ってくれます。 - -```php -// ❌ 危険なコード - SQL インジェクションに対して脆弱です -$database->query("SELECT * FROM users WHERE name = '$name'"); - -// ✅ 安全なパラメータ化されたクエリ -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -[起こりうるセキュリティリスク |security]も知っておいてください。 - - -クエリの書き方 -======= - - -WHERE の条件 ---------- - -`WHERE` の条件は連想配列で書けます。キーは列の名前、値は比較するデータです。Nette Database は値の型をもとに、いちばんふさわしい SQL の演算子を自動的に選びます。 - -```php -$database->query('SELECT * FROM users WHERE', [ - 'name' => 'John', - 'active' => true, -]); -// WHERE `name` = 'John' AND `active` = 1 -``` - -キーの中で比較の演算子をはっきり指定することもできます。 - -```php -$database->query('SELECT * FROM users WHERE', [ - 'age >' => 25, // > 演算子を使います - 'name LIKE' => '%John%', // LIKE 演算子を使います - 'email NOT LIKE' => '%example.com%', // NOT LIKE 演算子を使います -]); -// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' -``` - -Nette は `null` の値や配列といった特別な場合も自動的に扱います。 - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name' => 'Laptop', // = 演算子を使います - 'category_id' => [1, 2, 3], // IN を使います - 'description' => null, // IS NULL を使います -]); -// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL -``` - -否定の条件には `NOT` 演算子を使います。 - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // != 演算子を使います - 'category_id NOT' => [1, 2, 3], // NOT IN を使います - 'description NOT' => null, // IS NOT NULL を使います - 'id NOT' => [], // 飛ばされます -]); -// WHERE `name` != 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL -``` - -既定では条件は `AND` 演算子でつながれます。これは [?or のプレースホルダ |#SQL の組み立てのヒント]で変えられます。 - - -ORDER BY の規則 ------------- - -`ORDER BY` の句は配列で書けます。キーに列を指定し、真偽値で昇順(`true`)か降順(`false`)かを示します。 - -```php -$database->query('SELECT id FROM author ORDER BY', [ - 'id' => true, // 昇順 - 'name' => false, // 降順 -]); -// SELECT id FROM author ORDER BY `id`, `name` DESC -``` - - -データの挿入(INSERT) --------------- - -レコードの挿入には SQL の `INSERT` コマンドを使います。 - -```php -$values = [ - 'name' => 'John Doe', - 'email' => 'john@example.com', -]; -$database->query('INSERT INTO users ?', $values); -$userId = $database->getInsertId(); -``` - -`getInsertId()` メソッドは最後に挿入された行の ID を返します。データベースによっては(PostgreSQL など)、ID を生成するシーケンスの名前をパラメータとして `$database->getInsertId($sequenceId)` のように指定する必要があります。 - -パラメータとしては[特別な値 |#特別な値]、たとえばファイル、DateTime オブジェクト、enum 型も渡せます。 - -複数のレコードを一度に挿入します。 - -```php -$database->query('INSERT INTO users ?', [ - ['name' => 'User 1', 'email' => 'user1@mail.com'], - ['name' => 'User 2', 'email' => 'user2@mail.com'], -]); -``` - -複数レコードの INSERT はずっと速くなります。多くの個別のクエリの代わりに、データベースのクエリが 1 つだけ実行されるからです。 - -**セキュリティに関する注意:** 検証していないデータを `$values` として決して使わないでください。[起こりうるリスク |security#列を安全に扱う]を知っておいてください。 - - -データの更新(UPDATE) --------------- - -レコードの更新には SQL の `UPDATE` コマンドを使います。 - -```php -// ひとつのレコードを更新します -$values = [ - 'name' => 'John Smith', -]; -$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); -``` - -影響を受けた行の数は `$result->getRowCount()` が返します。 - -`UPDATE` では `+=` と `-=` の演算子を使えます。 - -```php -$database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // login_count を増やします -], 1); -``` - -レコードがすでにあれば更新し、なければ挿入する例です。`ON DUPLICATE KEY UPDATE` の手法を使います。 - -```php -$values = [ - 'name' => $name, - 'year' => $year, -]; -$database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', - $values + ['id' => $id], - $values, -); -// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978) -// ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 -``` - -Nette Database が、配列のパラメータが SQL のコマンドのどの文脈で使われているかを見分けて、それに応じた SQL のコードを組み立てていることに注目してください。最初の配列からは `(id, name, year) VALUES (123, 'Jim', 1978)` を組み立て、2 つめは `name = 'Jim', year = 1978` の形に変えました。これは [#SQL の組み立てのヒント]の節で詳しく説明します。 - - -データの削除(DELETE) --------------- - -レコードの削除には SQL の `DELETE` コマンドを使います。消された行の数を得る例です。 - -```php -$count = $database->query('DELETE FROM users WHERE id = ?', 1) - ->getRowCount(); -``` - - -SQL の組み立てのヒント -------------- - -ヒントとは、パラメータの値をどう SQL の式に変えるかを指定する、SQL のクエリの中の特別なプレースホルダです。 - -| ヒント | 説明 | 自動的に使われる場面 -|-----------|-------------------------------------------------|----------------------------- -| `?name` | テーブルや列の名前を入れるのに使います | - -| `?values` | `(key, ...) VALUES (value, ...)` を生成します | `INSERT ... ?`、`REPLACE ... ?` -| `?set` | 代入 `key = value, ...` を生成します | `SET ?`、`KEY UPDATE ?` -| `?and` | 配列の条件を `AND` でつなぎます | `WHERE ?`、`HAVING ?` -| `?or` | 配列の条件を `OR` でつなぎます | - -| `?order` | `ORDER BY` の句を生成します | `ORDER BY ?`、`GROUP BY ?` - -`?name` のプレースホルダは、テーブルや列の名前を動的にクエリに入れるのに使います。Nette Database はそのデータベースの流儀に従って識別子を正しく引用符で囲みます(MySQL ならバッククォートで囲みます)。 - -```php -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); -// SELECT `name` FROM `users` WHERE id = 1 (MySQL の場合) -``` - -**注意:** `?name` のプレースホルダは検証済みのテーブル名と列名にだけ使ってください。さもないと[セキュリティ上の弱点 |security#動的な識別子]を招きます。 - -ほかのヒントはふつう指定する必要はありません。Nette は SQL のクエリを組み立てるときに賢く自動判別するからです(表の 3 列めをご覧ください)。とはいえ、たとえば条件を `AND` ではなく `OR` でつなぎたい場面で使えます。 - -```php -$database->query('SELECT * FROM users WHERE ?or', [ - 'name' => 'John', - 'email' => 'john@example.com', -]); -// SELECT * FROM users WHERE `name` = 'John' OR `email` = 'john@example.com' -``` - - -特別な値 ----- - -よくあるスカラーの型(string、int、bool)のほかに、パラメータとして特別な値も渡せます。 - -- ファイル: `fopen('image.gif', 'r')` はファイルの中身をバイナリとして入れます -- 日付と時刻: `DateTimeInterface` のオブジェクトはデータベースの書式に変換されます -- enum 型: `enum` のインスタンスはその値に変換されます -- SQL のリテラル: `Connection::literal('NOW()')` で作られ、そのままクエリに入ります - -```php -$database->query('INSERT INTO articles ?', [ - 'title' => 'My Article', - 'published_at' => new DateTimeImmutable, // または new DateTime - 'content' => fopen('image.png', 'r'), - 'state' => Status::Draft, -]); -``` - -`datetime` のデータ型を本来は持たないデータベース(SQLite や Oracle など)では、`DateTime` と `DateTimeImmutable` のオブジェクトは、[データベースの設定|configuration]の `formatDateTime` の項目で指定された値に変換されます(既定値は `U`、つまり Unix タイムスタンプです)。 - - -SQL のリテラル ---------- - -生の SQL のコードを値として渡し、文字列として扱われたりエスケープされたりしないようにしたい場合があります。そのために `Nette\Database\SqlLiteral` クラスのオブジェクトを使います。これは `Connection::literal()` メソッドで作ります。 - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - 'year >' => $database::literal('YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) -``` - -あるいは次のようにも書けます。 - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) -``` - -SQL のリテラルはパラメータを含められます。 - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > ? AND year < ?', $min, $max), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) -``` - -これで面白い組み合わせが作れます。 - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('?or', [ - 'active' => true, - 'role' => $role, - ]), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (`active` = 1 OR `role` = 'admin') -``` - - -データの取り出し -======== - - -SELECT のクエリの近道 --------------- - -データの取り出しを簡単にするために、`Connection` は `query()` の呼び出しと、そのあとの `fetch*()` の呼び出しをひとつにまとめた近道をいくつか用意しています。これらのメソッドは `query()` と同じパラメータ、つまり SQL のクエリと省略できるパラメータを受け取ります。`fetch*()` メソッドの詳しい説明は[下 |#fetch()]にあります。 - -| `fetch($sql, ...$params): ?Row` | クエリを実行し、最初の行を `Row` オブジェクトとして、なければ `null` を返します。 -| `fetchAll($sql, ...$params): array` | クエリを実行し、すべての行を `Row` オブジェクトの配列として返します。 -| `fetchPairs($sql, ...$params): array` | クエリを実行し、連想配列(キー => 値の組)を返します。 -| `fetchField($sql, ...$params): mixed` | クエリを実行し、最初の行の最初の列の値を返します。 -| `fetchList($sql, ...$params): ?array` | クエリを実行し、最初の行を添字の配列として、なければ `null` を返します。 - -例です。 - -```php -// fetchField() - 最初のセルの値を返します -$count = $database->query('SELECT COUNT(*) FROM articles') - ->fetchField(); -``` - - -`foreach` - 行を順に回す ------------------- - -クエリを実行すると [ResultSet|api:Nette\Database\ResultSet]オブジェクトが返され、結果をいくつかの方法で回せます。クエリを実行して行を取り出すいちばん簡単な方法は、`foreach` のループで回すことです。この方法はメモリをもっとも節約します。データを 1 行ずつ取り出し、結果全体を一度にメモリへ読み込まないからです。 - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; - // ... -} -``` - -.[note] -`ResultSet` は一度しか回せません。何度も回す必要があるなら、まず `fetchAll()` メソッドなどでデータを配列に読み込まなければなりません。 - - -fetch(): ?Row .[method] ------------------------ - -行を `Row` オブジェクトとして返します。行がもうなければ `null` を返します。内部のポインタを次の行へ進めます。 - -```php -$result = $database->query('SELECT * FROM users'); -$row = $result->fetch(); // 最初の行を読み込みます -if ($row) { - echo $row->name; -} -``` - - -fetchAll(): array .[method] ---------------------------- - -`ResultSet` に残っているすべての行を `Row` オブジェクトの配列として返します。 - -```php -$result = $database->query('SELECT * FROM users'); -$rows = $result->fetchAll(); // すべての行を読み込みます -foreach ($rows as $row) { - echo $row->name; -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -結果を連想配列として返します。第 1 引数はキーとして使う列、第 2 引数は値として使う列を指定します。 - -```php -$result = $database->query('SELECT id, name FROM users'); -$names = $result->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -第 1 パラメータ(`$key`)だけを渡すと、行全体(`Row` オブジェクト)が値として使われます。 - -```php -$rows = $result->fetchPairs('id'); -// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...] -``` - -キーが重なった場合は最後の行の値が使われます。キーに `null` を使うと、ゼロから始まる添字の配列になり、キーの衝突が起きません。 - -```php -$names = $result->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -代わりに、行ごとに処理するコールバックを渡せます。コールバックはひとつの値か、キーと値の組を返せます。 - -```php -$result = $database->query('SELECT * FROM users'); -$items = $result->fetchPairs(fn($row) => "$row->id - $row->name"); -// ['1 - John', '2 - Jane', ...] - -// コールバックはキーと値の組の配列を返すこともできます: -$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); -// ['John' => 46, 'Jane' => 21, ...] -``` - - -fetchField(): mixed .[method] ------------------------------ - -今の行の最初の列の値を返します。行がもうなければ `null` を返します。内部のポインタを次の行へ進めます。 - -```php -$result = $database->query('SELECT name FROM users'); -$name = $result->fetchField(); // 最初の行から name を読み込みます -``` - - -fetchList(): ?array .[method] ------------------------------ - -行を添字の配列として返します。行がもうなければ `null` を返します。内部のポインタを次の行へ進めます。 - -```php -$result = $database->query('SELECT name, email FROM users'); -$row = $result->fetchList(); // ['John', 'john@example.com'] -``` - - -getRowCount(): ?int .[method] ------------------------------ - -直前の `UPDATE` または `DELETE` のクエリで影響を受けた行の数を返します。`SELECT` のクエリでは結果の行の数を返します。ただしこれは常に分かるとは限らず、その場合このメソッドは `null` を返します。 - - -getColumnCount(): ?int .[method] --------------------------------- - -`ResultSet` の列の数を返します。 - - -クエリの情報 -====== - -デバッグのために、最後に実行されたクエリの情報を取り出せます。 - -```php -echo $database->getLastQueryString(); // SQL のクエリを出力します - -$result = $database->query('SELECT * FROM articles'); -echo $result->getQueryString(); // SQL のクエリを出力します -echo $result->getTime(); // 実行にかかった時間を秒で出力します -``` - -結果を HTML の表として表示するには次のようにします。 - -```php -$result = $database->query('SELECT * FROM articles'); -$result->dump(); -``` - -`ResultSet` は列の型の情報も提供します。 - -```php -$result = $database->query('SELECT * FROM articles'); -$types = $result->getColumnTypes(); - -foreach ($types as $column => $type) { - echo "$column is of type $type"; // たとえば 'id is of type int' -} -``` - - -クエリのログ ------- - -クエリのログを独自に作れます。`onQuery` イベントは、実行されたクエリごとに呼ばれるコールバックの配列です。 - -```php -$database->onQuery[] = function ($database, $result) use ($logger) { - $logger->info('Query: ' . $result->getQueryString()); - $logger->info('Time: ' . $result->getTime()); - - if ($result->getRowCount() > 1000) { - $logger->warning('Large result set: ' . $result->getRowCount() . ' rows'); - } -}; -``` diff --git a/database/ja/transactions.texy b/database/ja/transactions.texy deleted file mode 100644 index d74fc82da4..0000000000 --- a/database/ja/transactions.texy +++ /dev/null @@ -1,45 +0,0 @@ -トランザクション -******** - -.[perex] -トランザクションは、その中のすべての操作が実行されるか、まったく実行されないかのどちらかであることを保証します。込み入った操作でデータの整合性を守るのに役立ちます。 - -トランザクションのもっとも単純な使い方は次のようになります。 - -```php -$database->beginTransaction(); -try { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); - $database->commit(); -} catch (\Exception $e) { - $database->rollBack(); - throw $e; -} -``` - -同じことを `transaction()` メソッドでずっと優雅にできます。これはトランザクションの中で実行されるコールバックを受け取ります。コールバックが例外を投げずに終われば、トランザクションは自動的にコミットされます。例外が起きればトランザクションはロールバックされ、例外はさらに上へ伝わります。 - -```php -$database->transaction(function ($database) use ($id) { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); -}); -``` - -`transaction()` の呼び出しは入れ子にできるので、それぞれが自分のトランザクションを管理するメソッドを簡単に組み合わせられます。実際に `BEGIN`/`COMMIT` としてデータベースに送られるのは、いちばん外側のトランザクションだけで、内側の呼び出しは入れ子の深さを数えるだけです。`transaction()` のコールバックの中で `beginTransaction()`、`commit()`、`rollBack()` を手で呼ぶと `LogicException` が投げられます。 - -`transaction()` メソッドは値を返すこともできます。 - -```php -$count = $database->transaction(function ($database) { - $result = $database->query('UPDATE users SET active = ?', true); - return $result->getRowCount(); // 更新された行の数を返します -}); -``` diff --git a/database/ja/type-conversion.texy b/database/ja/type-conversion.texy deleted file mode 100644 index bafc96e51e..0000000000 --- a/database/ja/type-conversion.texy +++ /dev/null @@ -1,55 +0,0 @@ -型変換 -*** - -.[perex] -Nette Database はデータベースから返された値を、対応する PHP の型に自動的に変換します。 - - -日付と時刻 ------ - -時刻の値は `Nette\Utils\DateTime` オブジェクトに変換されます。変更できない `Nette\Database\DateTime` オブジェクトに変換させたいなら、[設定 |configuration]で `newDateTime` オプションを true にします。 - -```php -$row = $database->fetch('SELECT created_at FROM articles'); -echo $row->created_at instanceof DateTime; // true -echo $row->created_at->format('j. n. Y'); -``` - -MySQL の場合、`TIME` のデータ型は `DateInterval` オブジェクトに変換されます。 - - -真偽値 ---- - -真偽値は自動的に `true` か `false` に変換されます。MySQL では、[設定 |configuration]で `convertBoolean` を設定すると `TINYINT(1)` が変換されます。 - -```php -$row = $database->fetch('SELECT is_published FROM articles'); -echo gettype($row->is_published); // 'boolean' -``` - - -数値 ---- - -数値はデータベースの列の型に応じて `int` か `float` に変換されます。 - -```php -$row = $database->fetch('SELECT id, price FROM products'); -echo gettype($row->id); // integer -echo gettype($row->price); // float -``` - - -独自の正規化 ------- - -`setRowNormalizer(?callable $normalizer)` メソッドで、データベースから来た行を変換する独自の関数を設定できます。たとえばデータ型の自動変換に役立ちます。 - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // ここで型変換が行われます - return $row; -}); -``` diff --git a/database/ja/upgrading.texy b/database/ja/upgrading.texy deleted file mode 100644 index 9372b3ff3f..0000000000 --- a/database/ja/upgrading.texy +++ /dev/null @@ -1,36 +0,0 @@ -アップグレード -******* - - -バージョン 3.2 へのアップグレード -=================== - -必要な PHP の最小のバージョンは 8.1 です。 - -コードは PHP 8.1 に合わせて丁寧に手を入れられました。メソッドとプロパティに新しい型宣言がすべて足されています。変更は小さなものです。 - -- MySQL: ゼロの日付 `0000-00-00` は `null` として返されます -- MySQL: 小数部のない decimal は float ではなく int として返されます -- `time` 型は、日付を現在の日付ではなく `0001-01-01` にした `DateTime` オブジェクトとして返されます - - -バージョン 3.1 へのアップグレード -=================== - -- クラス `Nette\Database\Context` は、[Database Explorer|explorer]という名前と揃えるために `Nette\Database\Explorer` に改名されました -- インターフェース `Nette\Database\IRow` と `Nette\Database\IRowContainer` は不要なものとして非推奨になりました -- `MySqlDriver` ドライバはサブクエリを使います -- SQL の文の翻訳器が、配列を渡せる場所をよりよく見張ります - - -バージョン 3.0 へのアップグレード -=================== - -`fetch()` や `fetchField()` などのいくつかのメソッドは、次の行がないときに `false` ではなく `null` を返すようになりました。 - - -バージョン 2.3 へのアップグレード -=================== - -- `MySqlDriver` は MySQL >= 5.5.3 で既定の文字コードとして `utf8` ではなく `utf8mb4` を使います -- `IReflection` は対になるインターフェース `IStructure` と `IConventions` に分けられました diff --git a/database/meta.json b/database/meta.json deleted file mode 100644 index 3a8408cd13..0000000000 --- a/database/meta.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": "4.x", - "repo": "nette/database", - "composer": "nette/database", - "api": "https://api.nette.org/database/" -} diff --git a/database/pl/@home.texy b/database/pl/@home.texy deleted file mode 100644 index bdb1bffaee..0000000000 --- a/database/pl/@home.texy +++ /dev/null @@ -1,18 +0,0 @@ -Obsługiwane bazy danych -======================= - -Obsługiwane są następujące serwery baz danych: - -|* Serwer bazy danych |* Nazwa DSN |* Wsparcie Core |* Wsparcie Explorer -| MySQL (>= 5.1) | mysql | TAK | TAK -| PostgreSQL (>= 9.0) | pgsql | TAK | TAK -| Sqlite 3 (>= 3.8) | sqlite | TAK | TAK -| Oracle | oci | TAK | - -| MS SQL (PDO_SQLSRV) | sqlsrv | TAK | TAK -| MS SQL (PDO_DBLIB) | mssql | TAK | - -| ODBC | odbc | TAK | - - - - -{{maintitle: Nette Database - świetna warstwa bazodanowa dla PHP}} -{{description: Nette Database znacząco upraszcza pobieranie danych z bazy bez pisania zapytań SQL. Wykonuje efektywne zapytania i nie przesyła niepotrzebnych danych.}} diff --git a/database/pl/@left-menu.texy b/database/pl/@left-menu.texy deleted file mode 100644 index 0469ba34f8..0000000000 --- a/database/pl/@left-menu.texy +++ /dev/null @@ -1,21 +0,0 @@ -Nette Database -************** -- [Pierwsze kroki |guide] -- [Podejście SQL|sql-way] -- [Explorer|explorer] -- [Transakcje|transactions] -- [Wyjątki|exceptions] -- [Refleksja|reflection] -- [Konwersja typów |type-conversion] -- [Konfiguracja|configuration] -- [Zagrożenia bezpieczeństwa |security] -- [Aktualizacja|upgrading] - - -Dalsza lektura -************** -- [Dokumentacja Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Dobre praktyki |best-practices:] -- [Rozwiązywanie problemów |nette:troubleshooting] diff --git a/database/pl/@meta.texy b/database/pl/@meta.texy deleted file mode 100644 index 61ac92d1af..0000000000 --- a/database/pl/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Dokumentacja Nette}} diff --git a/database/pl/configuration.texy b/database/pl/configuration.texy deleted file mode 100644 index ed4eff1875..0000000000 --- a/database/pl/configuration.texy +++ /dev/null @@ -1,110 +0,0 @@ -Konfiguracja bazy danych -************************ - -.[perex] -Przegląd opcji konfiguracyjnych dla Nette Database. - -Jeśli nie używasz całego frameworku, tylko tej biblioteki, przeczytaj, [jak wczytać konfigurację|bootstrap:]. - - -Jedno połączenie ----------------- - -Konfiguracja jednego połączenia z bazą danych: - -```neon -database: - # DSN, jedyny obowiązkowy klucz - dsn: "sqlite:%appDir%/Model/demo.db" - user: ... - password: ... -``` - -Utworzy to usługi `Nette\Database\Connection` i `Nette\Database\Explorer`, które przekazujemy zwykle przez [autowiring |dependency-injection:autowiring] albo odwołując się do [ich nazwy |#Usługi DI]. - -Pozostałe ustawienia: - -```neon -database: - # pokazać panel bazy danych w Tracy Bar? - debugger: ... # (bool) domyślnie włączone, jeśli Tracy jest aktywne - - # pokazać EXPLAIN zapytań w Tracy Bar? - explain: ... # (bool) domyślnie true - - # włączyć autowiring dla tego połączenia? - autowired: ... # (bool) domyślnie true dla pierwszego połączenia - - # konwencje tabel: discovered, static albo nazwa klasy - conventions: discovered # (string) domyślnie 'discovered' - - options: - # łączyć się z bazą danych dopiero wtedy, gdy jest potrzebna? - lazy: ... # (bool) domyślnie false - - # klasa sterownika bazy danych w PHP - driverClass: # (string) - - # tylko MySQL: ustawia sql_mode - sqlmode: # (string) - - # tylko MySQL: ustawia SET NAMES - charset: # (string) domyślnie 'utf8mb4' - - # tylko MySQL: konwertuje TINYINT(1) na bool - convertBoolean: # (bool) domyślnie false - - # zwraca kolumny dat jako obiekty niezmienne (od wersji 3.2.1) - newDateTime: # (bool) domyślnie false - - # tylko Oracle i SQLite: format zapisu daty - formatDateTime: # (string) domyślnie 'U' -``` - -Klucz `options` może zawierać także inne opcje, które znajdziesz w [dokumentacji sterowników PDO |https://www.php.net/manual/en/pdo.drivers.php], na przykład: - -```neon -database: - options: - PDO::MYSQL_ATTR_COMPRESS: true -``` - - -Wiele połączeń --------------- - -W konfiguracji możemy zdefiniować wiele połączeń z bazą danych, dzieląc je na nazwane sekcje: - -```neon -database: - main: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password - - another: - dsn: 'sqlite::memory:' -``` - -Autowiring jest włączony tylko dla usług z pierwszej sekcji. Można to zmienić za pomocą `autowired: false` albo `autowired: true`. - - -Usługi DI ---------- - -Do kontenera DI dodawane są te usługi, gdzie `###` oznacza nazwę połączenia: - -| Nazwa | Typ | Opis -|---------------------------|---------------------------------|--------------------------- -| `database.###.connection` | [api:Nette\Database\Connection] | połączenie z bazą danych -| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |explorer] - - -Jeśli zdefiniujemy tylko jedno połączenie, nazwy usług będą brzmieć `database.default.connection` i `database.default.explorer`. Jeśli zdefiniujemy wiele połączeń jak w powyższym przykładzie, nazwy będą odpowiadać sekcjom, czyli `database.main.connection`, `database.main.explorer`, a także `database.another.connection` i `database.another.explorer`. - -Usługi spoza autowiringu przekazujemy jawnie, odwołując się do ich nazwy: - -```neon -services: - - UserFacade(@database.another.connection) -``` diff --git a/database/pl/exceptions.texy b/database/pl/exceptions.texy deleted file mode 100644 index 3d63166710..0000000000 --- a/database/pl/exceptions.texy +++ /dev/null @@ -1,37 +0,0 @@ -Wyjątki -******* - -Nette Database używa hierarchii wyjątków. Klasą bazową jest `Nette\Database\DriverException`, która rozszerza `PDOException` i udostępnia rozszerzoną funkcjonalność pracy z błędami bazy danych: - -- Metoda `getDriverCode()` zwraca kod błędu ze sterownika bazy danych. -- Metoda `getSqlState()` zwraca kod SQLSTATE. -- Metody `getQueryString()` i `getParameters()` pozwalają pobrać oryginalne zapytanie i jego parametry. - -Klasę `DriverException` rozszerzają następujące wyspecjalizowane wyjątki: - -- `ConnectionException` - sygnalizuje niepowodzenie połączenia z serwerem bazy danych. - - `ConnectionLostException` .{data-version:3.2.9} - połączenie zostało przerwane w trakcie operacji (restart serwera, awaria sieci, idle timeout); przed dalszym użyciem konieczne jest ponowne połączenie. -- `ConstraintViolationException` - klasa bazowa dla naruszeń ograniczeń bazy danych, po której dziedziczą poniższe wyjątki: - - `ForeignKeyConstraintViolationException` - naruszenie ograniczenia klucza obcego. - - `NotNullConstraintViolationException` - naruszenie ograniczenia NOT NULL. - - `UniqueConstraintViolationException` - naruszenie ograniczenia unikalności. - - `CheckConstraintViolationException` .{data-version:3.2.9} - naruszenie ograniczenia CHECK. -- `DeadlockException` .{data-version:3.2.9} - deadlock albo konflikt serializacji wykryty przez serwer; transakcja została wycofana i można ją powtórzyć. -- `LockTimeoutException` .{data-version:3.2.9} - przekroczono czas oczekiwania na blokadę; polecenie zostało przerwane, ale otaczająca transakcja zwykle pozostaje otwarta. - -Poniższy przykład pokazuje, jak przechwycić `UniqueConstraintViolationException`, który powstaje przy próbie wstawienia użytkownika z e-mailem już istniejącym w bazie danych (przy założeniu, że kolumna `email` ma indeks unikalny): - -```php -try { - $database->query('INSERT INTO users', [ - 'email' => 'john@example.com', - 'name' => 'John Doe', - 'password' => $hashedPassword, - ]); -} catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'Użytkownik z tym e-mailem już istnieje.'; - -} catch (Nette\Database\DriverException $e) { - echo 'Podczas rejestracji wystąpił błąd: ' . $e->getMessage(); -} -``` diff --git a/database/pl/explorer.texy b/database/pl/explorer.texy deleted file mode 100644 index e2960c972e..0000000000 --- a/database/pl/explorer.texy +++ /dev/null @@ -1,912 +0,0 @@ -Database Explorer -***************** - -<div class=perex> - -Explorer oferuje intuicyjny i efektywny sposób pracy z bazą danych. Sam zajmuje się relacjami między tabelami i optymalizuje zapytania, dzięki czemu możesz skupić się na logice swojej aplikacji. Działa od razu, bez konfiguracji. Jeśli potrzebujesz pełnej kontroli nad zapytaniami SQL, możesz użyć [podejścia SQL |SQL way]. - -- Praca z danymi jest naturalna i łatwa do zrozumienia -- Generuje zoptymalizowane zapytania SQL pobierające tylko potrzebne dane -- Umożliwia łatwy dostęp do danych powiązanych bez potrzeby pisania zapytań JOIN -- Działa natychmiast bez żadnej konfiguracji i generowania encji - -</div> - - -Praca z Explorerem zaczyna się od wywołania metody `table()` na obiekcie [api:Nette\Database\Explorer] (szczegóły ustawiania połączenia z bazą danych znajdziesz w rozdziale [Połączenie i konfiguracja |guide#Połączenie i konfiguracja]): - -```php -$books = $explorer->table('book'); // 'book' to nazwa tabeli -``` - -Metoda zwraca obiekt [Selection |api:Nette\Database\Table\Selection], który reprezentuje zapytanie SQL. Do tego obiektu można doklejać kolejne metody filtrujące i sortujące wyniki. Zapytanie jest składane i wykonywane dopiero w momencie, gdy zażądamy danych, na przykład iterując przez `foreach`. Każdy wiersz reprezentuje obiekt [ActiveRow |api:Nette\Database\Table\ActiveRow]: - -```php -foreach ($books as $book) { - echo $book->title; // wypisuje kolumnę 'title' - echo $book->author_id; // wypisuje kolumnę 'author_id' -} -``` - -Explorer zasadniczo upraszcza pracę z [relacjami między tabelami |#Relacje między tabelami]. Poniższy przykład pokazuje, jak łatwo wypiszemy dane z powiązanych tabel (książki i ich autorzy). Zauważ, że nie trzeba pisać żadnych zapytań JOIN, Nette wygeneruje je za nas: - -```php -$books = $explorer->table('book'); - -foreach ($books as $book) { - echo 'Książka: ' . $book->title; - echo 'Autor: ' . $book->author->name; // tworzy JOIN do tabeli 'author' -} -``` - -Nette Database Explorer optymalizuje zapytania tak, żeby były maksymalnie efektywne. Powyższy przykład wykonuje tylko dwa zapytania SELECT, niezależnie od tego, czy przetwarzamy 10, czy 10 000 książek. - -Poza tym Explorer śledzi, które kolumny są w kodzie używane, i pobiera z bazy tylko je, oszczędzając dalej wydajność. To zachowanie jest w pełni automatyczne i adaptacyjne. Jeśli później zmodyfikujesz kod tak, żeby używał kolejnych kolumn, Explorer automatycznie dostosuje zapytania. Nie musisz nic ustawiać ani myśleć o tym, jakie kolumny będą potrzebne, zostaw to Nette. - - -Filtrowanie i sortowanie -======================== - -Klasa `Selection` udostępnia metody do filtrowania i sortowania wyboru danych. - -.[language-php] -| `where($condition, ...$params)` | Dodaje warunek WHERE. Kilka warunków łączonych jest operatorem AND | -| `whereOr(array $conditions)` | Dodaje grupę warunków WHERE łączonych operatorem OR | -| `wherePrimary($value)` | Dodaje warunek WHERE na podstawie klucza głównego | -| `order($columns, ...$params)` | Ustawia sortowanie przez ORDER BY | -| `select($columns, ...$params)` | Określa, które kolumny pobierać | -| `limit($limit, $offset = null)` | Ogranicza liczbę wierszy (LIMIT) i opcjonalnie ustawia OFFSET | -| `page($page, $itemsPerPage, &$numOfPages = null)` | Ustawia stronicowanie | -| `group($columns, ...$params)` | Grupuje wiersze (GROUP BY) | -| `having($condition, ...$params)`| Dodaje warunek HAVING do filtrowania zgrupowanych wierszy | - -Metody można łączyć w łańcuch (tak zwany [interfejs płynny |nette:introduction-to-object-oriented-programming#Interfejsy płynne]): `$table->where(...)->order(...)->limit(...)`. - -W tych metodach możesz też używać specjalnych zapisów do dostępu do [danych z powiązanych tabel |#Zapytania przez powiązane tabele]. - - -Escapowanie i identyfikatory ----------------------------- - -Metody automatycznie escapują parametry i cytują identyfikatory (nazwy tabel i kolumn), zapobiegając SQL injection. Żeby wszystko działało poprawnie, trzeba przestrzegać kilku zasad: - -- Słowa kluczowe, nazwy funkcji, procedur itd. pisz **wielkimi literami**. -- Nazwy kolumn i tabel pisz **małymi literami**. -- Ciągi zawsze przekazuj przez **parametry**. - -```php -where('name = ' . $name); // KRYTYCZNA LUKA: SQL injection -where('name LIKE "%search%"'); // ŹLE: komplikuje automatyczne cytowanie -where('name LIKE ?', '%search%'); // POPRAWNIE: wartość przekazana jako parametr - -where('name like ?', $name); // ŹLE: wygeneruje: `name` `like` ? -where('name LIKE ?', $name); // POPRAWNIE: wygeneruje: `name` LIKE ? -where('LOWER(name) = ?', $value);// POPRAWNIE: LOWER(`name`) = ? -``` - - -where(string|array $condition, ...$parameters): static .[method] ----------------------------------------------------------------- - -Filtruje wyniki za pomocą warunków WHERE. Jej siła tkwi w inteligentnej obsłudze różnych typów wartości i automatycznym wyborze odpowiednich operatorów SQL. - -Podstawowe użycie: - -```php -$table->where('id', $value); // WHERE `id` = 123 -$table->where('id > ?', $value); // WHERE `id` > 123 -$table->where('id = ? OR name = ?', $id, $name); // WHERE `id` = 1 OR `name` = 'Jon Snow' -``` - -Dzięki automatycznemu wykrywaniu odpowiednich operatorów nie musisz obsługiwać różnych przypadków szczególnych, Nette rozwiąże je za Ciebie: - -```php -$table->where('id', 1); // WHERE `id` = 1 -$table->where('id', null); // WHERE `id` IS NULL -$table->where('id', [1, 2, 3]); // WHERE `id` IN (1, 2, 3) -// Możesz też użyć zastępnika ? bez operatora: -$table->where('id ?', 1); // WHERE `id` = 1 -``` - -Metoda poprawnie obsługuje warunki negatywne i puste tablice: - -```php -$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- nic nie znajdzie -$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- znajdzie wszystko -$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- znajdzie wszystko -// $table->where('NOT id ?', $ids); // UWAGA: ta składnia nie jest wspierana -``` - -Jako parametr możesz przekazać także wynik zapytania z innej tabeli, tworząc podzapytanie: - -```php -// WHERE `id` IN (SELECT `id` FROM `tableName`) -$table->where('id', $explorer->table($tableName)); - -// WHERE `id` IN (SELECT `col` FROM `tableName`) -$table->where('id', $explorer->table($tableName)->select('col')); -``` - -Warunki możesz przekazać także jako tablicę, której elementy łączone są operatorem AND: - -```php -// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) -$table->where([ - 'price_final < price_original', - 'stock_count > min_stock', -]); -``` - -W tablicy możesz używać par klucz => wartość, a Nette znów automatycznie wybierze poprawne operatory: - -```php -// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3)) -$table->where([ - 'status' => 'active', - 'id' => [1, 2, 3], -]); -``` - -W tablicy możesz łączyć wyrażenia SQL z zastępnikami i wieloma parametrami. Nadaje się to do złożonych warunków z precyzyjnie określonymi operatorami: - -```php -// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) -$table->where([ - 'age > ?' => 18, - 'ROUND(score, ?) > ?' => [2, 75.5], // dwa parametry przekazujemy jako tablicę -]); -``` - -Wielokrotne wywołania `where()` automatycznie łączą warunki operatorem AND. - - -whereOr(array $parameters): static .[method] --------------------------------------------- - -Podobnie jak `where()` dodaje warunki, ale łączy je operatorem OR: - -```php -// WHERE (`status` = 'active') OR (`deleted` = 1) -$table->whereOr([ - 'status' => 'active', - 'deleted' => true, -]); -``` - -Można tu również używać bardziej złożonych wyrażeń: - -```php -// WHERE (`price` > 1000) OR (`price_with_tax` > 1500) -$table->whereOr([ - 'price > ?' => 1000, - 'price_with_tax > ?' => 1500, -]); -``` - - -wherePrimary(mixed $key): static .[method] ------------------------------------------- - -Dodaje warunek na klucz główny tabeli: - -```php -// WHERE `id` = 123 -$table->wherePrimary(123); - -// WHERE `id` IN (1, 2, 3) -$table->wherePrimary([1, 2, 3]); -``` - -Jeśli tabela ma złożony klucz główny (np. `foo_id`, `bar_id`), przekaż go jako tablicę: - -```php -// WHERE `foo_id` = 1 AND `bar_id` = 5 -$table->wherePrimary(['foo_id' => 1, 'bar_id' => 5])->fetch(); - -// WHERE (`foo_id`, `bar_id`) IN ((1, 5), (2, 3)) -$table->wherePrimary([ - ['foo_id' => 1, 'bar_id' => 5], - ['foo_id' => 2, 'bar_id' => 3], -])->fetchAll(); -``` - - -order(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Określa kolejność, w jakiej zwracane są wiersze. Możesz sortować według jednej albo wielu kolumn, rosnąco albo malejąco, albo według własnego wyrażenia: - -```php -$table->order('created'); // ORDER BY `created` -$table->order('created DESC'); // ORDER BY `created` DESC -$table->order('priority DESC, created'); // ORDER BY `priority` DESC, `created` -$table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC -``` - - -select(string $columns, ...$parameters): static .[method] ---------------------------------------------------------- - -Określa kolumny, które mają zostać zwrócone z bazy danych. Domyślnie Nette Database Explorer zwraca tylko te kolumny, które są faktycznie używane w kodzie. Metody `select()` użyj wtedy, gdy potrzebujesz pobrać konkretne wyrażenia: - -```php -// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` -$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); -``` - -Aliasy zdefiniowane przez `AS` są potem dostępne jako właściwości obiektu `ActiveRow`: - -```php -foreach ($table as $row) { - echo $row->formatted_date; // dostęp do aliasu -} -``` - - -limit(?int $limit, ?int $offset = null): static .[method] ---------------------------------------------------------- - -Ogranicza liczbę zwracanych wierszy (LIMIT) i opcjonalnie pozwala ustawić offset: - -```php -$table->limit(10); // LIMIT 10 (zwraca pierwsze 10 wierszy) -$table->limit(10, 20); // LIMIT 10 OFFSET 20 -``` - -Do stronicowania bardziej odpowiednia jest metoda `page()`. - - -page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] -------------------------------------------------------------------------- - -Ułatwia stronicowanie wyników. Przyjmuje numer strony (liczony od 1) i liczbę pozycji na stronie. Opcjonalnie możesz przekazać referencję do zmiennej, w której zostanie zapisana łączna liczba stron: - -```php -$numOfPages = null; -$table->page(page: 3, itemsPerPage: 10, numOfPages: $numOfPages); -echo "Łącznie stron: $numOfPages"; -``` - - -group(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Grupuje wiersze według podanych kolumn (GROUP BY). Używa się jej zwykle w połączeniu z funkcjami agregującymi: - -```php -// Liczy liczbę produktów w każdej kategorii -$table->select('category_id, COUNT(*) AS count') - ->group('category_id'); -``` - - -having(string $having, ...$parameters): static .[method] --------------------------------------------------------- - -Ustawia warunek filtrowania zgrupowanych wierszy (HAVING). Można jej używać w połączeniu z metodą `group()` i funkcjami agregującymi: - -```php -// Znajduje kategorie, które mają więcej niż 100 produktów -$table->select('category_id, COUNT(*) AS count') - ->group('category_id') - ->having('count > ?', 100); -``` - - -Odczyt danych -============= - -Do odczytu danych z bazy dostępnych jest kilka przydatnych metod: - -.[language-php] -| `foreach ($table as $key => $row)` | Przechodzi wszystkie wiersze, `$key` to wartość klucza głównego, `$row` to obiekt ActiveRow | -| `$row = $table->get($key)` | Zwraca jeden wiersz według klucza głównego | -| `$row = $table->fetch()` | Zwraca bieżący wiersz i przesuwa wskaźnik na kolejny | -| `$array = $table->fetchPairs()` | Tworzy z wyników tablicę asocjacyjną | -| `$array = $table->fetchAll()` | Zwraca wszystkie wiersze jako tablicę | -| `count($table)` | Zwraca liczbę wierszy w obiekcie Selection | - -Obiekt [ActiveRow |api:Nette\Database\Table\ActiveRow] jest tylko do odczytu. Oznacza to, że nie możesz zmieniać wartości jego właściwości. To ograniczenie zapewnia spójność danych i zapobiega nieoczekiwanym efektom ubocznym. Dane wczytywane są z bazy, a wszelkie zmiany powinny być wykonywane jawnie i w kontrolowany sposób. - - -`foreach` - iterowanie przez wszystkie wiersze ----------------------------------------------- - -Najprostszym sposobem wykonania zapytania i pobrania wierszy jest iterowanie w pętli `foreach`. Automatycznie wykonuje ona zapytanie SQL. - -```php -$books = $explorer->table('book'); -foreach ($books as $key => $book) { - // $key to wartość klucza głównego, $book to ActiveRow - echo "$book->title ({$book->author->name})"; -} -``` - - -get($key): ?ActiveRow .[method] -------------------------------- - -Wykonuje zapytanie SQL i zwraca wiersz według klucza głównego albo `null`, jeśli nie istnieje. - -```php -$book = $explorer->table('book')->get(123); // zwraca ActiveRow o ID 123 albo null -if ($book) { - echo $book->title; -} -``` - - -fetch(): ?ActiveRow .[method] ------------------------------ - -Zwraca bieżący wiersz i przesuwa wewnętrzny wskaźnik na kolejny. Jeśli nie ma już wierszy, zwraca `null`. - -```php -$books = $explorer->table('book'); -while ($book = $books->fetch()) { - $this->processBook($book); -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Zwraca wyniki jako tablicę asocjacyjną. Pierwszy argument określa nazwę kolumny używanej jako klucz tablicy, drugi argument nazwę kolumny używanej jako wartość: - -```php -$authors = $explorer->table('author')->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Jeśli podany jest tylko pierwszy parametr, wartością będzie cały wiersz, czyli obiekt `ActiveRow`: - -```php -$authors = $explorer->table('author')->fetchPairs('id'); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - -W przypadku zduplikowanych kluczy używana jest wartość z ostatniego wiersza. Przy użyciu `null` jako klucza tablica będzie indeksowana liczbowo od zera (wtedy nie dochodzi do kolizji): - -```php -$authors = $explorer->table('author')->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Alternatywnie możesz jako parametr przekazać callback, który dla każdego wiersza zwróci albo pojedynczą wartość, albo parę klucz-wartość. - -```php -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); -// ['First Book (John Novak)', ...] - -// Callback może zwrócić także tablicę z parą klucz i wartość: -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => [$row->title, $row->author->name]); -// ['First Book' => 'John Novak', ...] -``` - - -fetchAll(): array .[method] ---------------------------- - -Zwraca wszystkie wiersze jako tablicę asocjacyjną obiektów `ActiveRow`, gdzie kluczami są wartości kluczy głównych. - -```php -$allBooks = $explorer->table('book')->fetchAll(); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - - -count(): int .[method] ----------------------- - -Metoda `count()` bez parametru zwraca liczbę wierszy w obiekcie `Selection`: - -```php -$table->where('category', 1); -$count = $table->count(); -$count = count($table); // alternatywa -``` - -Uwaga: `count()` z parametrem wykonuje w bazie danych funkcję agregującą COUNT, patrz niżej. - - -ActiveRow::toArray(): array .[method] -------------------------------------- - -Konwertuje obiekt `ActiveRow` na tablicę asocjacyjną, gdzie kluczami są nazwy kolumn, a wartościami odpowiadające im dane. - -```php -$book = $explorer->table('book')->get(1); -$bookArray = $book->toArray(); -// $bookArray będzie ['id' => 1, 'title' => '...', 'author_id' => ..., ...] -``` - - -Agregacja -========= - -Klasa `Selection` udostępnia metody do łatwego wykonywania funkcji agregujących (COUNT, SUM, MIN, MAX, AVG itd.). - -.[language-php] -| `count($expr)` | Liczy liczbę wierszy | -| `min($expr)` | Zwraca minimalną wartość w kolumnie | -| `max($expr)` | Zwraca maksymalną wartość w kolumnie | -| `sum($expr)` | Zwraca sumę wartości w kolumnie | -| `aggregation($function)` | Pozwala na dowolną funkcję agregującą, jak `AVG()` czy `GROUP_CONCAT()` | - - -count(string $expr): int .[method] ----------------------------------- - -Wykonuje zapytanie SQL z funkcją COUNT i zwraca wynik. Metody używa się do ustalenia, ile wierszy odpowiada danemu warunkowi: - -```php -$count = $table->count('*'); // SELECT COUNT(*) FROM `table` -$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` -``` - -Uwaga: [#count()] bez parametru zwraca tylko liczbę wierszy w obiekcie `Selection`. - - -min(string $expr) i max(string $expr) .[method] ------------------------------------------------ - -Metody `min()` i `max()` zwracają minimalną i maksymalną wartość w podanej kolumnie albo wyrażeniu: - -```php -// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 -$maxPrice = $products->where('active', true) - ->max('price'); -``` - - -sum(string $expr): mixed .[method] ----------------------------------- - -Zwraca sumę wartości w podanej kolumnie albo wyrażeniu: - -```php -// SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 -$totalPrice = $products->where('active', true) - ->sum('price * items_in_stock'); -``` - - -aggregation(string $function, ?string $groupFunction = null): mixed .[method] ------------------------------------------------------------------------------ - -Pozwala wykonać dowolną funkcję agregującą. - -```php -// średnia cena produktów w kategorii -$avgPrice = $products->where('category_id', 1) - ->aggregation('AVG(price)'); - -// łączy tagi produktu w jeden ciąg -$tags = $products->where('id', 1) - ->aggregation('GROUP_CONCAT(tag.name) AS tags') - ->fetch() - ->tags; -``` - -Jeśli potrzebujemy zagregować wyniki, które same są wynikiem jakiejś funkcji agregującej i grupowania (np. `SUM(value)` po zgrupowanych wierszach), jako drugi argument podajemy funkcję agregującą, która ma zostać zastosowana do tych wyników pośrednich: - -```php -// Wylicza łączną cenę produktów w magazynie dla poszczególnych kategorii, a następnie sumuje te ceny razem. -$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') - ->group('category_id') - ->aggregation('SUM(category_total)', 'SUM'); -``` - -W tym przykładzie najpierw wyliczamy łączną cenę produktów w każdej kategorii (`SUM(price * stock) AS category_total`) i grupujemy wyniki według `category_id`. Następnie za pomocą `aggregation('SUM(category_total)', 'SUM')` sumujemy te pośrednie sumy `category_total`. Drugi argument `'SUM'` określa, że do wyników pośrednich ma zostać zastosowana funkcja SUM. - - -Insert, Update i Delete -======================= - -Nette Database Explorer upraszcza wstawianie, aktualizowanie i usuwanie danych. Wszystkie wymienione metody w razie błędu rzucają `Nette\Database\DriverException`. - - -Selection::insert(iterable $data) .[method] -------------------------------------------- - -Wstawia do tabeli nowe rekordy. - -**Wstawienie jednego rekordu:** - -Nowy rekord przekazujemy jako tablicę asocjacyjną albo obiekt iterowalny (jak `ArrayHash` używany w [formularzach |forms:]), gdzie klucze odpowiadają nazwom kolumn w tabeli. - -Jeśli tabela ma zdefiniowany klucz główny, metoda zwraca obiekt `ActiveRow`, który jest ponownie wczytywany z bazy danych, żeby uwzględnić zmiany dokonane na poziomie bazy (triggery, domyślne wartości kolumn, wyliczenie kolumn auto-increment). Zapewnia to spójność danych, a obiekt zawsze zawiera aktualne dane z bazy. Jeśli tabela nie ma klucza głównego, nie ma identyfikowalnego wiersza i metoda zwraca `null`. - -```php -$row = $explorer->table('users')->insert([ - 'name' => 'John Doe', - 'email' => 'john.doe@example.com', -]); -// $row jest instancją ActiveRow i zawiera kompletne dane wstawionego wiersza, -// włącznie z automatycznie wygenerowanym ID i ewentualnymi zmianami dokonanymi przez triggery -echo $row->id; // Wypisuje ID nowo wstawionego użytkownika -echo $row->created_at; // Wypisuje czas utworzenia, jeśli ustawia go trigger -``` - -**Wstawienie wielu rekordów naraz:** - -Metoda `insert()` pozwala wstawić wiele rekordów jednym zapytaniem SQL. W takim przypadku zwraca liczbę wstawionych wierszy. - -```php -$insertedRows = $explorer->table('users')->insert([ - [ - 'name' => 'John', - 'year' => 1994, - ], - [ - 'name' => 'Jack', - 'year' => 1995, - ], -]); -// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) -// $insertedRows będzie 2 -``` - -Jako parametr można przekazać także obiekt `Selection` z wyborem danych. - -```php -$newUsers = $explorer->table('potential_users') - ->where('approved', 1) - ->select('name, email'); - -$insertedRows = $explorer->table('users')->insert($newUsers); -``` - -**Wstawianie wartości specjalnych:** - -Jako wartości możemy przekazać także pliki, obiekty `DateTime` albo literały SQL: - -```php -$explorer->table('users')->insert([ - 'name' => 'John', - 'created_at' => new DateTime, // konwertuje na format bazy danych - 'avatar' => fopen('image.jpg', 'rb'), // wstawia binarną zawartość pliku - 'uuid' => $explorer::literal('UUID()'), // wywołuje funkcję UUID() -]); -``` - - -Selection::update(iterable $data): int .[method] ------------------------------------------------- - -Aktualizuje wiersze w tabeli według podanego filtra. Zwraca liczbę faktycznie zmienionych wierszy. - -Zmieniane kolumny przekazujemy jako tablicę asocjacyjną albo obiekt iterowalny (jak `ArrayHash` używany w [formularzach |forms:]), gdzie klucze odpowiadają nazwom kolumn w tabeli: - -```php -$affected = $explorer->table('users') - ->where('id', 10) - ->update([ - 'name' => 'John Smith', - 'year' => 1994, - ]); -// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 -``` - -Do zmiany wartości liczbowych możesz użyć operatorów `+=` i `-=`: - -```php -$explorer->table('users') - ->where('id', 10) - ->update([ - 'points+=' => 1, // zwiększa wartość kolumny 'points' o 1 - 'coins-=' => 1, // zmniejsza wartość kolumny 'coins' o 1 - ]); -// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 -``` - - -Selection::delete(): int .[method] ----------------------------------- - -Usuwa wiersze z tabeli według podanego filtra. Zwraca liczbę usuniętych wierszy. - -```php -$count = $explorer->table('users') - ->where('id', 10) - ->delete(); -// DELETE FROM `users` WHERE `id` = 10 -``` - -.[caution] -Przy wywoływaniu `update()` albo `delete()` nie zapomnij użyć `where()`, żeby określić wiersze, które mają zostać zmienione albo usunięte. Jeśli `where()` nie zostanie użyte, operacja zostanie wykonana na całej tabeli! - - -ActiveRow::update(iterable $data): bool .[method] -------------------------------------------------- - -Aktualizuje dane w wierszu bazy danych reprezentowanym przez obiekt `ActiveRow`. Przyjmuje iterowalne dane do aktualizacji (klucze to nazwy kolumn). Do zmiany wartości liczbowych możesz użyć operatorów `+=` i `-=`: - -Po wykonaniu aktualizacji `ActiveRow` jest automatycznie ponownie wczytywany z bazy danych, żeby uwzględnić zmiany dokonane na poziomie bazy (np. triggery). Metoda zwraca `true` tylko wtedy, gdy doszło do faktycznej zmiany danych. - -```php -$article = $explorer->table('article')->get(1); -$article->update([ - 'views += 1', // zwiększa liczbę wyświetleń -]); -echo $article->views; // Wypisuje aktualną liczbę wyświetleń -``` - -Ta metoda aktualizuje tylko jeden konkretny wiersz w bazie danych. Do masowej aktualizacji wielu wierszy użyj metody [#Selection::update()]. - - -ActiveRow::delete(): int .[method] ----------------------------------- - -Usuwa z bazy danych wiersz reprezentowany przez obiekt `ActiveRow`. Zwraca liczbę usuniętych wierszy, która powinna wynosić 1. - -```php -$book = $explorer->table('book')->get(1); -$book->delete(); // Usuwa książkę o ID 1 -``` - -Ta metoda usuwa tylko jeden konkretny wiersz w bazie danych. Do masowego usuwania wielu wierszy użyj metody [#Selection::delete()]. - - -Relacje między tabelami -======================= - -W bazach relacyjnych dane podzielone są na wiele tabel i powiązane ze sobą kluczami obcymi. Nette Database Explorer oferuje rewolucyjny sposób pracy z tymi relacjami: bez pisania zapytań JOIN i bez potrzeby czegokolwiek konfigurowania czy generowania. - -Do zilustrowania pracy z relacjami użyjemy przykładowej bazy danych książek ([znajdziesz ją na GitHubie |https://github.com/nette-examples/books]). W bazie mamy tabele: - -- `author` - pisarze i tłumacze (kolumny `id`, `name`, `web`, `born`) -- `book` - książki (kolumny `id`, `author_id`, `translator_id`, `title`, `sequel_id`) -- `tag` - tagi (kolumny `id`, `name`) -- `book_tag` - tabela łącząca książki i tagi (kolumny `book_id`, `tag_id`) - -[* db-schema-1-.webp *] *** Struktura bazy danych używanej w przykładach .<> - -W naszej przykładowej bazie książek znajdziemy kilka typów relacji (choć model jest uproszczony względem rzeczywistości): - -- **Jeden do wielu (1:N)** - każda książka **ma jednego** autora; autor może napisać **wiele** książek. -- **Zero do wielu (0:N)** - książka **może mieć** tłumacza; tłumacz może przetłumaczyć **wiele** książek. -- **Zero do jednego (0:1)** - książka **może mieć** kontynuację. -- **Wiele do wielu (M:N)** - książka **może mieć kilka** tagów, a tag może być przypisany do **kilku** książek. - -W tych relacjach zawsze istnieje **tabela nadrzędna** i **tabela podrzędna**. Na przykład w relacji między autorami a książkami tabela `author` jest nadrzędna, a tabela `book` podrzędna: możesz o tym myśleć tak, że książka zawsze "należy" do autora. Odzwierciedla to również struktura bazy danych: tabela podrzędna `book` zawiera klucz obcy `author_id` odwołujący się do tabeli nadrzędnej `author`. - -Jeśli potrzebujemy wypisać książki wraz z nazwiskami ich autorów, mamy dwie możliwości. Albo pobrać dane jednym zapytaniem SQL z użyciem JOIN: - -```sql -SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; -``` - -Albo pobrać dane w dwóch krokach, najpierw książki, potem ich autorów, a następnie złożyć je w PHP: - -```sql -SELECT * FROM book; -SELECT * FROM author WHERE id IN (1, 2, 3); -- ID autorów z wybranych książek -``` - -Drugie podejście jest w rzeczywistości **efektywniejsze**, choć może to zaskakiwać. Dane pobierane są tylko raz i można je lepiej wykorzystać w cache. Dokładnie tak działa Nette Database Explorer: wszystkim zajmuje się pod maską i oferuje Ci eleganckie API: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo 'title: ' . $book->title; - echo 'written by: ' . $book->author->name; // $book->author to rekord z tabeli 'author' - echo 'translated by: ' . $book->translator?->name; -} -``` - - -Dostęp do tabeli nadrzędnej ---------------------------- - -Dostęp do tabeli nadrzędnej jest prosty. Chodzi o relacje typu *książka ma autora* albo *książka może mieć tłumacza*. Powiązany rekord uzyskujemy przez właściwość obiektu ActiveRow, której nazwa odpowiada nazwie kolumny klucza obcego bez przyrostka `_id`: - -```php -$book = $explorer->table('book')->get(1); -echo $book->author->name; // znajduje autora na podstawie kolumny author_id -echo $book->translator?->name; // znajduje tłumacza na podstawie kolumny translator_id -``` - -Przy dostępie do właściwości `$book->author` Explorer szuka w tabeli `book` kolumny, której nazwa zawiera ciąg `author` (czyli `author_id`). Na podstawie wartości w tej kolumnie wczytuje odpowiadający rekord z tabeli `author` i zwraca go jako `ActiveRow`. Podobnie `$book->translator` używa kolumny `translator_id`. Ponieważ kolumna `translator_id` może zawierać `null`, używamy w kodzie operatora nullsafe `?->`. - -Alternatywne podejście oferuje metoda `ref()`, która przyjmuje dwa argumenty, nazwę tabeli docelowej i nazwę kolumny łączącej, i zwraca instancję `ActiveRow` albo `null`: - -```php -echo $book->ref('author', 'author_id')->name; // relacja do autora -echo $book->ref('author', 'translator_id')->name; // relacja do tłumacza -``` - -Metoda `ref()` przydaje się wtedy, gdy nie można użyć dostępu przez właściwość, na przykład dlatego, że tabela zawiera kolumnę o tej samej nazwie (czyli `author`). W pozostałych przypadkach zalecane jest użycie dostępu przez właściwość ze względu na lepszą czytelność. - -Explorer automatycznie optymalizuje zapytania do bazy danych. Gdy przechodzimy książki w pętli i sięgamy po ich powiązane rekordy (autorów, tłumaczy), Explorer nie generuje zapytania dla każdej książki osobno. Zamiast tego wykonuje tylko **jedno zapytanie SELECT dla każdego typu relacji**, co znacząco zmniejsza obciążenie bazy. Na przykład: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo $book->title . ': '; - echo $book->author->name; - echo $book->translator?->name; -} -``` - -Ten kod wykona tylko te trzy błyskawiczne zapytania do bazy danych: - -```sql -SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- ID z kolumny author_id wybranych książek -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- ID z kolumny translator_id wybranych książek -``` - -.[note] -Logikę szukania kolumny łączącej określa implementacja [Conventions |api:Nette\Database\Conventions]. Zalecamy użycie [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], które analizuje klucze obce i pozwala łatwo pracować z istniejącymi relacjami między tabelami. - - -Dostęp do tabeli podrzędnej ---------------------------- - -Dostęp do tabeli podrzędnej działa w przeciwnym kierunku. Teraz pytamy, *jakie książki napisał ten autor* albo *jakie książki przetłumaczył ten tłumacz*. Do tego typu zapytań służy metoda `related()`, która zwraca `Selection` z powiązanymi rekordami. Spójrzmy na przykład: - -```php -$author = $explorer->table('author')->get(1); - -// Wypisuje wszystkie książki autora -foreach ($author->related('book.author_id') as $book) { - echo "Napisał: $book->title"; -} - -// Wypisuje wszystkie książki przetłumaczone przez autora -foreach ($author->related('book.translator_id') as $book) { - echo "Przetłumaczył: $book->title"; -} -``` - -Metoda `related()` przyjmuje opis połączenia jako jeden argument z notacją kropkową albo jako dwa osobne argumenty: - -```php -$author->related('book.translator_id'); // jeden argument -$author->related('book', 'translator_id'); // dwa argumenty -``` - -Explorer potrafi automatycznie wykryć właściwą kolumnę łączącą na podstawie nazwy tabeli nadrzędnej. W tym przypadku łączy przez kolumnę `book.author_id`, bo nazwa tabeli źródłowej to `author`: - -```php -$author->related('book'); // używa book.author_id -``` - -Jeśli istnieje wiele możliwych połączeń, Explorer rzuci [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -Metody `related()` możemy oczywiście używać przy przechodzeniu wielu rekordów w pętli, a Explorer i w tym przypadku automatycznie zoptymalizuje zapytania: - -```php -$authors = $explorer->table('author'); -foreach ($authors as $author) { - echo $author->name . ' napisał:'; - foreach ($author->related('book') as $book) { - echo $book->title; - } -} -``` - -Ten kod wygeneruje tylko dwa błyskawiczne zapytania SQL: - -```sql -SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- ID wybranych autorów -``` - - -Relacja wiele do wielu ----------------------- - -Dla relacji wiele do wielu (M:N) potrzebna jest **tabela łącząca** (w naszym przypadku `book_tag`) zawierająca dwie kolumny kluczy obcych (`book_id`, `tag_id`). Każda z tych kolumn odwołuje się do klucza głównego jednej z powiązanych tabel. Żeby pobrać powiązane dane, najpierw uzyskujemy rekordy z tabeli łączącej za pomocą `related('book_tag')`, a potem przechodzimy do danych docelowych: - -```php -$book = $explorer->table('book')->get(1); -// wypisuje nazwy tagów przypisanych do książki -foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name; // wypisuje nazwę tagu przez tabelę łączącą -} - -$tag = $explorer->table('tag')->get(1); -// albo odwrotnie: wypisuje nazwy książek oznaczonych tym tagiem -foreach ($tag->related('book_tag') as $bookTag) { - echo $bookTag->book->title; // wypisuje tytuł książki -} -``` - -Explorer znów optymalizuje zapytania SQL do efektywnej postaci: - -```sql -SELECT * FROM `book`; -SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- ID wybranych książek -SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- ID tagów znalezionych w book_tag -``` - - -Zapytania przez powiązane tabele --------------------------------- - -W metodach `where()`, `select()`, `order()` i `group()` możesz używać specjalnych zapisów do dostępu do kolumn z innych tabel. Explorer automatycznie utworzy potrzebne JOIN-y. - -**Notacja kropkowa** (`tabela_nadrzedna.kolumna`) używana jest dla relacji 1:N z perspektywy tabeli podrzędnej: - -```php -$books = $explorer->table('book'); - -// Znajduje książki, których autor ma nazwisko zaczynające się od 'Jon' -$books->where('author.name LIKE ?', 'Jon%'); - -// Sortuje książki według nazwiska autora malejąco -$books->order('author.name DESC'); - -// Wypisuje tytuł książki i nazwisko autora -$books->select('book.title, author.name'); -``` - -**Notacja z dwukropkiem** (`:tabela_podrzedna.kolumna`) używana jest dla relacji 1:N z perspektywy tabeli nadrzędnej: - -```php -$authors = $explorer->table('author'); - -// Znajduje autorów, którzy napisali książkę z 'PHP' w tytule -$authors->where(':book.title LIKE ?', '%PHP%'); - -// Liczy liczbę książek każdego autora -$authors->select('*, COUNT(:book.id) AS book_count') - ->group('author.id'); -``` - -W powyższym przykładzie z notacją z dwukropkiem (`:book.title`) nie jest podana kolumna klucza obcego. Explorer automatycznie wykrywa właściwą kolumnę na podstawie nazwy tabeli nadrzędnej. W tym przypadku łączy przez kolumnę `book.author_id`, bo nazwa tabeli źródłowej to `author`. Jeśli istnieje wiele możliwych połączeń, Explorer rzuci [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -Kolumnę łączącą można jawnie podać w nawiasach: - -```php -// Znajduje autorów, którzy przetłumaczyli książkę z 'PHP' w tytule -$authors->where(':book(translator_id).title LIKE ?', '%PHP%'); -``` - -Zapisy można łączyć w łańcuch, żeby sięgać po dane w wielu tabelach: - -```php -// Znajduje autorów książek oznaczonych tagiem 'PHP' -$authors->where(':book:book_tag.tag.name', 'PHP') - ->group('author.id'); -``` - - -Rozszerzanie warunków dla JOIN ------------------------------- - -Metoda `joinWhere()` rozszerza warunki podawane przy łączeniu tabel w SQL po słowie kluczowym `ON`. - -Powiedzmy, że chcemy znaleźć książki przetłumaczone przez konkretnego tłumacza: - -```php -// Znajduje książki przetłumaczone przez tłumacza o imieniu 'David' -$books = $explorer->table('book') - ->joinWhere('translator', 'translator.name', 'David'); -// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') -``` - -W warunku `joinWhere()` możesz używać tych samych konstrukcji co w metodzie `where()`: operatorów, zastępników, tablic wartości czy wyrażeń SQL. - -Dla bardziej złożonych zapytań z wieloma JOIN-ami możesz zdefiniować aliasy tabel: - -```php -$tags = $explorer->table('tag') - ->joinWhere(':book_tag.book.author', 'book_author.born < ?', 1950) - ->alias(':book_tag.book.author', 'book_author'); -// LEFT JOIN `book_tag` ON `tag`.`id` = `book_tag`.`tag_id` -// LEFT JOIN `book` ON `book_tag`.`book_id` = `book`.`id` -// LEFT JOIN `author` `book_author` ON `book`.`author_id` = `book_author`.`id` -// AND (`book_author`.`born` < 1950) -``` - -Zwróć uwagę, że podczas gdy metoda `where()` dodaje warunki do klauzuli `WHERE`, metoda `joinWhere()` rozszerza warunki w klauzuli `ON` przy łączeniu tabel. diff --git a/database/pl/guide.texy b/database/pl/guide.texy deleted file mode 100644 index 237f8469a0..0000000000 --- a/database/pl/guide.texy +++ /dev/null @@ -1,218 +0,0 @@ -Nette Database -************** - -.[perex] -Nette Database to potężna i elegancka warstwa bazodanowa dla PHP, skupiona na prostocie i sprytnych funkcjach. Oferuje dwa sposoby pracy z bazą danych: [Explorer |explorer] do szybkiego tworzenia aplikacji albo [podejście SQL |SQL way] do bezpośredniej pracy z zapytaniami. - -<div class="grid gap-3"> -<div> - - -[Podejście SQL|sql-way] -======================= -- Bezpieczne, parametryzowane zapytania -- Precyzyjna kontrola nad strukturą zapytania SQL -- Gdy piszesz złożone zapytania z zaawansowanymi funkcjami -- Optymalizacja wydajności za pomocą konkretnych funkcji SQL - -</div> - -<div> - - -[Explorer |explorer] -==================== -- Szybkie tworzenie bez pisania SQL -- Intuicyjna praca z relacjami między tabelami -- Korzyść z automatycznej optymalizacji zapytań -- Odpowiedni do szybkiej i wygodnej pracy z bazą danych - -</div> - -</div> - - -Instalacja -========== - -Pobierz i zainstaluj bibliotekę za pomocą [Composera|best-practices:composer]: - -```shell -composer require nette/database -``` - - -Obsługiwane bazy danych -======================= - -Nette Database obsługuje następujące bazy danych: - -|* Serwer bazy danych |* Nazwa DSN |* Wsparcie Explorer -|-----------------------|--------------|-----------------------| -| MySQL (>= 5.1) | mysql | TAK | -| PostgreSQL (>= 9.0) | pgsql | TAK | -| SQLite 3 (>= 3.8) | sqlite | TAK | -| Oracle | oci | NIE | -| MS SQL (PDO_SQLSRV) | sqlsrv | TAK | -| MS SQL (PDO_DBLIB) | mssql | NIE | -| ODBC | odbc | NIE | - - -Dwa podejścia do pracy z bazą danych -==================================== - -Nette Database daje Ci wybór: możesz albo pisać zapytania SQL bezpośrednio (podejście SQL), albo pozwolić je generować automatycznie (Explorer). Zobaczmy, jak oba podejścia radzą sobie z tymi samymi zadaniami: - -[Podejście SQL|sql-way] - zapytania SQL - -```php -// Wstawienie rekordu -$database->query('INSERT INTO books', [ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// Pobranie rekordów: autorzy książek -$result = $database->query(' - SELECT authors.*, COUNT(books.id) AS books_count - FROM authors - LEFT JOIN books ON authors.id = books.author_id - WHERE authors.active = 1 - GROUP BY authors.id -'); - -// Wypisanie (nieoptymalne, generuje N dodatkowych zapytań) -foreach ($result as $author) { - $books = $database->query(' - SELECT * FROM books - WHERE author_id = ? - ORDER BY published_at DESC - ', $author->id); - - echo "Autor $author->name napisał $author->books_count książek:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -[Podejście Explorer|explorer] - automatyczne generowanie SQL - -```php -// Wstawienie rekordu -$database->table('books')->insert([ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// Pobranie rekordów: autorzy książek -$authors = $database->table('authors') - ->where('active', 1); - -// Wypisanie (automatycznie generuje tylko 2 zoptymalizowane zapytania) -foreach ($authors as $author) { - $books = $author->related('books') - ->order('published_at DESC'); - - echo "Autor $author->name napisał {$books->count()} książek:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -Podejście Explorer generuje i optymalizuje zapytania SQL automatycznie. W powyższym przykładzie podejście SQL generuje N+1 zapytań (jedno na autorów, a potem po jednym na książki każdego autora), podczas gdy Explorer automatycznie optymalizuje zapytania i wykonuje tylko dwa: jedno na autorów i jedno na wszystkie ich książki. - -Oba podejścia można w aplikacji swobodnie łączyć według potrzeb. - - -Połączenie i konfiguracja -========================= - -Żeby połączyć się z bazą danych, wystarczy utworzyć instancję klasy [api:Nette\Database\Connection]: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password); -``` - -Parametr `$dsn` (Data Source Name) jest taki sam, jak [używany przez PDO |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], np. `host=127.0.0.1;dbname=test`. W razie niepowodzenia rzuca `Nette\Database\ConnectionException`. - -Wygodniejszą metodę oferuje jednak [konfiguracja aplikacji |configuration], gdzie wystarczy dodać sekcję `database`. Utworzy to potrzebne obiekty, a także panel bazy danych w pasku [Tracy |tracy:]. - -```neon -database: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password -``` - -Obiekt połączenia można potem [uzyskać jako usługę z kontenera DI |dependency-injection:passing-dependencies], np.: - -```php -class Model -{ - public function __construct( - // albo Nette\Database\Explorer - private Nette\Database\Connection $database, - ) { - } -} -``` - -Więcej informacji o [konfiguracji bazy danych|configuration]. - - -Ręczne utworzenie Explorera ---------------------------- - -Jeśli nie używasz kontenera DI Nette, możesz utworzyć instancję `Nette\Database\Explorer` ręcznie: - -```php -// połączenie z bazą danych -$connection = new Nette\Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); -// magazyn cache, implementuje Nette\Caching\Storage, np.: -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); -// zajmuje się refleksją struktury bazy danych -$structure = new Nette\Database\Structure($connection, $storage); -// definiuje reguły mapowania nazw tabel, kolumn i kluczy obcych -$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); -$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage); -``` - - -Zarządzanie połączeniem -======================= - -Przy utworzeniu obiektu `Connection` połączenie nawiązywane jest automatycznie. Jeśli chcesz połączenie opóźnić, użyj trybu lazy: włączysz go w [konfiguracji|configuration] ustawieniem `lazy` albo tak: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => true]); -``` - -Do zarządzania połączeniem służą metody `connect()`, `disconnect()` i `reconnect()`. -- `connect()` tworzy połączenie, jeśli jeszcze nie istnieje, i może rzucić `Nette\Database\ConnectionException`. -- `disconnect()` rozłącza bieżące połączenie z bazą danych. -- `reconnect()` wykonuje rozłączenie i ponowne połączenie z bazą danych. Ta metoda również może rzucić `Nette\Database\ConnectionException`. - -Poza tym możesz monitorować zdarzenia związane z połączeniem za pomocą zdarzenia `onConnect`, które jest tablicą callbacków wywoływanych po nawiązaniu połączenia z bazą danych. - -```php -// wykonuje się po połączeniu z bazą danych -$database->onConnect[] = function($database) { - echo "Połączono z bazą danych"; -}; -``` - -Podobnie działa zdarzenie `onQuery`: jest to tablica callbacków wywoływanych po każdym wykonanym zapytaniu (a także wtedy, gdy zapytanie się nie powiedzie), przydatna do logowania albo profilowania. - - -Tracy Debug Bar -=============== - -Jeśli używasz [Tracy |tracy:], panel Database w Debug Barze aktywuje się automatycznie. Wyświetla wszystkie wykonane zapytania, ich parametry, czas wykonania i miejsce w kodzie, z którego zostały wywołane. - -[* db-panel.webp *] diff --git a/database/pl/reflection.texy b/database/pl/reflection.texy deleted file mode 100644 index cd295b4f26..0000000000 --- a/database/pl/reflection.texy +++ /dev/null @@ -1,127 +0,0 @@ -Refleksja struktury -******************* - -.{data-version:3.2.1} -Nette Database udostępnia narzędzia do introspekcji struktury bazy danych za pomocą klasy [api:Nette\Database\Reflection]. Pozwala ona uzyskać informacje o tabelach, kolumnach, indeksach i kluczach obcych. Refleksji możesz użyć do generowania schematów, tworzenia elastycznych aplikacji pracujących z bazą danych albo ogólnych narzędzi bazodanowych. - -Obiekt refleksji uzyskujemy z instancji połączenia z bazą danych: - -```php -$reflection = $database->getReflection(); -``` - - -Pobieranie tabel ----------------- - -Właściwość readonly `$reflection->tables` zawiera tablicę asocjacyjną wszystkich tabel w bazie danych: - -```php -// Wypisanie nazw wszystkich tabel -foreach ($reflection->tables as $name => $table) { - echo $name . "\n"; -} -``` - -Dostępne są jeszcze dwie metody: - -```php -// Sprawdzenie istnienia tabeli -if ($reflection->hasTable('users')) { - echo "Tabela users istnieje"; -} - -// Zwraca obiekt tabeli; jeśli nie istnieje, rzuca wyjątek -$table = $reflection->getTable('users'); -``` - - -Informacje o tabeli -------------------- - -Tabelę reprezentuje obiekt [Table|api:Nette\Database\Reflection\Table], który udostępnia następujące właściwości readonly: - -- `$name: string` - nazwa tabeli -- `$view: bool` - czy jest to widok -- `$fullName: ?string` - pełna nazwa tabeli wraz ze schematem (jeśli istnieje) -- `$columns: array<string, Column>` - tablica asocjacyjna kolumn tabeli -- `$indexes: Index[]` - tablica indeksów tabeli -- `$primaryKey: ?Index` - klucz główny tabeli albo null -- `$foreignKeys: ForeignKey[]` - tablica kluczy obcych tabeli -- `$comment: ?string` - komentarz tabeli - - -Kolumny -------- - -Właściwość `columns` tabeli udostępnia tablicę asocjacyjną kolumn, gdzie kluczem jest nazwa kolumny, a wartością instancja [Column|api:Nette\Database\Reflection\Column] z tymi właściwościami: - -- `$name: string` - nazwa kolumny -- `$table: ?Table` - odwołanie do tabeli kolumny -- `$nativeType: string` - natywny typ bazy danych -- `$size: ?int` - rozmiar/długość typu -- `$nullable: bool` - czy kolumna może zawierać NULL -- `$default: mixed` - wartość domyślna kolumny -- `$autoIncrement: bool` - czy kolumna jest auto-increment -- `$primary: bool` - czy jest częścią klucza głównego -- `$vendor: array` - dodatkowe metadane specyficzne dla danego systemu bazodanowego -- `$comment: ?string` - komentarz kolumny - -```php -foreach ($table->columns as $name => $column) { - echo "Kolumna: $name\n"; - echo "Typ: {$column->nativeType}\n"; - echo "Nullable: " . ($column->nullable ? 'Tak' : 'Nie') . "\n"; -} -``` - - -Indeksy -------- - -Właściwość `indexes` tabeli udostępnia tablicę indeksów, gdzie każdy indeks jest instancją [Index|api:Nette\Database\Reflection\Index] z tymi właściwościami: - -- `$columns: Column[]` - tablica kolumn tworzących indeks -- `$unique: bool` - czy indeks jest unikalny -- `$primary: bool` - czy jest kluczem głównym -- `$name: ?string` - nazwa indeksu - -Klucz główny tabeli można uzyskać właściwością `primaryKey`, która zwraca albo obiekt `Index`, albo `null`, jeśli tabela nie ma klucza głównego. - -```php -// Wypisanie indeksów -foreach ($table->indexes as $index) { - $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); - echo "Indeks" . ($index->name ? " {$index->name}" : '') . ":\n"; - echo " Kolumny: $columns\n"; - echo " Unikalny: " . ($index->unique ? 'Tak' : 'Nie') . "\n"; -} - -// Wypisanie klucza głównego -if ($primaryKey = $table->primaryKey) { - $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); - echo "Klucz główny: $columns\n"; -} -``` - - -Klucze obce ------------ - -Właściwość `foreignKeys` tabeli udostępnia tablicę kluczy obcych, gdzie każdy klucz obcy jest instancją [ForeignKey|api:Nette\Database\Reflection\ForeignKey] z tymi właściwościami: - -- `$foreignTable: Table` - tabela, do której się odwołuje -- `$localColumns: Column[]` - tablica kolumn lokalnych -- `$foreignColumns: Column[]` - tablica kolumn, do których się odwołuje -- `$name: string` - nazwa klucza obcego - -```php -// Wypisanie kluczy obcych -foreach ($table->foreignKeys as $fk) { - $localCols = implode(', ', array_map(fn($col) => $col->name, $fk->localColumns)); - $foreignCols = implode(', ', array_map(fn($col) => $col->name, $fk->foreignColumns)); - - echo "FK" . ($fk->name ? " {$fk->name}" : '') . ":\n"; - echo " $localCols -> {$fk->foreignTable->name}($foreignCols)\n"; -} -``` diff --git a/database/pl/security.texy b/database/pl/security.texy deleted file mode 100644 index 8b2a4e9956..0000000000 --- a/database/pl/security.texy +++ /dev/null @@ -1,185 +0,0 @@ -Zagrożenia bezpieczeństwa -************************* - -<div class=perex> - -Bazy danych często zawierają wrażliwe dane i pozwalają wykonywać niebezpieczne operacje. Dla bezpiecznej pracy z Nette Database kluczowe jest: - -- Zrozumieć różnicę między bezpiecznym a niebezpiecznym API -- Używać zapytań parametryzowanych -- Poprawnie walidować dane wejściowe - -</div> - - -Czym jest SQL injection? -======================== - -SQL injection to najpoważniejsze zagrożenie bezpieczeństwa przy pracy z bazami danych. Powstaje, gdy nieoczyszczone dane wejściowe od użytkownika stają się częścią zapytania SQL. Atakujący może wstawić własne polecenia SQL i tym samym: -- Uzyskać nieautoryzowany dostęp do danych -- Zmodyfikować albo usunąć dane w bazie -- Obejść uwierzytelnianie - -```php -// ❌ NIEBEZPIECZNY KOD - podatny na SQL injection -$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); - -// Atakujący może wpisać wartość taką jak: ' OR '1'='1 -// Wynikowe zapytanie brzmiałoby: SELECT * FROM users WHERE name = '' OR '1'='1' -// Co zwróci wszystkich użytkowników -``` - -To samo dotyczy Database Explorera: - -```php -// ❌ NIEBEZPIECZNY KOD - podatny na SQL injection -$table->where('name = ' . $_GET['name']); -$table->where("name = '$_GET[name]'"); -``` - - -Zapytania parametryzowane -========================= - -Podstawową obroną przed SQL injection są zapytania parametryzowane. Nette Database oferuje kilka sposobów ich użycia. - -Najprostszy sposób to użycie **zastępników w postaci znaku zapytania**: - -```php -// ✅ Bezpieczne zapytanie parametryzowane -$database->query('SELECT * FROM users WHERE name = ?', $name); - -// ✅ Bezpieczny warunek w Explorerze -$table->where('name = ?', $name); -``` - -Dotyczy to wszystkich pozostałych metod w [Database Explorerze|explorer], które pozwalają wstawiać wyrażenia z zastępnikami i parametrami. - -Dla poleceń INSERT, UPDATE albo klauzuli WHERE możemy przekazać wartości w tablicy: - -```php -// ✅ Bezpieczny INSERT -$database->query('INSERT INTO users', [ - 'name' => $name, - 'email' => $email, -]); - -// ✅ Bezpieczny INSERT w Explorerze -$table->insert([ - 'name' => $name, - 'email' => $email, -]); -``` - - -Walidacja wartości parametrów -============================= - -Zapytania parametryzowane są kamieniem węgielnym bezpiecznej pracy z bazą danych. Wartości, które do nich wstawiamy, muszą jednak przejść kilka poziomów kontroli: - - -Kontrola typów --------------- - -**Najważniejsze jest zapewnienie poprawnego typu danych parametrów** - to warunek konieczny bezpiecznego użycia Nette Database. Baza danych zakłada, że wszystkie dane wejściowe mają poprawny typ danych odpowiadający danej kolumnie. - -Gdyby na przykład `$name` z poprzednich przykładów było nieoczekiwanie tablicą zamiast ciągiem, Nette Database spróbowałoby wstawić do zapytania SQL wszystkie jej elementy, co doprowadziłoby do błędu. Dlatego **nigdy nie używaj** niezwalidowanych danych z `$_GET`, `$_POST` czy `$_COOKIE` bezpośrednio w zapytaniach do bazy danych. - - -Walidacja formatu ------------------ - -Na drugim poziomie sprawdzamy format danych, na przykład czy ciągi są w kodowaniu UTF-8 i czy ich długość odpowiada definicji kolumny albo czy wartości liczbowe mieszczą się w dozwolonym zakresie typu danych danej kolumny. - -Na tym poziomie walidacji możemy częściowo polegać na samej bazie danych: wiele baz odrzuci nieprawidłowe dane. Zachowanie może się jednak różnić, niektóre mogą po cichu przyciąć długie ciągi albo obciąć liczby spoza zakresu. - - -Walidacja specyficzna dla domeny --------------------------------- - -Trzeci poziom to logiczne kontrole specyficzne dla Twojej aplikacji. Na przykład sprawdzenie, czy wartości z selectboxów odpowiadają oferowanym opcjom, czy liczby mieszczą się w oczekiwanym zakresie (np. wiek 0-150 lat) albo czy wzajemne zależności między wartościami mają sens. - - -Zalecane sposoby walidacji --------------------------- - -- Używaj [Nette Forms|forms:], które automatycznie zapewniają właściwą walidację wszystkich wejść. -- Używaj [presenterów|application:] i podawaj typy danych parametrów w metodach `action*()` i `render*()`. -- Albo zaimplementuj własną warstwę walidacyjną za pomocą standardowych narzędzi PHP, jak `filter_var()`. - - -Bezpieczna praca z kolumnami -============================ - -W poprzedniej sekcji pokazaliśmy, jak poprawnie walidować wartości parametrów. Przy używaniu tablic w zapytaniach SQL musimy jednak poświęcić taką samą uwagę ich kluczom. - -```php -// ❌ NIEBEZPIECZNY KOD - klucze w tablicy nie są oczyszczone -$database->query('INSERT INTO users', $_POST); -``` - -Przy poleceniach INSERT i UPDATE jest to krytyczna luka bezpieczeństwa: atakujący może wstawić albo zmodyfikować dowolną kolumnę w bazie danych. Mógłby na przykład ustawić `is_admin = 1` albo wstawić dowolne dane do wrażliwych kolumn (tak zwana Mass Assignment Vulnerability). - -W warunkach WHERE jest to jeszcze niebezpieczniejsze, bo mogą one zawierać operatory: - -```php -// ❌ NIEBEZPIECZNY KOD - klucze w tablicy nie są oczyszczone -$_POST['salary >'] = 100000; -$database->query('SELECT * FROM users WHERE', $_POST); -// wykona zapytanie WHERE (`salary` > 100000) -``` - -Atakujący może w ten sposób systematycznie odkrywać pensje pracowników. Może zacząć na przykład od zapytania o pensje powyżej 100 000, potem poniżej 50 000, a stopniowo zawężając zakres, może ujawnić przybliżone pensje wszystkich pracowników. Ten typ ataku nazywa się SQL enumeration. - -Metody `where()` i `whereOr()` są nawet [znacznie bardziej elastyczne |explorer#where()] i wspierają w kluczach oraz wartościach wyrażenia SQL wraz z operatorami i funkcjami. Daje to atakującemu możliwość przeprowadzenia SQL injection: - -```php -// ❌ NIEBEZPIECZNY KOD - atakujący może wstawić własny SQL -$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1']; -$table->where($_POST); -// wykona zapytanie WHERE (0) UNION SELECT name, salary FROM users WHERE (1) -``` - -Ten atak kończy pierwotny warunek za pomocą `0)`, dokleja przez `UNION` własny `SELECT`, żeby uzyskać wrażliwe dane z tabeli `users`, i zamyka składniowo poprawne zapytanie za pomocą `WHERE (1)`. - - -Whitelista kolumn ------------------ - -Do bezpiecznej pracy z nazwami kolumn potrzebujemy mechanizmu, który zapewni, że użytkownik może pracować tylko z dozwolonymi kolumnami i nie może dodać własnych. Moglibyśmy spróbować wykrywać i blokować niebezpieczne nazwy kolumn (blacklista), ale to podejście jest zawodne: atakujący zawsze może wymyślić nowy sposób zapisania niebezpiecznej nazwy kolumny, którego nie przewidzieliśmy. - -Dlatego znacznie bezpieczniej jest odwrócić logikę i zdefiniować jawną listę dozwolonych kolumn (whitelistę): - -```php -// Kolumny, które użytkownik może modyfikować -$allowedColumns = ['name', 'email', 'active']; - -// Usuwamy z wejścia wszystkie nieuprawnione kolumny -$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); - -// ✅ Teraz bezpiecznie użyjemy w zapytaniach, na przykład: -$database->query('INSERT INTO users', $filteredData); -$table->update($filteredData); -$table->where($filteredData); -``` - - -Dynamiczne identyfikatory -========================= - -Dla dynamicznych nazw tabel i kolumn używaj zastępnika `?name`. Zapewnia on poprawne escapowanie identyfikatorów zgodnie ze składnią danej bazy danych (np. za pomocą backticków w MySQL): - -```php -// ✅ Bezpieczne użycie zaufanych identyfikatorów -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name', $column, $table); -// Wynik w MySQL: SELECT `name` FROM `users` -``` - -Ważne: symbolu `?name` używaj wyłącznie dla zaufanych wartości zdefiniowanych w kodzie aplikacji. Dla wartości od użytkownika użyj ponownie [whitelisty |#Whitelista kolumn]. W przeciwnym razie narażasz się na zagrożenia bezpieczeństwa: - -```php -// ❌ NIEBEZPIECZNE - nigdy nie używaj wejścia od użytkownika -$database->query('SELECT ?name FROM users', $_GET['column']); -``` diff --git a/database/pl/sql-way.texy b/database/pl/sql-way.texy deleted file mode 100644 index 077bfb5094..0000000000 --- a/database/pl/sql-way.texy +++ /dev/null @@ -1,513 +0,0 @@ -Podejście SQL -************* - -.[perex] -Nette Database oferuje dwa sposoby pracy: zapytania SQL możesz pisać sam (podejście SQL) albo pozwolić je generować automatycznie (patrz [Explorer |explorer]). Podejście SQL daje Ci pełną kontrolę nad zapytaniami i jednocześnie zapewnia, że będą składane bezpiecznie. - -.[note] -Szczegóły dotyczące połączenia i konfiguracji bazy danych znajdziesz w rozdziale [Połączenie i konfiguracja |guide#Połączenie i konfiguracja]. - - -Podstawowe zapytania -==================== - -Do odpytywania bazy danych służy metoda `query()`. Zwraca obiekt [ResultSet |api:Nette\Database\ResultSet], który reprezentuje wynik zapytania. Jeśli zapytanie się nie powiedzie, metoda [rzuca wyjątek|exceptions]. Wynik zapytania możesz przejść pętlą `foreach` albo użyć jednej z [metod pomocniczych |#Pobieranie danych]. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; -} -``` - -Do bezpiecznego wstawiania wartości do zapytań SQL służą zapytania parametryzowane. Nette Database czyni to niezwykle prostym: wystarczy po zapytaniu SQL dodać przecinek i wartość: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Przy większej liczbie parametrów masz dwie możliwości. Możesz albo przeplatać zapytanie SQL parametrami: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age); -``` - -Albo najpierw napisać całe zapytanie SQL, a potem dołączyć wszystkie parametry: - -```php -$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); -``` - - -Ochrona przed SQL injection -=========================== - -Dlaczego ważne jest używanie zapytań parametryzowanych? Bo chronią Cię przed atakiem zwanym SQL injection, w którym atakujący mógłby wstrzyknąć własne polecenia SQL i tym samym uzyskać dostęp do danych w bazie albo je uszkodzić. - -.[warning] -**Nigdy nie wstawiaj zmiennych bezpośrednio do zapytania SQL!** Zawsze używaj zapytań parametryzowanych, które chronią Cię przed SQL injection. - -```php -// ❌ NIEBEZPIECZNY KOD - podatny na SQL injection -$database->query("SELECT * FROM users WHERE name = '$name'"); - -// ✅ Bezpieczne zapytanie parametryzowane -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Zapoznaj się z [potencjalnymi zagrożeniami bezpieczeństwa |security]. - - -Techniki zapytań -================ - - -Warunki WHERE -------------- - -Warunki `WHERE` możesz zapisać jako tablicę asocjacyjną, gdzie kluczami są nazwy kolumn, a wartościami dane do porównania. Nette Database automatycznie wybiera najodpowiedniejszy operator SQL na podstawie typu wartości. - -```php -$database->query('SELECT * FROM users WHERE', [ - 'name' => 'John', - 'active' => true, -]); -// WHERE `name` = 'John' AND `active` = 1 -``` - -Operator porównania możesz też podać jawnie w kluczu: - -```php -$database->query('SELECT * FROM users WHERE', [ - 'age >' => 25, // użyje operatora > - 'name LIKE' => '%John%', // użyje operatora LIKE - 'email NOT LIKE' => '%example.com%', // użyje operatora NOT LIKE -]); -// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' -``` - -Nette automatycznie obsługuje przypadki szczególne, jak wartości `null` czy tablice. - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name' => 'Laptop', // użyje operatora = - 'category_id' => [1, 2, 3], // użyje IN - 'description' => null, // użyje IS NULL -]); -// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL -``` - -Dla warunków negatywnych użyj operatora `NOT`: - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // użyje operatora != - 'category_id NOT' => [1, 2, 3], // użyje NOT IN - 'description NOT' => null, // użyje IS NOT NULL - 'id NOT' => [], // pominięte -]); -// WHERE `name` != 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL -``` - -Domyślnie warunki łączone są operatorem `AND`. Można to zmienić za pomocą [zastępnika ?or |#Wskazówki składania SQL]. - - -Reguły ORDER BY ---------------- - -Klauzulę `ORDER BY` można zapisać za pomocą tablicy. W kluczach podaj kolumny, a wartością logiczną wskaż kolejność rosnącą (`true`) albo malejącą (`false`): - -```php -$database->query('SELECT id FROM author ORDER BY', [ - 'id' => true, // rosnąco - 'name' => false, // malejąco -]); -// SELECT id FROM author ORDER BY `id`, `name` DESC -``` - - -Wstawianie danych (INSERT) --------------------------- - -Do wstawiania rekordów służy polecenie SQL `INSERT`. - -```php -$values = [ - 'name' => 'John Doe', - 'email' => 'john@example.com', -]; -$database->query('INSERT INTO users ?', $values); -$userId = $database->getInsertId(); -``` - -Metoda `getInsertId()` zwraca ID ostatnio wstawionego wiersza. Dla niektórych baz danych (np. PostgreSQL) trzeba podać jako parametr nazwę sekwencji, z której ma zostać wygenerowane ID, za pomocą `$database->getInsertId($sequenceId)`. - -Jako parametry możesz przekazać także [#Wartości specjalne], jak pliki, obiekty DateTime czy typy enum. - -Wstawianie wielu rekordów naraz: - -```php -$database->query('INSERT INTO users ?', [ - ['name' => 'User 1', 'email' => 'user1@mail.com'], - ['name' => 'User 2', 'email' => 'user2@mail.com'], -]); -``` - -INSERT wielu rekordów jest znacznie szybszy, bo wykonywane jest tylko jedno zapytanie do bazy zamiast wielu pojedynczych. - -**Uwaga dotycząca bezpieczeństwa:** nigdy nie używaj jako `$values` niezwalidowanych danych. Zapoznaj się z [możliwymi zagrożeniami |security#Bezpieczna praca z kolumnami]. - - -Aktualizacja danych (UPDATE) ----------------------------- - -Do aktualizacji rekordów służy polecenie SQL `UPDATE`. - -```php -// Aktualizacja jednego rekordu -$values = [ - 'name' => 'John Smith', -]; -$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); -``` - -Liczbę dotkniętych wierszy zwraca `$result->getRowCount()`. - -Przy `UPDATE` możemy użyć operatorów `+=` i `-=`: - -```php -$database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // zwiększa login_count -], 1); -``` - -Przykład wstawienia albo aktualizacji rekordu, jeśli już istnieje. Użyjemy techniki `ON DUPLICATE KEY UPDATE`: - -```php -$values = [ - 'name' => $name, - 'year' => $year, -]; -$database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', - $values + ['id' => $id], - $values, -); -// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978) -// ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 -``` - -Zauważ, że Nette Database rozpoznaje kontekst, w którym w poleceniu SQL użyty jest parametr tablicowy, i odpowiednio składa kod SQL. Z pierwszej tablicy złożyło więc `(id, name, year) VALUES (123, 'Jim', 1978)`, podczas gdy drugą przekształciło w postać `name = 'Jim', year = 1978`. Omawiamy to szczegółowo w sekcji [#Wskazówki składania SQL]. - - -Usuwanie danych (DELETE) ------------------------- - -Do usuwania rekordów służy polecenie SQL `DELETE`. Przykład uzyskania liczby usuniętych wierszy: - -```php -$count = $database->query('DELETE FROM users WHERE id = ?', 1) - ->getRowCount(); -``` - - -Wskazówki składania SQL ------------------------ - -Wskazówka to specjalny zastępnik w zapytaniu SQL, który określa, jak wartość parametru ma zostać przekształcona w wyrażenie SQL: - -| Wskazówka | Opis | Automatycznie używana dla -|-----------|-------------------------------------------------|----------------------------- -| `?name` | Służy do wstawiania nazw tabel albo kolumn | - -| `?values` | Generuje `(klucz, ...) VALUES (wartość, ...)` | `INSERT ... ?`, `REPLACE ... ?` -| `?set` | Generuje przypisania `klucz = wartość, ...` | `SET ?`, `KEY UPDATE ?` -| `?and` | Łączy warunki w tablicy operatorem `AND` | `WHERE ?`, `HAVING ?` -| `?or` | Łączy warunki w tablicy operatorem `OR` | - -| `?order` | Generuje klauzulę `ORDER BY` | `ORDER BY ?`, `GROUP BY ?` - -Zastępnik `?name` służy do dynamicznego wstawiania do zapytania nazw tabel i kolumn. Nette Database dba o poprawne cytowanie identyfikatorów zgodnie z konwencjami bazy danych (np. otoczenie backtickami w MySQL). - -```php -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); -// SELECT `name` FROM `users` WHERE id = 1 (w MySQL) -``` - -**Uwaga:** zastępnika `?name` używaj wyłącznie dla zwalidowanych nazw tabel i kolumn. W przeciwnym razie ryzykujesz [luki bezpieczeństwa |security#Dynamiczne identyfikatory]. - -Pozostałych wskazówek zwykle nie trzeba podawać, bo Nette przy składaniu zapytania SQL używa sprytnej autodetekcji (patrz trzecia kolumna tabeli). Możesz jednak ich użyć na przykład w sytuacji, gdy chcesz połączyć warunki operatorem `OR` zamiast `AND`: - -```php -$database->query('SELECT * FROM users WHERE ?or', [ - 'name' => 'John', - 'email' => 'john@example.com', -]); -// SELECT * FROM users WHERE `name` = 'John' OR `email` = 'john@example.com' -``` - - -Wartości specjalne ------------------- - -Oprócz zwykłych typów skalarnych (string, int, bool) możesz jako parametry przekazać także wartości specjalne: - -- pliki: `fopen('image.gif', 'r')` wstawia binarną zawartość pliku -- data i czas: obiekty `DateTimeInterface` konwertowane są na format bazy danych -- typy enum: instancje `enum` konwertowane są na swoją wartość -- literały SQL: tworzone przez `Connection::literal('NOW()')` wstawiane są do zapytania bezpośrednio - -```php -$database->query('INSERT INTO articles ?', [ - 'title' => 'My Article', - 'published_at' => new DateTimeImmutable, // albo new DateTime - 'content' => fopen('image.png', 'r'), - 'state' => Status::Draft, -]); -``` - -Dla baz danych, które nie mają natywnego wsparcia dla typu danych `datetime` (jak SQLite i Oracle), obiekty `DateTime` i `DateTimeImmutable` konwertowane są na wartość określoną w [konfiguracji bazy danych|configuration] pozycją `formatDateTime` (wartość domyślna to `U`, czyli uniksowy timestamp). - - -Literały SQL ------------- - -W niektórych przypadkach potrzebujesz przekazać jako wartość surowy kod SQL, który nie ma być traktowany jak ciąg i escapowany. Służą do tego obiekty klasy `Nette\Database\SqlLiteral`. Tworzy je metoda `Connection::literal()`. - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - 'year >' => $database::literal('YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) -``` - -Alternatywnie: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) -``` - -Literały SQL mogą zawierać parametry: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > ? AND year < ?', $min, $max), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) -``` - -Pozwala to na ciekawe kombinacje: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('?or', [ - 'active' => true, - 'role' => $role, - ]), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (`active` = 1 OR `role` = 'admin') -``` - - -Pobieranie danych -================= - - -Skróty dla zapytań SELECT -------------------------- - -Dla uproszczenia pobierania danych `Connection` oferuje kilka skrótów łączących wywołanie `query()` z późniejszym wywołaniem `fetch*()`. Metody te przyjmują te same parametry co `query()`, czyli zapytanie SQL i opcjonalne parametry. Pełny opis metod `fetch*()` znajdziesz [niżej |#fetch()]. - -| `fetch($sql, ...$params): ?Row` | Wykonuje zapytanie i zwraca pierwszy wiersz jako obiekt `Row` albo `null`. -| `fetchAll($sql, ...$params): array` | Wykonuje zapytanie i zwraca wszystkie wiersze jako tablicę obiektów `Row`. -| `fetchPairs($sql, ...$params): array` | Wykonuje zapytanie i zwraca tablicę asocjacyjną (pary klucz => wartość). -| `fetchField($sql, ...$params): mixed` | Wykonuje zapytanie i zwraca wartość pierwszej kolumny z pierwszego wiersza. -| `fetchList($sql, ...$params): ?array` | Wykonuje zapytanie i zwraca pierwszy wiersz jako tablicę indeksowaną albo `null`. - -Przykład: - -```php -// fetchField() - zwraca wartość pierwszej komórki -$count = $database->query('SELECT COUNT(*) FROM articles') - ->fetchField(); -``` - - -`foreach` - iterowanie po wierszach ------------------------------------ - -Po wykonaniu zapytania zwracany jest obiekt [ResultSet|api:Nette\Database\ResultSet], który pozwala przejść wyniki na kilka sposobów. Najprostszym sposobem wykonania zapytania i pobrania wierszy jest iterowanie w pętli `foreach`. Ta metoda jest najoszczędniejsza pamięciowo, bo pobiera dane wiersz po wierszu i nie ładuje całego zbioru wyników naraz do pamięci. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; - // ... -} -``` - -.[note] -Po `ResultSet` można iterować tylko raz. Jeśli potrzebujesz iterować wielokrotnie, musisz najpierw wczytać dane do tablicy, na przykład metodą `fetchAll()`. - - -fetch(): ?Row .[method] ------------------------ - -Zwraca wiersz jako obiekt `Row`. Jeśli nie ma już kolejnych wierszy, zwraca `null`. Przesuwa wewnętrzny wskaźnik na kolejny wiersz. - -```php -$result = $database->query('SELECT * FROM users'); -$row = $result->fetch(); // wczytuje pierwszy wiersz -if ($row) { - echo $row->name; -} -``` - - -fetchAll(): array .[method] ---------------------------- - -Zwraca wszystkie pozostałe wiersze z `ResultSet` jako tablicę obiektów `Row`. - -```php -$result = $database->query('SELECT * FROM users'); -$rows = $result->fetchAll(); // wczytuje wszystkie wiersze -foreach ($rows as $row) { - echo $row->name; -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Zwraca zbiór wyników jako tablicę asocjacyjną. Pierwszy argument określa kolumnę używaną jako klucze, a drugi kolumnę używaną jako wartości: - -```php -$result = $database->query('SELECT id, name FROM users'); -$names = $result->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Jeśli podany jest tylko pierwszy parametr (`$key`), jako wartość użyty zostanie cały wiersz (obiekt `Row`): - -```php -$rows = $result->fetchPairs('id'); -// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...] -``` - -W przypadku zduplikowanych kluczy używana jest wartość z ostatniego wiersza. Użycie `null` jako klucza daje tablicę indeksowaną liczbowo (od zera), co zapobiega kolizjom kluczy: - -```php -$names = $result->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Alternatywnie możesz podać callback, który przetworzy każdy wiersz. Callback może zwrócić pojedynczą wartość albo parę klucz-wartość. - -```php -$result = $database->query('SELECT * FROM users'); -$items = $result->fetchPairs(fn($row) => "$row->id - $row->name"); -// ['1 - John', '2 - Jane', ...] - -// Callback może zwrócić także tablicę z parą klucz i wartość: -$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); -// ['John' => 46, 'Jane' => 21, ...] -``` - - -fetchField(): mixed .[method] ------------------------------ - -Zwraca wartość pierwszej kolumny z bieżącego wiersza. Jeśli nie ma już kolejnych wierszy, zwraca `null`. Przesuwa wewnętrzny wskaźnik na kolejny wiersz. - -```php -$result = $database->query('SELECT name FROM users'); -$name = $result->fetchField(); // wczytuje name z pierwszego wiersza -``` - - -fetchList(): ?array .[method] ------------------------------ - -Zwraca wiersz jako tablicę indeksowaną. Jeśli nie ma już kolejnych wierszy, zwraca `null`. Przesuwa wewnętrzny wskaźnik na kolejny wiersz. - -```php -$result = $database->query('SELECT name, email FROM users'); -$row = $result->fetchList(); // ['John', 'john@example.com'] -``` - - -getRowCount(): ?int .[method] ------------------------------ - -Zwraca liczbę dotkniętych wierszy z ostatniego zapytania `UPDATE` albo `DELETE`. Dla zapytań `SELECT` zwraca liczbę wierszy w zbiorze wyników. Nie zawsze jednak musi być ona znana, wtedy metoda zwraca `null`. - - -getColumnCount(): ?int .[method] --------------------------------- - -Zwraca liczbę kolumn w `ResultSet`. - - -Informacje o zapytaniu -====================== - -Na potrzeby debugowania możemy uzyskać informacje o ostatnio wykonanym zapytaniu: - -```php -echo $database->getLastQueryString(); // wypisuje zapytanie SQL - -$result = $database->query('SELECT * FROM articles'); -echo $result->getQueryString(); // wypisuje zapytanie SQL -echo $result->getTime(); // wypisuje czas wykonania w sekundach -``` - -Żeby wyświetlić wynik jako tabelę HTML, możesz użyć: - -```php -$result = $database->query('SELECT * FROM articles'); -$result->dump(); -``` - -`ResultSet` udostępnia informacje o typach kolumn: - -```php -$result = $database->query('SELECT * FROM articles'); -$types = $result->getColumnTypes(); - -foreach ($types as $column => $type) { - echo "$column jest typu $type"; // np. 'id jest typu int' -} -``` - - -Logowanie zapytań ------------------ - -Możemy zaimplementować własne logowanie zapytań. Zdarzenie `onQuery` to tablica callbacków wywoływanych po każdym wykonanym zapytaniu: - -```php -$database->onQuery[] = function ($database, $result) use ($logger) { - $logger->info('Query: ' . $result->getQueryString()); - $logger->info('Time: ' . $result->getTime()); - - if ($result->getRowCount() > 1000) { - $logger->warning('Large result set: ' . $result->getRowCount() . ' rows'); - } -}; -``` diff --git a/database/pl/transactions.texy b/database/pl/transactions.texy deleted file mode 100644 index 3c6d8243ad..0000000000 --- a/database/pl/transactions.texy +++ /dev/null @@ -1,45 +0,0 @@ -Transakcje -********** - -.[perex] -Transakcje gwarantują, że albo wykonają się wszystkie operacje w ramach transakcji, albo żadna. Przydają się do zapewnienia spójności danych przy złożonych operacjach. - -Najprostszy sposób użycia transakcji wygląda tak: - -```php -$database->beginTransaction(); -try { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); - $database->commit(); -} catch (\Exception $e) { - $database->rollBack(); - throw $e; -} -``` - -Ten sam efekt osiągniesz znacznie elegancej metodą `transaction()`. Przyjmuje ona callback, który wykonywany jest w ramach transakcji. Jeśli callback zakończy się bez wyjątku, transakcja jest automatycznie zatwierdzana. Jeśli wystąpi wyjątek, transakcja jest wycofywana, a wyjątek propagowany dalej. - -```php -$database->transaction(function ($database) use ($id) { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); -}); -``` - -Wywołania `transaction()` można zagnieżdżać, co ułatwia komponowanie metod, z których każda zarządza własną transakcją. Do bazy danych jako `BEGIN`/`COMMIT` wysyłana jest w rzeczywistości tylko transakcja zewnętrzna; wewnętrzne wywołania jedynie śledzą głębokość zagnieżdżenia. Ręczne wywołanie `beginTransaction()`, `commit()` albo `rollBack()` wewnątrz callbacku `transaction()` rzuca `LogicException`. - -Metoda `transaction()` może też zwracać wartości: - -```php -$count = $database->transaction(function ($database) { - $result = $database->query('UPDATE users SET active = ?', true); - return $result->getRowCount(); // zwraca liczbę zaktualizowanych wierszy -}); -``` diff --git a/database/pl/type-conversion.texy b/database/pl/type-conversion.texy deleted file mode 100644 index a57a4cc47a..0000000000 --- a/database/pl/type-conversion.texy +++ /dev/null @@ -1,55 +0,0 @@ -Konwersja typów -*************** - -.[perex] -Nette Database automatycznie konwertuje wartości zwracane z bazy danych na odpowiadające im typy PHP. - - -Data i czas ------------ - -Wartości czasowe konwertowane są na obiekty `Nette\Utils\DateTime`. Jeśli chcesz, żeby wartości czasowe były konwertowane na niezmienne obiekty `Nette\Database\DateTime`, ustaw w [konfiguracji |configuration] opcję `newDateTime` na true. - -```php -$row = $database->fetch('SELECT created_at FROM articles'); -echo $row->created_at instanceof DateTime; // true -echo $row->created_at->format('j. n. Y'); -``` - -W przypadku MySQL typ danych `TIME` konwertowany jest na obiekty `DateInterval`. - - -Wartości logiczne ------------------ - -Wartości logiczne są automatycznie konwertowane na `true` albo `false`. Dla MySQL konwertowany jest `TINYINT(1)`, jeśli ustawimy w [konfiguracji |configuration] `convertBoolean`. - -```php -$row = $database->fetch('SELECT is_published FROM articles'); -echo gettype($row->is_published); // 'boolean' -``` - - -Wartości liczbowe ------------------ - -Wartości liczbowe konwertowane są na `int` albo `float` zgodnie z typem kolumny w bazie danych: - -```php -$row = $database->fetch('SELECT id, price FROM products'); -echo gettype($row->id); // integer -echo gettype($row->price); // float -``` - - -Własna normalizacja -------------------- - -Metodą `setRowNormalizer(?callable $normalizer)` możesz ustawić własną funkcję przekształcającą wiersze z bazy danych. Przydaje się to na przykład do automatycznej konwersji typów danych. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // tutaj odbywa się konwersja typów - return $row; -}); -``` diff --git a/database/pl/upgrading.texy b/database/pl/upgrading.texy deleted file mode 100644 index a8241f91de..0000000000 --- a/database/pl/upgrading.texy +++ /dev/null @@ -1,36 +0,0 @@ -Aktualizacja -************ - - -Aktualizacja do wersji 3.2 -========================== - -Minimalna wymagana wersja PHP to 8.1. - -Kod został starannie dostrojony pod PHP 8.1. Dodano wszystkie nowe type hinty metod i właściwości. Zmiany są drobne: - -- MySQL: zerowa data `0000-00-00` zwracana jest jako `null` -- MySQL: decimal bez miejsc dziesiętnych zwracany jest jako int zamiast float -- typ `time` zwracany jest jako obiekt `DateTime` z datą ustawioną na `0001-01-01` zamiast na bieżącą datę - - -Aktualizacja do wersji 3.1 -========================== - -- klasa `Nette\Database\Context` została przemianowana na `Nette\Database\Explorer` dla spójności z nazwą [Database Explorer|explorer] -- interfejsy `Nette\Database\IRow` i `Nette\Database\IRowContainer` są oznaczone jako przestarzałe jako zbędne -- sterownik `MySqlDriver` używa podzapytań -- translator poleceń SQL lepiej kontroluje, gdzie można przekazywać tablice - - -Aktualizacja do wersji 3.0 -========================== - -Niektóre metody, jak `fetch()` czy `fetchField()`, zwracają teraz `null` zamiast `false`, gdy nie ma kolejnego wiersza. - - -Aktualizacja do wersji 2.3 -========================== - -- `MySqlDriver` używa dla MySQL >= 5.5.3 domyślnie kodowania `utf8mb4` zamiast `utf8` -- `IReflection` został podzielony na bliźniacze interfejsy `IStructure` i `IConventions` diff --git a/database/ru/@home.texy b/database/ru/@home.texy deleted file mode 100644 index 412e6353f6..0000000000 --- a/database/ru/@home.texy +++ /dev/null @@ -1,18 +0,0 @@ -Поддерживаемые базы данных -========================== - -Поддерживаются такие серверы баз данных: - -|* Сервер базы данных |* Имя DSN |* Поддержка Core |* Поддержка Explorer -| MySQL (>= 5.1) | mysql | ДА | ДА -| PostgreSQL (>= 9.0) | pgsql | ДА | ДА -| Sqlite 3 (>= 3.8) | sqlite | ДА | ДА -| Oracle | oci | ДА | - -| MS SQL (PDO_SQLSRV) | sqlsrv | ДА | ДА -| MS SQL (PDO_DBLIB) | mssql | ДА | - -| ODBC | odbc | ДА | - - - - -{{maintitle: Nette Database - прекрасный слой работы с базой данных для PHP}} -{{description: Nette Database значительно упрощает получение данных из базы без написания SQL-запросов. Он выполняет эффективные запросы и не передаёт лишних данных.}} diff --git a/database/ru/@left-menu.texy b/database/ru/@left-menu.texy deleted file mode 100644 index 655f71354f..0000000000 --- a/database/ru/@left-menu.texy +++ /dev/null @@ -1,21 +0,0 @@ -Nette Database -************** -- [Первые шаги |guide] -- [SQL-подход|sql-way] -- [Explorer|explorer] -- [Транзакции|transactions] -- [Исключения|exceptions] -- [Рефлексия|reflection] -- [Преобразование типов |type-conversion] -- [Конфигурация|configuration] -- [Риски безопасности |security] -- [Обновление|upgrading] - - -Дополнительные материалы -************************ -- [Документация Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Лучшие практики |best-practices:] -- [Устранение неполадок |nette:troubleshooting] diff --git a/database/ru/@meta.texy b/database/ru/@meta.texy deleted file mode 100644 index 7f329adfce..0000000000 --- a/database/ru/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документация Nette}} diff --git a/database/ru/configuration.texy b/database/ru/configuration.texy deleted file mode 100644 index b6250e6811..0000000000 --- a/database/ru/configuration.texy +++ /dev/null @@ -1,110 +0,0 @@ -Конфигурация базы данных -************************ - -.[perex] -Обзор параметров конфигурации Nette Database. - -Если вы используете не весь фреймворк, а только эту библиотеку, прочитайте, [как загрузить конфигурацию|bootstrap:]. - - -Одно соединение ---------------- - -Настройка одного соединения с базой данных: - -```neon -database: - # DSN, единственный обязательный ключ - dsn: "sqlite:%appDir%/Model/demo.db" - user: ... - password: ... -``` - -Так создаются сервисы `Nette\Database\Connection` и `Nette\Database\Explorer`, которые обычно передаются через [autowiring |dependency-injection:autowiring] или по ссылке на [их имя |#Сервисы DI]. - -Другие настройки: - -```neon -database: - # показывать панель базы данных в Tracy Bar? - debugger: ... # (bool) по умолчанию включено, если Tracy активна - - # показывать EXPLAIN запроса в Tracy Bar? - explain: ... # (bool) по умолчанию true - - # включить autowiring для этого соединения? - autowired: ... # (bool) по умолчанию true для первого соединения - - # соглашения о таблицах: discovered, static или имя класса - conventions: discovered # (string) по умолчанию 'discovered' - - options: - # подключаться к базе данных только при необходимости? - lazy: ... # (bool) по умолчанию false - - # класс драйвера базы данных PHP - driverClass: # (string) - - # только MySQL: задаёт sql_mode - sqlmode: # (string) - - # только MySQL: задаёт SET NAMES - charset: # (string) по умолчанию 'utf8mb4' - - # только MySQL: преобразует TINYINT(1) в bool - convertBoolean: # (bool) по умолчанию false - - # возвращает столбцы с датой как неизменяемые объекты (начиная с версии 3.2.1) - newDateTime: # (bool) по умолчанию false - - # только Oracle и SQLite: формат хранения даты - formatDateTime: # (string) по умолчанию 'U' -``` - -Ключ `options` может содержать и другие параметры из [документации драйвера PDO |https://www.php.net/manual/en/pdo.drivers.php], например: - -```neon -database: - options: - PDO::MYSQL_ATTR_COMPRESS: true -``` - - -Несколько соединений --------------------- - -В конфигурации мы можем определить несколько соединений с базой данных, разделив их на именованные секции: - -```neon -database: - main: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password - - another: - dsn: 'sqlite::memory:' -``` - -Autowiring включён только для сервисов из первой секции. Это можно изменить через `autowired: false` или `autowired: true`. - - -Сервисы DI ----------- - -Эти сервисы добавляются в DI-контейнер, где `###` обозначает имя соединения: - -| Имя | Тип | Описание -|---------------------------|---------------------------------|--------------------------- -| `database.###.connection` | [api:Nette\Database\Connection] | соединение с базой данных -| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |explorer] - - -Если мы определяем только одно соединение, именами сервисов будут `database.default.connection` и `database.default.explorer`. Если мы определяем несколько соединений, как в примере выше, имена будут соответствовать секциям, то есть `database.main.connection`, `database.main.explorer`, а также `database.another.connection` и `database.another.explorer`. - -Сервисы, не участвующие в autowiring, мы передаём явно, ссылаясь на их имя: - -```neon -services: - - UserFacade(@database.another.connection) -``` diff --git a/database/ru/exceptions.texy b/database/ru/exceptions.texy deleted file mode 100644 index 7aa8625103..0000000000 --- a/database/ru/exceptions.texy +++ /dev/null @@ -1,37 +0,0 @@ -Исключения -********** - -Nette Database использует иерархию исключений. Базовый класс - `Nette\Database\DriverException`, который расширяет `PDOException` и даёт дополнительные возможности для работы с ошибками базы данных: - -- Метод `getDriverCode()` возвращает код ошибки от драйвера базы данных. -- Метод `getSqlState()` возвращает код SQLSTATE. -- Методы `getQueryString()` и `getParameters()` позволяют получить исходный запрос и его параметры. - -Класс `DriverException` расширяют следующие специализированные исключения: - -- `ConnectionException` - обозначает неудачу при подключении к серверу базы данных. - - `ConnectionLostException` .{data-version:3.2.9} - соединение было разорвано во время операции (перезапуск сервера, сбой сети, idle timeout); перед дальнейшим использованием нужно переподключиться. -- `ConstraintViolationException` - базовый класс для нарушений ограничений базы данных, от которого наследуются следующие исключения: - - `ForeignKeyConstraintViolationException` - нарушение ограничения внешнего ключа. - - `NotNullConstraintViolationException` - нарушение ограничения NOT NULL. - - `UniqueConstraintViolationException` - нарушение ограничения уникальности. - - `CheckConstraintViolationException` .{data-version:3.2.9} - нарушение ограничения CHECK. -- `DeadlockException` .{data-version:3.2.9} - deadlock или сериализационный конфликт, обнаруженный сервером; транзакция была откачена и может быть повторена. -- `LockTimeoutException` .{data-version:3.2.9} - превышено время ожидания блокировки; инструкция была прервана, но окружающая транзакция обычно остаётся открытой. - -Следующий пример показывает, как перехватить `UniqueConstraintViolationException`, которое возникает при попытке вставить пользователя с адресом электронной почты, уже существующим в базе данных (при условии, что у столбца `email` есть уникальный индекс): - -```php -try { - $database->query('INSERT INTO users', [ - 'email' => 'john@example.com', - 'name' => 'John Doe', - 'password' => $hashedPassword, - ]); -} catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'A user with this email already exists.'; - -} catch (Nette\Database\DriverException $e) { - echo 'An error occurred during registration: ' . $e->getMessage(); -} -``` diff --git a/database/ru/explorer.texy b/database/ru/explorer.texy deleted file mode 100644 index 2a2f27de84..0000000000 --- a/database/ru/explorer.texy +++ /dev/null @@ -1,912 +0,0 @@ -Database Explorer -***************** - -<div class=perex> - -Explorer предлагает интуитивный и эффективный способ работы с базой данных. Он сам заботится о связях между таблицами и оптимизирует запросы, так что вы можете сосредоточиться на логике приложения. Работает сразу, без всякой настройки. Если вам нужен полный контроль над SQL-запросами, воспользуйтесь [SQL-подходом |SQL way]. - -- Работа с данными естественна и понятна -- Порождает оптимизированные SQL-запросы, которые получают только нужные данные -- Даёт лёгкий доступ к связанным данным без написания JOIN-запросов -- Работает сразу, без всякой настройки и порождения сущностей - -</div> - - -Работа с Explorer начинается с вызова метода `table()` на объекте [api:Nette\Database\Explorer] (о настройке соединения с базой данных читайте в разделе [Соединение и настройка |guide#Соединение и настройка]): - -```php -$books = $explorer->table('book'); // 'book' - имя таблицы -``` - -Метод возвращает объект [Selection |api:Nette\Database\Table\Selection], представляющий SQL-запрос. К этому объекту можно цепочкой присоединять дальнейшие методы для фильтрации и сортировки результатов. Запрос собирается и выполняется только в момент, когда данные запрашиваются, например при обходе через `foreach`. Каждая строка представлена объектом [ActiveRow |api:Nette\Database\Table\ActiveRow]: - -```php -foreach ($books as $book) { - echo $book->title; // выводит столбец 'title' - echo $book->author_id; // выводит столбец 'author_id' -} -``` - -Explorer сильно упрощает работу со [связями между таблицами |#Связи между таблицами]. Следующий пример показывает, как легко вывести данные из связанных таблиц (книги и их авторы). Обратите внимание, что писать JOIN-запросы не нужно, Nette порождает их за нас: - -```php -$books = $explorer->table('book'); - -foreach ($books as $book) { - echo 'Книга: ' . $book->title; - echo 'Автор: ' . $book->author->name; // создаёт JOIN с таблицей 'author' -} -``` - -Nette Database Explorer оптимизирует запросы так, чтобы они были максимально эффективными. Приведённый выше пример выполняет всего два SELECT-запроса независимо от того, обрабатываем ли мы 10 или 10 000 книг. - -Кроме того, Explorer отслеживает, какие столбцы используются в коде, и получает из базы данных только их, что даёт дополнительный выигрыш в производительности. Это поведение полностью автоматическое и адаптивное. Если позже вы измените код и станете использовать другие столбцы, Explorer сам подстроит запросы. Ничего не нужно настраивать и не нужно думать о том, какие столбцы понадобятся, оставьте это Nette. - - -Фильтрация и сортировка -======================= - -Класс `Selection` предоставляет методы для фильтрации и сортировки выборки данных. - -.[language-php] -| `where($condition, ...$params)` | Добавляет условие WHERE. Несколько условий объединяются оператором AND | -| `whereOr(array $conditions)` | Добавляет группу условий WHERE, объединённых оператором OR | -| `wherePrimary($value)` | Добавляет условие WHERE по первичному ключу | -| `order($columns, ...$params)` | Задаёт сортировку ORDER BY | -| `select($columns, ...$params)` | Указывает, какие столбцы получать | -| `limit($limit, $offset = null)` | Ограничивает количество строк (LIMIT) и при необходимости задаёт OFFSET | -| `page($page, $itemsPerPage, &$numOfPages = null)` | Задаёт постраничный вывод | -| `group($columns, ...$params)` | Группирует строки (GROUP BY) | -| `having($condition, ...$params)`| Добавляет условие HAVING для фильтрации сгруппированных строк | - -Методы можно объединять в цепочку (так называемый [текучий интерфейс |nette:introduction-to-object-oriented-programming#Текучие интерфейсы]): `$table->where(...)->order(...)->limit(...)`. - -В этих методах можно также использовать особые обозначения для доступа к [данным из связанных таблиц |#Запросы через связанные таблицы]. - - -Экранирование и идентификаторы ------------------------------- - -Методы автоматически экранируют параметры и заключают идентификаторы (имена таблиц и столбцов) в кавычки, что предотвращает SQL injection. Чтобы всё работало правильно, нужно соблюдать несколько правил: - -- Ключевые слова, имена функций, процедур и т. п. пишите **заглавными буквами**. -- Имена столбцов и таблиц пишите **строчными буквами**. -- Строки всегда передавайте через **параметры**. - -```php -where('name = ' . $name); // КРИТИЧЕСКАЯ УЯЗВИМОСТЬ: SQL injection -where('name LIKE "%search%"'); // НЕВЕРНО: усложняет автоматическое заключение в кавычки -where('name LIKE ?', '%search%'); // ВЕРНО: значение передано параметром - -where('name like ?', $name); // НЕВЕРНО: порождает: `name` `like` ? -where('name LIKE ?', $name); // ВЕРНО: порождает: `name` LIKE ? -where('LOWER(name) = ?', $value);// ВЕРНО: LOWER(`name`) = ? -``` - - -where(string|array $condition, ...$parameters): static .[method] ----------------------------------------------------------------- - -Фильтрует результаты с помощью условий WHERE. Его сила в том, что он умно обрабатывает разные типы значений и сам выбирает подходящие SQL-операторы. - -Базовое использование: - -```php -$table->where('id', $value); // WHERE `id` = 123 -$table->where('id > ?', $value); // WHERE `id` > 123 -$table->where('id = ? OR name = ?', $id, $name); // WHERE `id` = 1 OR `name` = 'Jon Snow' -``` - -Благодаря автоматическому определению подходящего оператора вам не нужно разбираться с разными особыми случаями, Nette решит их за вас: - -```php -$table->where('id', 1); // WHERE `id` = 1 -$table->where('id', null); // WHERE `id` IS NULL -$table->where('id', [1, 2, 3]); // WHERE `id` IN (1, 2, 3) -// можно использовать и подстановку ? без оператора: -$table->where('id ?', 1); // WHERE `id` = 1 -``` - -Метод правильно обрабатывает и отрицательные условия, и пустые массивы: - -```php -$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- не найдёт ничего -$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- найдёт всё -$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- найдёт всё -// $table->where('NOT id ?', $ids); // ВНИМАНИЕ: такой синтаксис не поддерживается -``` - -Параметром можно передать и результат другого запроса к таблице, тем самым создав подзапрос: - -```php -// WHERE `id` IN (SELECT `id` FROM `tableName`) -$table->where('id', $explorer->table($tableName)); - -// WHERE `id` IN (SELECT `col` FROM `tableName`) -$table->where('id', $explorer->table($tableName)->select('col')); -``` - -Условия можно передать и массивом, элементы которого объединяются оператором AND: - -```php -// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) -$table->where([ - 'price_final < price_original', - 'stock_count > min_stock', -]); -``` - -В массиве можно использовать пары ключ => значение, и Nette снова сам выберет верные операторы: - -```php -// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3)) -$table->where([ - 'status' => 'active', - 'id' => [1, 2, 3], -]); -``` - -В массиве можно сочетать SQL-выражения с подстановками и несколькими параметрами. Это удобно для сложных условий с точно заданными операторами: - -```php -// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) -$table->where([ - 'age > ?' => 18, - 'ROUND(score, ?) > ?' => [2, 75.5], // два параметра передаются массивом -]); -``` - -Многократные вызовы `where()` автоматически объединяют условия оператором AND. - - -whereOr(array $parameters): static .[method] --------------------------------------------- - -Похож на `where()`: тоже добавляет условия, но объединяет их оператором OR: - -```php -// WHERE (`status` = 'active') OR (`deleted` = 1) -$table->whereOr([ - 'status' => 'active', - 'deleted' => true, -]); -``` - -И здесь можно использовать более сложные выражения: - -```php -// WHERE (`price` > 1000) OR (`price_with_tax` > 1500) -$table->whereOr([ - 'price > ?' => 1000, - 'price_with_tax > ?' => 1500, -]); -``` - - -wherePrimary(mixed $key): static .[method] ------------------------------------------- - -Добавляет условие по первичному ключу таблицы: - -```php -// WHERE `id` = 123 -$table->wherePrimary(123); - -// WHERE `id` IN (1, 2, 3) -$table->wherePrimary([1, 2, 3]); -``` - -Если у таблицы составной первичный ключ (например, `foo_id`, `bar_id`), передайте его массивом: - -```php -// WHERE `foo_id` = 1 AND `bar_id` = 5 -$table->wherePrimary(['foo_id' => 1, 'bar_id' => 5])->fetch(); - -// WHERE (`foo_id`, `bar_id`) IN ((1, 5), (2, 3)) -$table->wherePrimary([ - ['foo_id' => 1, 'bar_id' => 5], - ['foo_id' => 2, 'bar_id' => 3], -])->fetchAll(); -``` - - -order(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Задаёт порядок, в котором возвращаются строки. Сортировать можно по одному или нескольким столбцам, по возрастанию или по убыванию, а также по собственному выражению: - -```php -$table->order('created'); // ORDER BY `created` -$table->order('created DESC'); // ORDER BY `created` DESC -$table->order('priority DESC, created'); // ORDER BY `priority` DESC, `created` -$table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC -``` - - -select(string $columns, ...$parameters): static .[method] ---------------------------------------------------------- - -Указывает столбцы, которые нужно вернуть из базы данных. По умолчанию Nette Database Explorer возвращает только те столбцы, которые действительно используются в коде. Метод `select()` применяйте тогда, когда нужно получить конкретные выражения: - -```php -// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` -$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); -``` - -Псевдонимы, заданные через `AS`, затем доступны как свойства объекта `ActiveRow`: - -```php -foreach ($table as $row) { - echo $row->formatted_date; // обращение к псевдониму -} -``` - - -limit(?int $limit, ?int $offset = null): static .[method] ---------------------------------------------------------- - -Ограничивает количество возвращаемых строк (LIMIT) и при необходимости позволяет задать смещение: - -```php -$table->limit(10); // LIMIT 10 (вернёт первые 10 строк) -$table->limit(10, 20); // LIMIT 10 OFFSET 20 -``` - -Для постраничного вывода уместнее использовать метод `page()`. - - -page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] -------------------------------------------------------------------------- - -Облегчает постраничный вывод результатов. Принимает номер страницы (начиная с 1) и количество элементов на странице. Необязательно можно передать ссылку на переменную, в которую будет записано общее количество страниц: - -```php -$numOfPages = null; -$table->page(page: 3, itemsPerPage: 10, numOfPages: $numOfPages); -echo "Всего страниц: $numOfPages"; -``` - - -group(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Группирует строки по указанным столбцам (GROUP BY). Обычно используется вместе с агрегатными функциями: - -```php -// Подсчитывает количество товаров в каждой категории -$table->select('category_id, COUNT(*) AS count') - ->group('category_id'); -``` - - -having(string $having, ...$parameters): static .[method] --------------------------------------------------------- - -Задаёт условие для фильтрации сгруппированных строк (HAVING). Его можно использовать вместе с методом `group()` и агрегатными функциями: - -```php -// Находит категории, в которых больше 100 товаров -$table->select('category_id, COUNT(*) AS count') - ->group('category_id') - ->having('count > ?', 100); -``` - - -Чтение данных -============= - -Для чтения данных из базы доступно несколько полезных методов: - -.[language-php] -| `foreach ($table as $key => $row)` | Обходит все строки, `$key` - значение первичного ключа, `$row` - объект ActiveRow | -| `$row = $table->get($key)` | Возвращает одну строку по первичному ключу | -| `$row = $table->fetch()` | Возвращает текущую строку и сдвигает указатель на следующую | -| `$array = $table->fetchPairs()` | Создаёт из результатов ассоциативный массив | -| `$array = $table->fetchAll()` | Возвращает все строки массивом | -| `count($table)` | Возвращает количество строк в объекте Selection | - -Объект [ActiveRow |api:Nette\Database\Table\ActiveRow] доступен только для чтения. Это значит, что менять значения его свойств нельзя. Такое ограничение обеспечивает согласованность данных и предотвращает неожиданные побочные эффекты. Данные загружаются из базы, и любые изменения должны выполняться явно и контролируемо. - - -`foreach` - обход всех строк ----------------------------- - -Проще всего выполнить запрос и получить строки обходом через цикл `foreach`. Он автоматически выполнит SQL-запрос. - -```php -$books = $explorer->table('book'); -foreach ($books as $key => $book) { - // $key - значение первичного ключа, $book - ActiveRow - echo "$book->title ({$book->author->name})"; -} -``` - - -get($key): ?ActiveRow .[method] -------------------------------- - -Выполняет SQL-запрос и возвращает строку по первичному ключу либо `null`, если такой строки нет. - -```php -$book = $explorer->table('book')->get(123); // вернёт ActiveRow с ID 123 или null -if ($book) { - echo $book->title; -} -``` - - -fetch(): ?ActiveRow .[method] ------------------------------ - -Возвращает текущую строку и сдвигает внутренний указатель на следующую. Если строк больше нет, возвращает `null`. - -```php -$books = $explorer->table('book'); -while ($book = $books->fetch()) { - $this->processBook($book); -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Возвращает результаты ассоциативным массивом. Первый аргумент задаёт имя столбца, который будет использован как ключ массива, второй - имя столбца, который будет использован как значение: - -```php -$authors = $explorer->table('author')->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Если задан только первый параметр, значением будет вся строка, то есть объект `ActiveRow`: - -```php -$authors = $explorer->table('author')->fetchPairs('id'); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - -При повторяющихся ключах используется значение из последней строки. Если в качестве ключа указать `null`, массив будет пронумерован с нуля (тогда коллизий не возникает): - -```php -$authors = $explorer->table('author')->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Как вариант, параметром можно передать callback, который для каждой строки вернёт либо само значение, либо пару ключ - значение. - -```php -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); -// ['First Book (John Novak)', ...] - -// Callback может вернуть и массив с парой ключ и значение: -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => [$row->title, $row->author->name]); -// ['First Book' => 'John Novak', ...] -``` - - -fetchAll(): array .[method] ---------------------------- - -Возвращает все строки ассоциативным массивом объектов `ActiveRow`, где ключами служат значения первичного ключа. - -```php -$allBooks = $explorer->table('book')->fetchAll(); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - - -count(): int .[method] ----------------------- - -Метод `count()` без параметра возвращает количество строк в объекте `Selection`: - -```php -$table->where('category', 1); -$count = $table->count(); -$count = count($table); // вариант -``` - -Замечание: `count()` с параметром выполняет в базе данных агрегатную функцию COUNT, см. ниже. - - -ActiveRow::toArray(): array .[method] -------------------------------------- - -Преобразует объект `ActiveRow` в ассоциативный массив, где ключи - имена столбцов, а значения - соответствующие данные. - -```php -$book = $explorer->table('book')->get(1); -$bookArray = $book->toArray(); -// $bookArray будет ['id' => 1, 'title' => '...', 'author_id' => ..., ...] -``` - - -Агрегация -========= - -Класс `Selection` предоставляет методы для удобного выполнения агрегатных функций (COUNT, SUM, MIN, MAX, AVG и т. д.). - -.[language-php] -| `count($expr)` | Подсчитывает количество строк | -| `min($expr)` | Возвращает наименьшее значение в столбце | -| `max($expr)` | Возвращает наибольшее значение в столбце | -| `sum($expr)` | Возвращает сумму значений в столбце | -| `aggregation($function)` | Позволяет выполнить любую агрегатную функцию, например `AVG()` или `GROUP_CONCAT()` | - - -count(string $expr): int .[method] ----------------------------------- - -Выполняет SQL-запрос с функцией COUNT и возвращает результат. Метод используется, чтобы узнать, сколько строк соответствует определённому условию: - -```php -$count = $table->count('*'); // SELECT COUNT(*) FROM `table` -$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` -``` - -Замечание: [#count()] без параметра лишь возвращает количество строк в объекте `Selection`. - - -min(string $expr) и max(string $expr) .[method] ------------------------------------------------ - -Методы `min()` и `max()` возвращают наименьшее и наибольшее значение в указанном столбце или выражении: - -```php -// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 -$maxPrice = $products->where('active', true) - ->max('price'); -``` - - -sum(string $expr): mixed .[method] ----------------------------------- - -Возвращает сумму значений в указанном столбце или выражении: - -```php -// SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 -$totalPrice = $products->where('active', true) - ->sum('price * items_in_stock'); -``` - - -aggregation(string $function, ?string $groupFunction = null): mixed .[method] ------------------------------------------------------------------------------ - -Позволяет выполнить любую агрегатную функцию. - -```php -// средняя цена товаров в категории -$avgPrice = $products->where('category_id', 1) - ->aggregation('AVG(price)'); - -// объединяет теги товара в одну строку -$tags = $products->where('id', 1) - ->aggregation('GROUP_CONCAT(tag.name) AS tags') - ->fetch() - ->tags; -``` - -Если нам нужно агрегировать результаты, которые сами уже получены агрегатной функцией и группировкой (например, `SUM(value)` по сгруппированным строкам), вторым аргументом мы указываем агрегатную функцию, которую следует применить к этим промежуточным результатам: - -```php -// Вычисляет суммарную цену товаров на складе для отдельных категорий, а затем складывает эти цены вместе. -$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') - ->group('category_id') - ->aggregation('SUM(category_total)', 'SUM'); -``` - -В этом примере мы сначала вычисляем суммарную цену товаров в каждой категории (`SUM(price * stock) AS category_total`) и группируем результаты по `category_id`. Затем через `aggregation('SUM(category_total)', 'SUM')` складываем эти промежуточные суммы `category_total`. Второй аргумент `'SUM'` указывает, что к промежуточным результатам нужно применить функцию SUM. - - -Вставка, изменение и удаление -============================= - -Nette Database Explorer упрощает вставку, изменение и удаление данных. Все упомянутые методы в случае ошибки выбрасывают `Nette\Database\DriverException`. - - -Selection::insert(iterable $data) .[method] -------------------------------------------- - -Вставляет в таблицу новые записи. - -**Вставка одной записи:** - -Новую запись передайте ассоциативным массивом или итерируемым объектом (например, `ArrayHash`, который используется в [формах |forms:]), где ключи соответствуют именам столбцов таблицы. - -Если у таблицы задан первичный ключ, метод возвращает объект `ActiveRow`, который перезагружается из базы данных, чтобы отразить изменения, сделанные на уровне базы (триггеры, значения столбцов по умолчанию, вычисление автоинкрементных столбцов). Тем самым обеспечивается согласованность данных, а объект всегда содержит актуальные данные из базы. Если у таблицы нет первичного ключа, идентифицировать строку невозможно, и метод возвращает `null`. - -```php -$row = $explorer->table('users')->insert([ - 'name' => 'John Doe', - 'email' => 'john.doe@example.com', -]); -// $row - экземпляр ActiveRow, содержащий полные данные вставленной строки, -// включая автоматически порождённый ID и любые изменения, сделанные триггерами -echo $row->id; // Выведет ID только что вставленного пользователя -echo $row->created_at; // Выведет время создания, если его задаёт триггер -``` - -**Вставка нескольких записей сразу:** - -Метод `insert()` позволяет вставить несколько записей одним SQL-запросом. В этом случае он возвращает количество вставленных строк. - -```php -$insertedRows = $explorer->table('users')->insert([ - [ - 'name' => 'John', - 'year' => 1994, - ], - [ - 'name' => 'Jack', - 'year' => 1995, - ], -]); -// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) -// $insertedRows будет 2 -``` - -Параметром можно передать и объект `Selection` с выборкой данных. - -```php -$newUsers = $explorer->table('potential_users') - ->where('approved', 1) - ->select('name, email'); - -$insertedRows = $explorer->table('users')->insert($newUsers); -``` - -**Вставка особых значений:** - -В качестве значений можно передавать и файлы, объекты `DateTime` или SQL-литералы: - -```php -$explorer->table('users')->insert([ - 'name' => 'John', - 'created_at' => new DateTime, // преобразует в формат базы данных - 'avatar' => fopen('image.jpg', 'rb'), // вставит двоичное содержимое файла - 'uuid' => $explorer::literal('UUID()'), // вызовет функцию UUID() -]); -``` - - -Selection::update(iterable $data): int .[method] ------------------------------------------------- - -Изменяет строки в таблице согласно заданному фильтру. Возвращает количество действительно изменённых строк. - -Изменяемые столбцы передайте ассоциативным массивом или итерируемым объектом (например, `ArrayHash`, который используется в [формах |forms:]), где ключи соответствуют именам столбцов таблицы: - -```php -$affected = $explorer->table('users') - ->where('id', 10) - ->update([ - 'name' => 'John Smith', - 'year' => 1994, - ]); -// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 -``` - -Для изменения числовых значений можно использовать операторы `+=` и `-=`: - -```php -$explorer->table('users') - ->where('id', 10) - ->update([ - 'points+=' => 1, // увеличивает значение столбца 'points' на 1 - 'coins-=' => 1, // уменьшает значение столбца 'coins' на 1 - ]); -// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 -``` - - -Selection::delete(): int .[method] ----------------------------------- - -Удаляет строки из таблицы согласно заданному фильтру. Возвращает количество удалённых строк. - -```php -$count = $explorer->table('users') - ->where('id', 10) - ->delete(); -// DELETE FROM `users` WHERE `id` = 10 -``` - -.[caution] -При вызове `update()` или `delete()` не забудьте с помощью `where()` указать строки, которые нужно изменить или удалить. Если `where()` не использовать, операция выполнится над всей таблицей! - - -ActiveRow::update(iterable $data): bool .[method] -------------------------------------------------- - -Изменяет данные в строке базы данных, представленной объектом `ActiveRow`. Принимает итерируемую структуру с данными для изменения (ключи - имена столбцов). Для изменения числовых значений можно использовать операторы `+=` и `-=`: - -После выполнения изменения `ActiveRow` автоматически перезагружается из базы данных, чтобы отразить изменения, сделанные на уровне базы (например, триггерами). Метод возвращает `true` только тогда, когда данные действительно изменились. - -```php -$article = $explorer->table('article')->get(1); -$article->update([ - 'views += 1', // увеличивает счётчик просмотров -]); -echo $article->views; // Выведет текущее количество просмотров -``` - -Этот метод изменяет только одну конкретную строку в базе данных. Для массового изменения нескольких строк используйте метод [#Selection::update()]. - - -ActiveRow::delete(): int .[method] ----------------------------------- - -Удаляет строку базы данных, представленную объектом `ActiveRow`. Возвращает количество удалённых строк, которое должно быть равно 1. - -```php -$book = $explorer->table('book')->get(1); -$book->delete(); // Удалит книгу с ID 1 -``` - -Этот метод удаляет только одну конкретную строку в базе данных. Для массового удаления нескольких строк используйте метод [#Selection::delete()]. - - -Связи между таблицами -===================== - -В реляционных базах данных данные разделены на несколько таблиц и связаны между собой внешними ключами. Nette Database Explorer предлагает революционный способ работы с этими связями: без написания JOIN-запросов и без необходимости что-либо настраивать или порождать. - -Для демонстрации работы со связями воспользуемся примером базы данных книг ([найдёте её на GitHub |https://github.com/nette-examples/books]). В базе данных у нас есть таблицы: - -- `author` - писатели и переводчики (столбцы `id`, `name`, `web`, `born`) -- `book` - книги (столбцы `id`, `author_id`, `translator_id`, `title`, `sequel_id`) -- `tag` - теги (столбцы `id`, `name`) -- `book_tag` - связующая таблица между книгами и тегами (столбцы `book_id`, `tag_id`) - -[* db-schema-1-.webp *] *** Структура базы данных, используемой в примерах .<> - -В нашем примере базы данных книг мы находим несколько типов связей (хотя модель упрощена по сравнению с реальностью): - -- **Один ко многим (1:N)** - у каждой книги **есть один** автор, автор может написать **несколько** книг. -- **Ноль ко многим (0:N)** - у книги **может быть** переводчик, переводчик может перевести **несколько** книг. -- **Ноль к одному (0:1)** - у книги **может быть** продолжение. -- **Многие ко многим (M:N)** - у книги **может быть несколько** тегов, а тег может быть присвоен **нескольким** книгам. - -В этих связях всегда есть **родительская таблица** и **дочерняя таблица**. Например, в связи между авторами и книгами таблица `author` родительская, а таблица `book` дочерняя: можно представить себе, что книга всегда "принадлежит" какому-то автору. Это отражено и в структуре базы данных: дочерняя таблица `book` содержит внешний ключ `author_id`, ссылающийся на родительскую таблицу `author`. - -Если нам нужно вывести книги вместе с именами их авторов, у нас есть две возможности. Либо получить данные одним SQL-запросом с использованием JOIN: - -```sql -SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; -``` - -Либо получить данные в два шага - сначала книги, потом их авторов - и затем собрать их в PHP: - -```sql -SELECT * FROM book; -SELECT * FROM author WHERE id IN (1, 2, 3); -- ID авторов выбранных книг -``` - -Второй подход на самом деле **эффективнее**, хотя это может показаться неожиданным. Данные получаются только один раз и лучше используются в кеше. Именно так работает Nette Database Explorer: он всё делает за кулисами и предлагает вам изящный API: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo 'название: ' . $book->title; - echo 'написал: ' . $book->author->name; // $book->author - запись из таблицы 'author' - echo 'перевёл: ' . $book->translator?->name; -} -``` - - -Доступ к родительской таблице ------------------------------ - -Доступ к родительской таблице прост. Речь о связях вроде *у книги есть автор* или *у книги может быть переводчик*. Связанную запись получаем через свойство объекта ActiveRow, имя которого соответствует имени столбца внешнего ключа без суффикса `_id`: - -```php -$book = $explorer->table('book')->get(1); -echo $book->author->name; // найдёт автора по столбцу author_id -echo $book->translator?->name; // найдёт переводчика по столбцу translator_id -``` - -При обращении к свойству `$book->author` Explorer ищет в таблице `book` столбец, имя которого содержит строку `author` (то есть `author_id`). По значению в этом столбце он загружает соответствующую запись из таблицы `author` и возвращает её как `ActiveRow`. Точно так же `$book->translator` использует столбец `translator_id`. Поскольку столбец `translator_id` может содержать `null`, в коде мы используем nullsafe-оператор `?->`. - -Другой подход предлагает метод `ref()`, который принимает два аргумента - имя целевой таблицы и имя связующего столбца - и возвращает экземпляр `ActiveRow` или `null`: - -```php -echo $book->ref('author', 'author_id')->name; // связь с автором -echo $book->ref('author', 'translator_id')->name; // связь с переводчиком -``` - -Метод `ref()` пригодится, если нельзя использовать обращение через свойство, например потому что таблица содержит столбец с таким же именем (то есть `author`). В остальных случаях для лучшей читаемости рекомендуется использовать обращение через свойство. - -Explorer автоматически оптимизирует запросы к базе данных. Когда мы обходим книги в цикле и обращаемся к их связанным записям (авторам, переводчикам), Explorer не порождает запрос для каждой книги отдельно. Вместо этого он выполняет только **один SELECT-запрос для каждого типа связи**, что существенно снижает нагрузку на базу данных. Например: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo $book->title . ': '; - echo $book->author->name; - echo $book->translator?->name; -} -``` - -Этот код выполнит к базе данных всего три молниеносных запроса: - -```sql -SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- ID из столбца author_id выбранных книг -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- ID из столбца translator_id выбранных книг -``` - -.[note] -Логика поиска связующего столбца задаётся реализацией [Conventions |api:Nette\Database\Conventions]. Мы рекомендуем использовать [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], которая анализирует внешние ключи и позволяет легко работать с существующими связями между таблицами. - - -Доступ к дочерней таблице -------------------------- - -Доступ к дочерней таблице работает в обратном направлении. Теперь мы спрашиваем, *какие книги написал этот автор* или *какие книги перевёл этот переводчик*. Для такого запроса служит метод `related()`, который возвращает `Selection` со связанными записями. Посмотрим на пример: - -```php -$author = $explorer->table('author')->get(1); - -// Выводит все книги автора -foreach ($author->related('book.author_id') as $book) { - echo "Написал: $book->title"; -} - -// Выводит все книги, переведённые автором -foreach ($author->related('book.translator_id') as $book) { - echo "Перевёл: $book->title"; -} -``` - -Метод `related()` принимает описание связи одним аргументом с точечной записью либо двумя отдельными аргументами: - -```php -$author->related('book.translator_id'); // один аргумент -$author->related('book', 'translator_id'); // два аргумента -``` - -Explorer умеет автоматически определить верный связующий столбец по имени родительской таблицы. В данном случае он соединяет через столбец `book.author_id`, потому что имя исходной таблицы - `author`: - -```php -$author->related('book'); // использует book.author_id -``` - -Если возможных соединений несколько, Explorer выбросит [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -Метод `related()` мы можем, разумеется, использовать и при обходе нескольких записей в цикле, и Explorer автоматически оптимизирует запросы и в этом случае: - -```php -$authors = $explorer->table('author'); -foreach ($authors as $author) { - echo $author->name . ' написал:'; - foreach ($author->related('book') as $book) { - echo $book->title; - } -} -``` - -Этот код породит всего два молниеносных SQL-запроса: - -```sql -SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- ID выбранных авторов -``` - - -Связь многие ко многим ----------------------- - -Для связи многие ко многим (M:N) нужна **связующая таблица** (в нашем случае `book_tag`), содержащая два столбца внешних ключей (`book_id`, `tag_id`). Каждый из этих столбцов ссылается на первичный ключ одной из связываемых таблиц. Чтобы получить связанные данные, мы сначала получаем записи из связующей таблицы через `related('book_tag')`, а затем идём дальше к целевым данным: - -```php -$book = $explorer->table('book')->get(1); -// выводит имена тегов, присвоенных книге -foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name; // выводит имя тега через связующую таблицу -} - -$tag = $explorer->table('tag')->get(1); -// или наоборот: выводит названия книг, помеченных этим тегом -foreach ($tag->related('book_tag') as $bookTag) { - echo $bookTag->book->title; // выводит название книги -} -``` - -Explorer снова оптимизирует SQL-запросы в эффективную форму: - -```sql -SELECT * FROM `book`; -SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- ID выбранных книг -SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- ID тегов, найденных в book_tag -``` - - -Запросы через связанные таблицы -------------------------------- - -В методах `where()`, `select()`, `order()` и `group()` можно использовать особые обозначения для доступа к столбцам из других таблиц. Explorer автоматически создаст нужные JOIN. - -**Точечная запись** (`родительская_таблица.столбец`) используется для связи 1:N с точки зрения дочерней таблицы: - -```php -$books = $explorer->table('book'); - -// Находит книги, имя автора которых начинается на 'Jon' -$books->where('author.name LIKE ?', 'Jon%'); - -// Сортирует книги по имени автора по убыванию -$books->order('author.name DESC'); - -// Выводит название книги и имя автора -$books->select('book.title, author.name'); -``` - -**Запись с двоеточием** (`:дочерняя_таблица.столбец`) используется для связи 1:N с точки зрения родительской таблицы: - -```php -$authors = $explorer->table('author'); - -// Находит авторов, которые написали книгу с 'PHP' в названии -$authors->where(':book.title LIKE ?', '%PHP%'); - -// Подсчитывает количество книг у каждого автора -$authors->select('*, COUNT(:book.id) AS book_count') - ->group('author.id'); -``` - -В примере выше с записью через двоеточие (`:book.title`) столбец внешнего ключа не указан. Explorer автоматически определит верный столбец по имени родительской таблицы. В данном случае он соединяет через столбец `book.author_id`, потому что имя исходной таблицы - `author`. Если возможных соединений несколько, Explorer выбросит [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -Связующий столбец можно явно указать в скобках: - -```php -// Находит авторов, которые перевели книгу с 'PHP' в названии -$authors->where(':book(translator_id).title LIKE ?', '%PHP%'); -``` - -Записи можно объединять в цепочку и обращаться к данным через несколько таблиц: - -```php -// Находит авторов книг, помеченных тегом 'PHP' -$authors->where(':book:book_tag.tag.name', 'PHP') - ->group('author.id'); -``` - - -Расширение условий для JOIN ---------------------------- - -Метод `joinWhere()` расширяет условия, задаваемые при соединении таблиц в SQL после ключевого слова `ON`. - -Допустим, мы хотим найти книги, переведённые определённым переводчиком: - -```php -// Находит книги, переведённые переводчиком по имени 'David' -$books = $explorer->table('book') - ->joinWhere('translator', 'translator.name', 'David'); -// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') -``` - -В условии `joinWhere()` можно использовать те же конструкции, что и в методе `where()`: операторы, подстановки, массивы значений или SQL-выражения. - -Для более сложных запросов с несколькими JOIN можно задать псевдонимы таблиц: - -```php -$tags = $explorer->table('tag') - ->joinWhere(':book_tag.book.author', 'book_author.born < ?', 1950) - ->alias(':book_tag.book.author', 'book_author'); -// LEFT JOIN `book_tag` ON `tag`.`id` = `book_tag`.`tag_id` -// LEFT JOIN `book` ON `book_tag`.`book_id` = `book`.`id` -// LEFT JOIN `author` `book_author` ON `book`.`author_id` = `book_author`.`id` -// AND (`book_author`.`born` < 1950) -``` - -Обратите внимание, что если метод `where()` добавляет условия в конструкцию `WHERE`, то метод `joinWhere()` расширяет условия в конструкции `ON` при соединении таблиц. diff --git a/database/ru/guide.texy b/database/ru/guide.texy deleted file mode 100644 index b6d7567f79..0000000000 --- a/database/ru/guide.texy +++ /dev/null @@ -1,218 +0,0 @@ -Nette Database -************** - -.[perex] -Nette Database - мощный и изящный слой работы с базой данных для PHP, нацеленный на простоту и продуманные возможности. Он предлагает два способа работы с базой: [Explorer |explorer] для быстрой разработки приложений или [SQL-подход |SQL way] для прямой работы с запросами. - -<div class="grid gap-3"> -<div> - - -[SQL-подход|sql-way] -==================== -- Безопасные параметризованные запросы -- Точный контроль над структурой SQL-запроса -- Когда нужно писать сложные запросы с продвинутыми функциями -- Оптимизация производительности через конкретные функции SQL - -</div> - -<div> - - -[Explorer |explorer] -==================== -- Быстрая разработка без написания SQL -- Интуитивная работа со связями между таблицами -- Выигрыш от автоматической оптимизации запросов -- Подходит для быстрой и удобной работы с базой данных - -</div> - -</div> - - -Установка -========= - -Скачайте и установите библиотеку с помощью [Composer|best-practices:composer]: - -```shell -composer require nette/database -``` - - -Поддерживаемые базы данных -========================== - -Nette Database поддерживает следующие базы данных: - -|* Сервер базы данных |* Имя DSN |* Поддержка Explorer -|-----------------------|--------------|-----------------------| -| MySQL (>= 5.1) | mysql | ДА | -| PostgreSQL (>= 9.0) | pgsql | ДА | -| SQLite 3 (>= 3.8) | sqlite | ДА | -| Oracle | oci | НЕТ | -| MS SQL (PDO_SQLSRV) | sqlsrv | ДА | -| MS SQL (PDO_DBLIB) | mssql | НЕТ | -| ODBC | odbc | НЕТ | - - -Два подхода к работе с базой данных -=================================== - -Nette Database даёт вам выбор: вы можете либо писать SQL-запросы напрямую (SQL-подход), либо позволить порождать их автоматически (Explorer). Посмотрим, как оба подхода справляются с одними и теми же задачами: - -[SQL-подход|sql-way] - SQL-запросы - -```php -// Вставка записи -$database->query('INSERT INTO books', [ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// Получение записей: авторы книг -$result = $database->query(' - SELECT authors.*, COUNT(books.id) AS books_count - FROM authors - LEFT JOIN books ON authors.id = books.author_id - WHERE authors.active = 1 - GROUP BY authors.id -'); - -// Вывод (не оптимально, порождает N дополнительных запросов) -foreach ($result as $author) { - $books = $database->query(' - SELECT * FROM books - WHERE author_id = ? - ORDER BY published_at DESC - ', $author->id); - - echo "Author $author->name has written $author->books_count books:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -[Подход Explorer|explorer] - автоматическое порождение SQL - -```php -// Вставка записи -$database->table('books')->insert([ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// Получение записей: авторы книг -$authors = $database->table('authors') - ->where('active', 1); - -// Вывод (автоматически порождает всего 2 оптимизированных запроса) -foreach ($authors as $author) { - $books = $author->related('books') - ->order('published_at DESC'); - - echo "Author $author->name has written {$books->count()} books:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -Подход Explorer порождает и оптимизирует SQL-запросы автоматически. В примере выше SQL-подход порождает N+1 запросов (один для авторов и затем по одному для книг каждого автора), тогда как Explorer автоматически оптимизирует запросы и выполняет только два: один для авторов и один для всех их книг. - -Оба подхода можно свободно сочетать в приложении по мере надобности. - - -Соединение и настройка -====================== - -Чтобы подключиться к базе данных, достаточно создать экземпляр класса [api:Nette\Database\Connection]: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password); -``` - -Параметр `$dsn` (Data Source Name) такой же, [какой использует PDO |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], например `host=127.0.0.1;dbname=test`. В случае неудачи он выбрасывает `Nette\Database\ConnectionException`. - -Однако более удобный способ предлагает [конфигурация приложения |configuration], где достаточно добавить секцию `database`. Так создаются нужные объекты, а также панель базы данных в панели [Tracy |tracy:]. - -```neon -database: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password -``` - -Затем объект соединения можно [получить как сервис из DI-контейнера |dependency-injection:passing-dependencies], например: - -```php -class Model -{ - public function __construct( - // или Nette\Database\Explorer - private Nette\Database\Connection $database, - ) { - } -} -``` - -Подробнее о [конфигурации базы данных|configuration]. - - -Создание Explorer вручную -------------------------- - -Если вы не используете DI-контейнер Nette, вы можете создать экземпляр `Nette\Database\Explorer` вручную: - -```php -// соединение с базой данных -$connection = new Nette\Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); -// хранилище кеша, реализующее Nette\Caching\Storage, например: -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); -// занимается рефлексией структуры базы данных -$structure = new Nette\Database\Structure($connection, $storage); -// задаёт правила отображения имён таблиц, столбцов и внешних ключей -$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); -$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage); -``` - - -Управление соединением -====================== - -При создании объекта `Connection` соединение устанавливается автоматически. Если вы хотите отложить соединение, используйте ленивый режим: включите его в [конфигурации|configuration] параметром `lazy` или так: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => true]); -``` - -Для управления соединением служат методы `connect()`, `disconnect()` и `reconnect()`. -- `connect()` создаёт соединение, если его ещё нет, и может выбросить `Nette\Database\ConnectionException`. -- `disconnect()` разрывает текущее соединение с базой данных. -- `reconnect()` выполняет разрыв и последующее повторное подключение к базе данных. Этот метод тоже может выбросить `Nette\Database\ConnectionException`. - -Кроме того, вы можете следить за событиями, связанными с соединением, через событие `onConnect`, которое представляет собой массив callback-функций, вызываемых после установки соединения с базой данных. - -```php -// выполняется после подключения к базе данных -$database->onConnect[] = function($database) { - echo "Connected to the database"; -}; -``` - -Похожим образом работает событие `onQuery`: это массив callback-функций, вызываемых после каждого выполненного запроса (и когда запрос завершается ошибкой), что удобно для логирования или профилирования. - - -Панель отладки Tracy -==================== - -Если вы используете [Tracy |tracy:], панель Database в Debug Bar включается автоматически. Она показывает все выполненные запросы, их параметры, время выполнения и место в коде, откуда они были вызваны. - -[* db-panel.webp *] diff --git a/database/ru/reflection.texy b/database/ru/reflection.texy deleted file mode 100644 index efdc70b4e5..0000000000 --- a/database/ru/reflection.texy +++ /dev/null @@ -1,127 +0,0 @@ -Рефлексия структуры -******************* - -.{data-version:3.2.1} -Nette Database предоставляет инструменты для исследования структуры базы данных через класс [api:Nette\Database\Reflection]. Он позволяет получать сведения о таблицах, столбцах, индексах и внешних ключах. Рефлексию можно использовать для порождения схем, создания гибких приложений, работающих с базой данных, или для общих инструментов работы с базами. - -Объект рефлексии получают из экземпляра соединения с базой данных: - -```php -$reflection = $database->getReflection(); -``` - - -Получение таблиц ----------------- - -Свойство только для чтения `$reflection->tables` содержит ассоциативный массив всех таблиц базы данных: - -```php -// Вывод имён всех таблиц -foreach ($reflection->tables as $name => $table) { - echo $name . "\n"; -} -``` - -Доступны ещё два метода: - -```php -// Проверка существования таблицы -if ($reflection->hasTable('users')) { - echo "Table users exists"; -} - -// Возвращает объект таблицы; выбрасывает исключение, если её нет -$table = $reflection->getTable('users'); -``` - - -Сведения о таблице ------------------- - -Таблица представлена объектом [Table|api:Nette\Database\Reflection\Table], который предоставляет следующие свойства только для чтения: - -- `$name: string` - имя таблицы -- `$view: bool` - является ли она представлением -- `$fullName: ?string` - полное имя таблицы вместе со схемой (если есть) -- `$columns: array<string, Column>` - ассоциативный массив столбцов таблицы -- `$indexes: Index[]` - массив индексов таблицы -- `$primaryKey: ?Index` - первичный ключ таблицы или null -- `$foreignKeys: ForeignKey[]` - массив внешних ключей таблицы -- `$comment: ?string` - комментарий таблицы - - -Столбцы -------- - -Свойство таблицы `columns` предоставляет ассоциативный массив столбцов, где ключом служит имя столбца, а значением - экземпляр [Column|api:Nette\Database\Reflection\Column] с такими свойствами: - -- `$name: string` - имя столбца -- `$table: ?Table` - ссылка на таблицу столбца -- `$nativeType: string` - нативный тип базы данных -- `$size: ?int` - размер или длина типа -- `$nullable: bool` - может ли столбец содержать NULL -- `$default: mixed` - значение столбца по умолчанию -- `$autoIncrement: bool` - является ли столбец автоинкрементным -- `$primary: bool` - входит ли он в первичный ключ -- `$vendor: array` - дополнительные метаданные, специфичные для данной СУБД -- `$comment: ?string` - комментарий столбца - -```php -foreach ($table->columns as $name => $column) { - echo "Column: $name\n"; - echo "Type: {$column->nativeType}\n"; - echo "Nullable: " . ($column->nullable ? 'Yes' : 'No') . "\n"; -} -``` - - -Индексы -------- - -Свойство таблицы `indexes` предоставляет массив индексов, где каждый индекс - экземпляр [Index|api:Nette\Database\Reflection\Index] с такими свойствами: - -- `$columns: Column[]` - массив столбцов, образующих индекс -- `$unique: bool` - является ли индекс уникальным -- `$primary: bool` - является ли он первичным ключом -- `$name: ?string` - имя индекса - -Первичный ключ таблицы можно получить через свойство `primaryKey`, которое возвращает либо объект `Index`, либо `null`, если у таблицы нет первичного ключа. - -```php -// Вывод индексов -foreach ($table->indexes as $index) { - $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); - echo "Index" . ($index->name ? " {$index->name}" : '') . ":\n"; - echo " Columns: $columns\n"; - echo " Unique: " . ($index->unique ? 'Yes' : 'No') . "\n"; -} - -// Вывод первичного ключа -if ($primaryKey = $table->primaryKey) { - $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); - echo "Primary Key: $columns\n"; -} -``` - - -Внешние ключи -------------- - -Свойство таблицы `foreignKeys` предоставляет массив внешних ключей, где каждый внешний ключ - экземпляр [ForeignKey|api:Nette\Database\Reflection\ForeignKey] с такими свойствами: - -- `$foreignTable: Table` - таблица, на которую он ссылается -- `$localColumns: Column[]` - массив локальных столбцов -- `$foreignColumns: Column[]` - массив столбцов, на которые идёт ссылка -- `$name: string` - имя внешнего ключа - -```php -// Вывод внешних ключей -foreach ($table->foreignKeys as $fk) { - $localCols = implode(', ', array_map(fn($col) => $col->name, $fk->localColumns)); - $foreignCols = implode(', ', array_map(fn($col) => $col->name, $fk->foreignColumns)); - - echo "FK" . ($fk->name ? " {$fk->name}" : '') . ":\n"; - echo " $localCols -> {$fk->foreignTable->name}($foreignCols)\n"; -} -``` diff --git a/database/ru/security.texy b/database/ru/security.texy deleted file mode 100644 index 226caa5aba..0000000000 --- a/database/ru/security.texy +++ /dev/null @@ -1,185 +0,0 @@ -Риски безопасности -****************** - -<div class=perex> - -Базы данных часто содержат конфиденциальные данные и позволяют выполнять опасные операции. Для безопасной работы с Nette Database принципиально важно: - -- Понимать разницу между безопасным и небезопасным API -- Использовать параметризованные запросы -- Правильно проверять входные данные - -</div> - - -Что такое SQL injection? -======================== - -SQL injection - самый серьёзный риск безопасности при работе с базами данных. Она возникает, когда неочищенный пользовательский ввод становится частью SQL-запроса. Злоумышленник может вставить собственные SQL-команды и тем самым: -- получить несанкционированный доступ к данным -- изменить или удалить данные в базе -- обойти аутентификацию - -```php -// ❌ ОПАСНЫЙ КОД - уязвим для SQL injection -$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); - -// Злоумышленник может ввести значение вроде: ' OR '1'='1 -// Получившийся запрос будет: SELECT * FROM users WHERE name = '' OR '1'='1' -// Он вернёт всех пользователей -``` - -То же относится и к Database Explorer: - -```php -// ❌ ОПАСНЫЙ КОД - уязвим для SQL injection -$table->where('name = ' . $_GET['name']); -$table->where("name = '$_GET[name]'"); -``` - - -Параметризованные запросы -========================= - -Основная защита от SQL injection - параметризованные запросы. Nette Database предлагает несколько способов их использовать. - -Проще всего использовать **подстановки в виде вопросительных знаков**: - -```php -// ✅ Безопасный параметризованный запрос -$database->query('SELECT * FROM users WHERE name = ?', $name); - -// ✅ Безопасное условие в Explorer -$table->where('name = ?', $name); -``` - -Это относится ко всем остальным методам [Database Explorer|explorer], позволяющим вставлять выражения с вопросительными знаками и параметрами. - -Для команд INSERT, UPDATE или для условия WHERE мы можем передать значения массивом: - -```php -// ✅ Безопасный INSERT -$database->query('INSERT INTO users', [ - 'name' => $name, - 'email' => $email, -]); - -// ✅ Безопасный INSERT в Explorer -$table->insert([ - 'name' => $name, - 'email' => $email, -]); -``` - - -Проверка значений параметров -============================ - -Параметризованные запросы - краеугольный камень безопасной работы с базой данных. Однако значения, которые мы в них вставляем, должны пройти несколько уровней проверок: - - -Проверка типа -------------- - -**Самое важное - обеспечить правильный тип данных параметров**: это необходимое условие безопасного использования Nette Database. База данных исходит из того, что все входные данные имеют правильный тип, соответствующий данному столбцу. - -Например, если бы `$name` в предыдущих примерах неожиданно оказалась массивом вместо строки, Nette Database попытался бы вставить все его элементы в SQL-запрос, что привело бы к ошибке. Поэтому **никогда не используйте** непроверенные данные из `$_GET`, `$_POST` или `$_COOKIE` напрямую в запросах к базе. - - -Проверка формата ----------------- - -На втором уровне мы проверяем формат данных: например, что строки в кодировке UTF-8 и их длина соответствует описанию столбца или что числовые значения находятся в допустимом диапазоне для типа данных столбца. - -На этом уровне проверки мы можем частично положиться на саму базу данных: многие базы отклонят некорректные данные. Однако поведение может различаться: некоторые молча обрежут длинные строки или числа вне диапазона. - - -Проверка, свойственная предметной области ------------------------------------------ - -Третий уровень - логические проверки, специфичные для вашего приложения. Например, проверка, что значения из выпадающих списков соответствуют предложенным вариантам, что числа находятся в ожидаемом диапазоне (например, возраст 0-150 лет) или что взаимные зависимости между значениями осмысленны. - - -Рекомендуемые способы проверки ------------------------------- - -- Используйте [Nette Forms|forms:], которые автоматически обеспечивают правильную проверку всех вводимых данных. -- Используйте [презентеры|application:] и указывайте типы данных параметров в методах `action*()` и `render*()`. -- Или реализуйте собственный слой проверки стандартными средствами PHP вроде `filter_var()`. - - -Безопасная работа со столбцами -============================== - -В предыдущем разделе мы показали, как правильно проверять значения параметров. Однако, используя массивы в SQL-запросах, мы должны с тем же вниманием отнестись и к их ключам. - -```php -// ❌ ОПАСНЫЙ КОД - ключи в массиве не очищены -$database->query('INSERT INTO users', $_POST); -``` - -Для команд INSERT и UPDATE это критическая брешь в безопасности: злоумышленник может вставить или изменить любой столбец базы данных. Он мог бы, например, задать `is_admin = 1` или вставить произвольные данные в конфиденциальные столбцы (так называемая Mass Assignment Vulnerability). - -В условиях WHERE это ещё опаснее, потому что они могут содержать операторы: - -```php -// ❌ ОПАСНЫЙ КОД - ключи в массиве не очищены -$_POST['salary >'] = 100000; -$database->query('SELECT * FROM users WHERE', $_POST); -// выполняет запрос WHERE (`salary` > 100000) -``` - -Злоумышленник может таким способом планомерно выяснить зарплаты сотрудников. Он может начать, например, с запроса о зарплатах выше 100 000, затем ниже 50 000 и, постепенно сужая диапазон, раскрыть примерные зарплаты всех сотрудников. Такой вид атаки называется SQL enumeration. - -Методы `where()` и `whereOr()` [ещё гораздо гибче |explorer#where()] и поддерживают SQL-выражения, включая операторы и функции, в ключах и значениях. Это даёт злоумышленнику возможность выполнить SQL injection: - -```php -// ❌ ОПАСНЫЙ КОД - злоумышленник может вставить собственный SQL -$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1']; -$table->where($_POST); -// выполняет запрос WHERE (0) UNION SELECT name, salary FROM users WHERE (1) -``` - -Эта атака завершает исходное условие через `0)`, добавляет собственный `SELECT` через `UNION`, чтобы получить конфиденциальные данные из таблицы `users`, и закрывает синтаксически корректный запрос через `WHERE (1)`. - - -Белый список столбцов ---------------------- - -Для безопасной работы с именами столбцов нам нужен механизм, гарантирующий, что пользователь может работать только с разрешёнными столбцами и не может добавить свои. Мы могли бы попытаться обнаруживать и блокировать опасные имена столбцов (чёрный список), но такой подход ненадёжен: злоумышленник всегда может придумать новый способ записать опасное имя столбца, который мы не предусмотрели. - -Поэтому гораздо безопаснее перевернуть логику и задать явный список разрешённых столбцов (белый список): - -```php -// Столбцы, которые пользователю разрешено менять -$allowedColumns = ['name', 'email', 'active']; - -// Убираем из ввода все неразрешённые столбцы -$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); - -// ✅ Теперь безопасно использовать в запросах, например: -$database->query('INSERT INTO users', $filteredData); -$table->update($filteredData); -$table->where($filteredData); -``` - - -Динамические идентификаторы -=========================== - -Для динамических имён таблиц и столбцов используйте подстановку `?name`. Она обеспечивает правильное экранирование идентификаторов по синтаксису данной базы данных (например, обратными кавычками в MySQL): - -```php -// ✅ Безопасное использование доверенных идентификаторов -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name', $column, $table); -// Результат в MySQL: SELECT `name` FROM `users` -``` - -Важно: используйте обозначение `?name` только для доверенных значений, заданных в коде приложения. Для значений от пользователя снова используйте [белый список |#Белый список столбцов]. Иначе вы подвергаете себя рискам безопасности: - -```php -// ❌ ОПАСНО - никогда не используйте пользовательский ввод -$database->query('SELECT ?name FROM users', $_GET['column']); -``` diff --git a/database/ru/sql-way.texy b/database/ru/sql-way.texy deleted file mode 100644 index 8e26e2e91e..0000000000 --- a/database/ru/sql-way.texy +++ /dev/null @@ -1,513 +0,0 @@ -SQL-подход -********** - -.[perex] -Nette Database предлагает два способа работы: вы можете писать SQL-запросы сами (SQL-подход) или позволить порождать их автоматически (см. [Explorer |explorer]). SQL-подход даёт вам полный контроль над запросами и при этом обеспечивает их безопасное построение. - -.[note] -Подробности о соединении с базой данных и его настройке можно найти в главе [Соединение и настройка |guide#Соединение и настройка]. - - -Основы запросов -=============== - -Для запросов к базе данных служит метод `query()`. Он возвращает объект [ResultSet |api:Nette\Database\ResultSet], представляющий результат запроса. Если запрос не удаётся, метод [выбрасывает исключение|exceptions]. Результат запроса можно обойти циклом `foreach` или воспользоваться одним из [вспомогательных методов |#Получение данных]. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; -} -``` - -Чтобы безопасно вставлять значения в SQL-запросы, используйте параметризованные запросы. Nette Database делает это исключительно просто: достаточно добавить после SQL-запроса запятую и значение: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -При нескольких параметрах у вас два варианта. Вы можете чередовать SQL-запрос и параметры: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age); -``` - -Или сначала написать весь SQL-запрос, а затем добавить все параметры: - -```php -$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); -``` - - -Защита от SQL injection -======================= - -Почему важно использовать параметризованные запросы? Потому что они защищают вас от атаки под названием SQL injection, при которой злоумышленник может подсунуть собственные SQL-команды и тем самым получить доступ к данным в базе или повредить их. - -.[warning] -**Никогда не вставляйте переменные прямо в SQL-запрос!** Всегда используйте параметризованные запросы, которые защищают вас от SQL injection. - -```php -// ❌ ОПАСНЫЙ КОД - уязвим для SQL injection -$database->query("SELECT * FROM users WHERE name = '$name'"); - -// ✅ Безопасный параметризованный запрос -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Ознакомьтесь с [возможными рисками безопасности |security]. - - -Приёмы построения запросов -========================== - - -Условия WHERE -------------- - -Условия `WHERE` можно записать ассоциативным массивом, где ключи - имена столбцов, а значения - данные для сравнения. Nette Database автоматически выбирает наиболее подходящий оператор SQL по типу значения. - -```php -$database->query('SELECT * FROM users WHERE', [ - 'name' => 'John', - 'active' => true, -]); -// WHERE `name` = 'John' AND `active` = 1 -``` - -Оператор сравнения можно указать в ключе и явно: - -```php -$database->query('SELECT * FROM users WHERE', [ - 'age >' => 25, // использует оператор > - 'name LIKE' => '%John%', // использует оператор LIKE - 'email NOT LIKE' => '%example.com%', // использует оператор NOT LIKE -]); -// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' -``` - -Nette автоматически обрабатывает особые случаи вроде значений `null` или массивов. - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name' => 'Laptop', // использует оператор = - 'category_id' => [1, 2, 3], // использует IN - 'description' => null, // использует IS NULL -]); -// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL -``` - -Для отрицательных условий используйте оператор `NOT`: - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // использует оператор != - 'category_id NOT' => [1, 2, 3], // использует NOT IN - 'description NOT' => null, // использует IS NOT NULL - 'id NOT' => [], // пропускается -]); -// WHERE `name` != 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL -``` - -По умолчанию условия объединяются оператором `AND`. Это можно изменить [подсказкой ?or |#Подсказки построения SQL]. - - -Правила ORDER BY ----------------- - -Конструкцию `ORDER BY` можно записать массивом. Столбцы указывайте в ключах, а логическим значением обозначайте порядок: по возрастанию (`true`) или по убыванию (`false`): - -```php -$database->query('SELECT id FROM author ORDER BY', [ - 'id' => true, // по возрастанию - 'name' => false, // по убыванию -]); -// SELECT id FROM author ORDER BY `id`, `name` DESC -``` - - -Вставка данных (INSERT) ------------------------ - -Для вставки записей служит команда SQL `INSERT`. - -```php -$values = [ - 'name' => 'John Doe', - 'email' => 'john@example.com', -]; -$database->query('INSERT INTO users ?', $values); -$userId = $database->getInsertId(); -``` - -Метод `getInsertId()` возвращает ID последней вставленной записи. Для некоторых баз данных (например, PostgreSQL) нужно указать параметром имя последовательности, из которой должен порождаться ID: `$database->getInsertId($sequenceId)`. - -Параметрами можно передавать и [особые значения |#Особые значения], такие как файлы, объекты DateTime или типы enum. - -Вставка нескольких записей сразу: - -```php -$database->query('INSERT INTO users ?', [ - ['name' => 'User 1', 'email' => 'user1@mail.com'], - ['name' => 'User 2', 'email' => 'user2@mail.com'], -]); -``` - -Многозаписный INSERT намного быстрее, потому что выполняется всего один запрос к базе вместо множества отдельных. - -**Замечание о безопасности:** никогда не используйте непроверенные данные в качестве `$values`. Ознакомьтесь с [возможными рисками |security#Безопасная работа со столбцами]. - - -Обновление данных (UPDATE) --------------------------- - -Для обновления записей служит команда SQL `UPDATE`. - -```php -// Обновление одной записи -$values = [ - 'name' => 'John Smith', -]; -$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); -``` - -Число затронутых записей возвращает `$result->getRowCount()`. - -Для `UPDATE` мы можем использовать операторы `+=` и `-=`: - -```php -$database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // увеличивает login_count -], 1); -``` - -Пример вставки или обновления записи, если она уже существует. Мы используем приём `ON DUPLICATE KEY UPDATE`: - -```php -$values = [ - 'name' => $name, - 'year' => $year, -]; -$database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', - $values + ['id' => $id], - $values, -); -// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978) -// ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 -``` - -Обратите внимание, что Nette Database распознаёт контекст, в котором в SQL-команде используется параметр-массив, и строит SQL-код соответственно. Так, из первого массива он построил `(id, name, year) VALUES (123, 'Jim', 1978)`, а второй преобразовал в вид `name = 'Jim', year = 1978`. Подробнее об этом в разделе [#Подсказки построения SQL]. - - -Удаление данных (DELETE) ------------------------- - -Для удаления записей служит команда SQL `DELETE`. Пример получения числа удалённых записей: - -```php -$count = $database->query('DELETE FROM users WHERE id = ?', 1) - ->getRowCount(); -``` - - -Подсказки построения SQL ------------------------- - -Подсказка - особая подстановка в SQL-запросе, задающая, как значение параметра нужно превратить в выражение SQL: - -| Подсказка | Описание | Автоматически используется в -|-----------|-------------------------------------------------|----------------------------- -| `?name` | Служит для вставки имён таблиц или столбцов | - -| `?values` | Порождает `(key, ...) VALUES (value, ...)` | `INSERT ... ?`, `REPLACE ... ?` -| `?set` | Порождает присваивания `key = value, ...` | `SET ?`, `KEY UPDATE ?` -| `?and` | Объединяет условия массива через `AND` | `WHERE ?`, `HAVING ?` -| `?or` | Объединяет условия массива через `OR` | - -| `?order` | Порождает конструкцию `ORDER BY` | `ORDER BY ?`, `GROUP BY ?` - -Подстановка `?name` служит для динамической вставки имён таблиц и столбцов в запрос. Nette Database заботится о правильном экранировании идентификаторов по соглашениям базы данных (например, о заключении в обратные кавычки в MySQL). - -```php -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); -// SELECT `name` FROM `users` WHERE id = 1 (в MySQL) -``` - -**Внимание:** используйте подстановку `?name` только для проверенных имён таблиц и столбцов. Иначе вы рискуете [уязвимостями безопасности |security#Динамические идентификаторы]. - -Остальные подсказки обычно указывать не нужно, потому что при построении SQL-запроса Nette использует умное автоопределение (см. третий столбец таблицы). Но вы можете применить их, например, в ситуации, когда хотите объединить условия через `OR` вместо `AND`: - -```php -$database->query('SELECT * FROM users WHERE ?or', [ - 'name' => 'John', - 'email' => 'john@example.com', -]); -// SELECT * FROM users WHERE `name` = 'John' OR `email` = 'john@example.com' -``` - - -Особые значения ---------------- - -Помимо обычных скалярных типов (string, int, bool) параметрами можно передавать и особые значения: - -- файлы: `fopen('image.gif', 'r')` вставляет двоичное содержимое файла -- дату и время: объекты `DateTimeInterface` преобразуются в формат базы данных -- типы enum: экземпляры `enum` преобразуются в своё значение -- литералы SQL: созданные через `Connection::literal('NOW()')` вставляются в запрос напрямую - -```php -$database->query('INSERT INTO articles ?', [ - 'title' => 'My Article', - 'published_at' => new DateTimeImmutable, // или new DateTime - 'content' => fopen('image.png', 'r'), - 'state' => Status::Draft, -]); -``` - -Для баз данных без нативной поддержки типа `datetime` (таких как SQLite и Oracle) объекты `DateTime` и `DateTimeImmutable` преобразуются в значение, заданное в [конфигурации базы данных|configuration] параметром `formatDateTime` (значение по умолчанию - `U`, то есть Unix-время). - - -Литералы SQL ------------- - -В некоторых случаях вам нужно передать значением сырой SQL-код, который не должен восприниматься как строка и экранироваться. Для этого служат объекты класса `Nette\Database\SqlLiteral`. Их создаёт метод `Connection::literal()`. - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - 'year >' => $database::literal('YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) -``` - -Как вариант: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) -``` - -Литералы SQL могут содержать параметры: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > ? AND year < ?', $min, $max), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) -``` - -Это позволяет строить интересные сочетания: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('?or', [ - 'active' => true, - 'role' => $role, - ]), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (`active` = 1 OR `role` = 'admin') -``` - - -Получение данных -================ - - -Сокращения для запросов SELECT ------------------------------- - -Чтобы упростить получение данных, `Connection` предлагает несколько сокращений, объединяющих вызов `query()` с последующим вызовом `fetch*()`. Эти методы принимают те же параметры, что и `query()`, то есть SQL-запрос и необязательные параметры. Полное описание методов `fetch*()` можно найти [ниже |#fetch()]. - -| `fetch($sql, ...$params): ?Row` | Выполняет запрос и возвращает первую запись как объект `Row` или `null`. -| `fetchAll($sql, ...$params): array` | Выполняет запрос и возвращает все записи как массив объектов `Row`. -| `fetchPairs($sql, ...$params): array` | Выполняет запрос и возвращает ассоциативный массив (пары ключ => значение). -| `fetchField($sql, ...$params): mixed` | Выполняет запрос и возвращает значение первого столбца первой записи. -| `fetchList($sql, ...$params): ?array` | Выполняет запрос и возвращает первую запись как индексированный массив или `null`. - -Пример: - -```php -// fetchField() - возвращает значение первой ячейки -$count = $database->query('SELECT COUNT(*) FROM articles') - ->fetchField(); -``` - - -`foreach` - обход записей -------------------------- - -После выполнения запроса возвращается объект [ResultSet|api:Nette\Database\ResultSet], позволяющий обходить результаты несколькими способами. Проще всего выполнить запрос и получить записи обходом в цикле `foreach`. Этот способ наиболее экономен по памяти, потому что получает данные запись за записью и не загружает весь результат в память сразу. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; - // ... -} -``` - -.[note] -`ResultSet` можно обойти только один раз. Если вам нужен повторный обход, сначала загрузите данные в массив, например методом `fetchAll()`. - - -fetch(): ?Row .[method] ------------------------ - -Возвращает запись как объект `Row`. Если записей больше нет, возвращает `null`. Сдвигает внутренний указатель на следующую запись. - -```php -$result = $database->query('SELECT * FROM users'); -$row = $result->fetch(); // загружает первую запись -if ($row) { - echo $row->name; -} -``` - - -fetchAll(): array .[method] ---------------------------- - -Возвращает все оставшиеся записи из `ResultSet` как массив объектов `Row`. - -```php -$result = $database->query('SELECT * FROM users'); -$rows = $result->fetchAll(); // загружает все записи -foreach ($rows as $row) { - echo $row->name; -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Возвращает результат как ассоциативный массив. Первый аргумент задаёт столбец, который используется как ключи, а второй - столбец, используемый как значения: - -```php -$result = $database->query('SELECT id, name FROM users'); -$names = $result->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Если задан только первый параметр (`$key`), значением будет вся запись (объект `Row`): - -```php -$rows = $result->fetchPairs('id'); -// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...] -``` - -При повторяющихся ключах используется значение из последней записи. Использование `null` в качестве ключа даёт массив с числовой индексацией (начиная с нуля), что предотвращает столкновения ключей: - -```php -$names = $result->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Как вариант, вы можете передать callback, обрабатывающий каждую запись. Callback может вернуть одно значение или пару ключ-значение. - -```php -$result = $database->query('SELECT * FROM users'); -$items = $result->fetchPairs(fn($row) => "$row->id - $row->name"); -// ['1 - John', '2 - Jane', ...] - -// Callback может вернуть и массив с парой ключ и значение: -$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); -// ['John' => 46, 'Jane' => 21, ...] -``` - - -fetchField(): mixed .[method] ------------------------------ - -Возвращает значение первого столбца текущей записи. Если записей больше нет, возвращает `null`. Сдвигает внутренний указатель на следующую запись. - -```php -$result = $database->query('SELECT name FROM users'); -$name = $result->fetchField(); // загружает name из первой записи -``` - - -fetchList(): ?array .[method] ------------------------------ - -Возвращает запись как индексированный массив. Если записей больше нет, возвращает `null`. Сдвигает внутренний указатель на следующую запись. - -```php -$result = $database->query('SELECT name, email FROM users'); -$row = $result->fetchList(); // ['John', 'john@example.com'] -``` - - -getRowCount(): ?int .[method] ------------------------------ - -Возвращает число затронутых записей от последнего запроса `UPDATE` или `DELETE`. Для запросов `SELECT` возвращает число записей в результате. Однако оно не всегда бывает известно, и в этом случае метод возвращает `null`. - - -getColumnCount(): ?int .[method] --------------------------------- - -Возвращает число столбцов в `ResultSet`. - - -Сведения о запросе -================== - -Ради отладки мы можем получить сведения о последнем выполненном запросе: - -```php -echo $database->getLastQueryString(); // выводит SQL-запрос - -$result = $database->query('SELECT * FROM articles'); -echo $result->getQueryString(); // выводит SQL-запрос -echo $result->getTime(); // выводит время выполнения в секундах -``` - -Чтобы вывести результат в виде HTML-таблицы, можно использовать: - -```php -$result = $database->query('SELECT * FROM articles'); -$result->dump(); -``` - -`ResultSet` предоставляет сведения о типах столбцов: - -```php -$result = $database->query('SELECT * FROM articles'); -$types = $result->getColumnTypes(); - -foreach ($types as $column => $type) { - echo "$column is of type $type"; // например, 'id is of type int' -} -``` - - -Логирование запросов --------------------- - -Мы можем реализовать собственное логирование запросов. Событие `onQuery` - массив callback-функций, вызываемых после каждого выполненного запроса: - -```php -$database->onQuery[] = function ($database, $result) use ($logger) { - $logger->info('Query: ' . $result->getQueryString()); - $logger->info('Time: ' . $result->getTime()); - - if ($result->getRowCount() > 1000) { - $logger->warning('Large result set: ' . $result->getRowCount() . ' rows'); - } -}; -``` diff --git a/database/ru/transactions.texy b/database/ru/transactions.texy deleted file mode 100644 index 7792cb35ac..0000000000 --- a/database/ru/transactions.texy +++ /dev/null @@ -1,45 +0,0 @@ -Транзакции -********** - -.[perex] -Транзакции гарантируют, что либо будут выполнены все операции внутри транзакции, либо ни одна. Они полезны для обеспечения согласованности данных при сложных операциях. - -Простейший способ использовать транзакции выглядит так: - -```php -$database->beginTransaction(); -try { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); - $database->commit(); -} catch (\Exception $e) { - $database->rollBack(); - throw $e; -} -``` - -Того же результата можно добиться куда изящнее методом `transaction()`. Он принимает callback, который выполняется внутри транзакции. Если callback отрабатывает без исключения, транзакция автоматически фиксируется. Если возникает исключение, транзакция откатывается, а исключение распространяется дальше. - -```php -$database->transaction(function ($database) use ($id) { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); -}); -``` - -Вызовы `transaction()` можно вкладывать друг в друга, благодаря чему легко составлять методы, каждый из которых управляет своей транзакцией. В базу данных как `BEGIN`/`COMMIT` на самом деле отправляется только самая внешняя транзакция; внутренние вызовы лишь отслеживают глубину вложенности. Вызов `beginTransaction()`, `commit()` или `rollBack()` вручную внутри callback метода `transaction()` выбрасывает `LogicException`. - -Метод `transaction()` может и возвращать значения: - -```php -$count = $database->transaction(function ($database) { - $result = $database->query('UPDATE users SET active = ?', true); - return $result->getRowCount(); // возвращает число обновлённых записей -}); -``` diff --git a/database/ru/type-conversion.texy b/database/ru/type-conversion.texy deleted file mode 100644 index 0865e740ac..0000000000 --- a/database/ru/type-conversion.texy +++ /dev/null @@ -1,55 +0,0 @@ -Преобразование типов -******************** - -.[perex] -Nette Database автоматически преобразует значения, возвращаемые из базы данных, в соответствующие типы PHP. - - -Дата и время ------------- - -Временные значения преобразуются в объекты `Nette\Utils\DateTime`. Если вы хотите, чтобы временные значения преобразовывались в неизменяемые объекты `Nette\Database\DateTime`, задайте в [конфигурации |configuration] параметру `newDateTime` значение true. - -```php -$row = $database->fetch('SELECT created_at FROM articles'); -echo $row->created_at instanceof DateTime; // true -echo $row->created_at->format('j. n. Y'); -``` - -В случае MySQL тип данных `TIME` преобразуется в объекты `DateInterval`. - - -Логические значения -------------------- - -Логические значения автоматически преобразуются в `true` или `false`. Для MySQL `TINYINT(1)` преобразуется, если мы зададим `convertBoolean` в [конфигурации |configuration]. - -```php -$row = $database->fetch('SELECT is_published FROM articles'); -echo gettype($row->is_published); // 'boolean' -``` - - -Числовые значения ------------------ - -Числовые значения преобразуются в `int` или `float` в соответствии с типом столбца в базе данных: - -```php -$row = $database->fetch('SELECT id, price FROM products'); -echo gettype($row->id); // integer -echo gettype($row->price); // float -``` - - -Собственная нормализация ------------------------- - -Методом `setRowNormalizer(?callable $normalizer)` вы можете задать собственную функцию преобразования записей из базы данных. Это полезно, например, для автоматического преобразования типов данных. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // здесь происходит преобразование типов - return $row; -}); -``` diff --git a/database/ru/upgrading.texy b/database/ru/upgrading.texy deleted file mode 100644 index 4b13b13831..0000000000 --- a/database/ru/upgrading.texy +++ /dev/null @@ -1,36 +0,0 @@ -Обновление -********** - - -Обновление до версии 3.2 -======================== - -Минимальная требуемая версия PHP - 8.1. - -Код тщательно подогнан под PHP 8.1. Добавлены все новые объявления типов методов и свойств. Изменения незначительны: - -- MySQL: нулевая дата `0000-00-00` возвращается как `null` -- MySQL: decimal без знаков после запятой возвращается как int, а не float -- Тип `time` возвращается объектом `DateTime` с датой `0001-01-01` вместо текущей даты - - -Обновление до версии 3.1 -======================== - -- класс `Nette\Database\Context` переименован в `Nette\Database\Explorer` ради согласованности с названием [Database Explorer|explorer] -- интерфейсы `Nette\Database\IRow` и `Nette\Database\IRowContainer` помечены как устаревшие за ненадобностью -- драйвер `MySqlDriver` использует подзапросы -- переводчик SQL-запросов лучше контролирует, где можно передавать массивы - - -Обновление до версии 3.0 -======================== - -Некоторые методы, такие как `fetch()` или `fetchField()`, теперь возвращают `null` вместо `false`, когда следующей записи нет. - - -Обновление до версии 2.3 -======================== - -- `MySqlDriver` по умолчанию использует для MySQL >= 5.5.3 кодировку `utf8mb4` вместо `utf8` -- `IReflection` разделён на парные интерфейсы `IStructure` и `IConventions` diff --git a/database/tr/@home.texy b/database/tr/@home.texy deleted file mode 100644 index 43782badfb..0000000000 --- a/database/tr/@home.texy +++ /dev/null @@ -1,18 +0,0 @@ -Desteklenen Veritabanları -========================= - -Şu veritabanı sunucuları desteklenir: - -|* Veritabanı sunucusu |* DSN adı |* Core desteği |* Explorer desteği -| MySQL (>= 5.1) | mysql | EVET | EVET -| PostgreSQL (>= 9.0) | pgsql | EVET | EVET -| Sqlite 3 (>= 3.8) | sqlite | EVET | EVET -| Oracle | oci | EVET | - -| MS SQL (PDO_SQLSRV) | sqlsrv | EVET | EVET -| MS SQL (PDO_DBLIB) | mssql | EVET | - -| ODBC | odbc | EVET | - - - - -{{maintitle: Nette Database - PHP için harika veritabanı katmanı}} -{{description: Nette Database, SQL sorguları yazmadan veritabanından veri almayı belirgin biçimde kolaylaştırır. Verimli sorgular çalıştırır ve gereksiz veri taşımaz.}} diff --git a/database/tr/@left-menu.texy b/database/tr/@left-menu.texy deleted file mode 100644 index afd037e071..0000000000 --- a/database/tr/@left-menu.texy +++ /dev/null @@ -1,21 +0,0 @@ -Nette Database -************** -- [Başlangıç |guide] -- [SQL yolu|sql-way] -- [Explorer|explorer] -- [Transaction'lar|transactions] -- [İstisnalar|exceptions] -- [Reflection|reflection] -- [Tür dönüşümü |type-conversion] -- [Yapılandırma|configuration] -- [Güvenlik riskleri |security] -- [Yükseltme|upgrading] - - -Daha Fazla Okuma -**************** -- [Nette dokümantasyonu |nette:] -- [Nette Application |application:how-it-works] -- [Yardımcı araçlar |utils:] -- [En iyi uygulamalar |best-practices:] -- [Sorun giderme |nette:troubleshooting] diff --git a/database/tr/@meta.texy b/database/tr/@meta.texy deleted file mode 100644 index 8dfe82f311..0000000000 --- a/database/tr/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokümantasyonu}} diff --git a/database/tr/configuration.texy b/database/tr/configuration.texy deleted file mode 100644 index 85005e3216..0000000000 --- a/database/tr/configuration.texy +++ /dev/null @@ -1,110 +0,0 @@ -Veritabanı Yapılandırması -************************* - -.[perex] -Nette Database'in yapılandırma seçeneklerine genel bakış. - -Framework'ün tamamını değil yalnızca bu kütüphaneyi kullanıyorsanız, [yapılandırmanın nasıl yükleneceğini|bootstrap:] okuyun. - - -Tek Bağlantı ------------- - -Tek bir veritabanı bağlantısını yapılandırın: - -```neon -database: - # DSN, tek zorunlu anahtar - dsn: "sqlite:%appDir%/Model/demo.db" - user: ... - password: ... -``` - -Bu, genellikle [autowiring |dependency-injection:autowiring] ile ya da [adlarına |#DI Servisleri] başvurarak aktarılan `Nette\Database\Connection` ve `Nette\Database\Explorer` servislerini oluşturur. - -Diğer ayarlar: - -```neon -database: - # veritabanı paneli Tracy Bar'da gösterilsin mi? - debugger: ... # (bool) Tracy etkinse varsayılan olarak açık - - # sorgunun EXPLAIN çıktısı Tracy Bar'da gösterilsin mi? - explain: ... # (bool) varsayılan true - - # bu bağlantı için autowiring açık olsun mu? - autowired: ... # (bool) ilk bağlantı için varsayılan true - - # tablo uzlaşımları: discovered, static ya da sınıf adı - conventions: discovered # (string) varsayılan 'discovered' - - options: - # veritabanına yalnızca gerektiğinde bağlanılsın mı? - lazy: ... # (bool) varsayılan false - - # PHP veritabanı sürücüsü sınıfı - driverClass: # (string) - - # yalnızca MySQL: sql_mode ayarlar - sqlmode: # (string) - - # yalnızca MySQL: SET NAMES ayarlar - charset: # (string) varsayılan 'utf8mb4' - - # yalnızca MySQL: TINYINT(1) değerini bool'a dönüştürür - convertBoolean: # (bool) varsayılan false - - # tarih sütunlarını değişmez nesneler olarak döndürür (3.2.1 sürümünden beri) - newDateTime: # (bool) varsayılan false - - # yalnızca Oracle ve SQLite: tarihi saklama biçimi - formatDateTime: # (string) varsayılan 'U' -``` - -`options` anahtarı, [PDO sürücü belgelerinde |https://www.php.net/manual/en/pdo.drivers.php] bulunan başka seçenekleri de içerebilir, örneğin: - -```neon -database: - options: - PDO::MYSQL_ATTR_COMPRESS: true -``` - - -Birden Çok Bağlantı -------------------- - -Yapılandırmada, adlandırılmış bölümlere ayırarak birden çok veritabanı bağlantısı tanımlayabiliriz: - -```neon -database: - main: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password - - another: - dsn: 'sqlite::memory:' -``` - -Autowiring yalnızca ilk bölümdeki servisler için açıktır. Bu, `autowired: false` ya da `autowired: true` ile değiştirilebilir. - - -DI Servisleri -------------- - -DI container'a şu servisler eklenir; `###` bağlantının adını temsil eder: - -| Ad | Tür | Açıklama -|---------------------------|---------------------------------|--------------------------- -| `database.###.connection` | [api:Nette\Database\Connection] | veritabanı bağlantısı -| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |explorer] - - -Yalnızca bir bağlantı tanımlarsak servis adları `database.default.connection` ve `database.default.explorer` olur. Yukarıdaki örnekteki gibi birden çok bağlantı tanımlarsak adlar bölümlere karşılık gelir; yani `database.main.connection`, `database.main.explorer`, ayrıca `database.another.connection` ve `database.another.explorer`. - -Autowiring'e girmeyen servisleri, adlarına başvurarak açıkça aktarırız: - -```neon -services: - - UserFacade(@database.another.connection) -``` diff --git a/database/tr/exceptions.texy b/database/tr/exceptions.texy deleted file mode 100644 index 01d97a0ecb..0000000000 --- a/database/tr/exceptions.texy +++ /dev/null @@ -1,37 +0,0 @@ -İstisnalar -********** - -Nette Database bir istisna hiyerarşisi kullanır. Temel sınıf `Nette\Database\DriverException` olup `PDOException` sınıfını genişletir ve veritabanı hatalarıyla çalışmak için gelişmiş işlevler sunar: - -- `getDriverCode()` metodu, veritabanı sürücüsünden gelen hata kodunu döndürür. -- `getSqlState()` metodu, SQLSTATE kodunu döndürür. -- `getQueryString()` ve `getParameters()` metotları, özgün sorguyu ve parametrelerini almayı sağlar. - -`DriverException` sınıfı şu özelleşmiş istisnalarla genişletilir: - -- `ConnectionException` - veritabanı sunucusuna bağlanılamadığını gösterir. - - `ConnectionLostException` .{data-version:3.2.9} - bağlantı bir işlem sırasında koptu (sunucu yeniden başlatma, ağ arızası, idle timeout); yeniden kullanmadan önce yeniden bağlanmak gerekir. -- `ConstraintViolationException` - veritabanı kısıt ihlallerinin temel sınıfı; şu istisnalar ondan türer: - - `ForeignKeyConstraintViolationException` - yabancı anahtar kısıtının ihlali. - - `NotNullConstraintViolationException` - NOT NULL kısıtının ihlali. - - `UniqueConstraintViolationException` - benzersizlik kısıtının ihlali. - - `CheckConstraintViolationException` .{data-version:3.2.9} - CHECK kısıtının ihlali. -- `DeadlockException` .{data-version:3.2.9} - sunucunun saptadığı bir deadlock ya da serileştirme çakışması; transaction geri alındı ve yeniden denenebilir. -- `LockTimeoutException` .{data-version:3.2.9} - kilit bekleme süresi aşıldı; deyim iptal edildi, ama onu çevreleyen transaction genellikle açık kalır. - -Aşağıdaki örnek, veritabanında zaten var olan bir e-postayla kullanıcı eklemeye çalışıldığında oluşan `UniqueConstraintViolationException` istisnasının nasıl yakalanacağını gösterir (`email` sütununda benzersiz bir indeks olduğu varsayılır): - -```php -try { - $database->query('INSERT INTO users', [ - 'email' => 'john@example.com', - 'name' => 'John Doe', - 'password' => $hashedPassword, - ]); -} catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'Bu e-postayla bir kullanıcı zaten var.'; - -} catch (Nette\Database\DriverException $e) { - echo 'Kayıt sırasında bir hata oluştu: ' . $e->getMessage(); -} -``` diff --git a/database/tr/explorer.texy b/database/tr/explorer.texy deleted file mode 100644 index 506e757bf5..0000000000 --- a/database/tr/explorer.texy +++ /dev/null @@ -1,912 +0,0 @@ -Database Explorer -***************** - -<div class=perex> - -Explorer, veritabanınızla çalışmanın sezgisel ve verimli bir yolunu sunar. Tablo ilişkilerini kendiliğinden ele alır ve sorguları iyileştirir; böylece siz uygulama mantığınıza odaklanabilirsiniz. Yapılandırma olmadan hemen çalışır. SQL sorguları üzerinde tam denetim isterseniz [SQL yolunu |SQL way] kullanabilirsiniz. - -- Verilerle çalışmak doğal ve anlaşılması kolaydır -- Yalnızca gereken veriyi getiren iyileştirilmiş SQL sorguları üretir -- JOIN sorguları yazmadan ilişkili verilere kolay erişim sağlar -- Hiçbir yapılandırma ya da varlık üretimi olmadan hemen çalışır - -</div> - - -Explorer ile çalışmak, [api:Nette\Database\Explorer] nesnesinde `table()` metodunu çağırmakla başlar (veritabanı bağlantısını kurmanın ayrıntıları için bkz. [Bağlantı ve yapılandırma |guide#Bağlantı ve Yapılandırma]): - -```php -$books = $explorer->table('book'); // 'book' tablonun adıdır -``` - -Metot, bir SQL sorgusunu temsil eden bir [Selection |api:Nette\Database\Table\Selection] nesnesi döndürür. Sonuçları filtrelemek ve sıralamak için bu nesneye başka metotlar zincirlenebilir. Sorgu, veri istendiğinde (örneğin `foreach` ile dolaşıldığında) kurulur ve çalıştırılır. Her satır bir [ActiveRow |api:Nette\Database\Table\ActiveRow] nesnesiyle temsil edilir: - -```php -foreach ($books as $book) { - echo $book->title; // 'title' sütununu çıktılar - echo $book->author_id; // 'author_id' sütununu çıktılar -} -``` - -Explorer, [tablo ilişkileriyle |#Tablolar Arası İlişkiler] çalışmayı büyük ölçüde kolaylaştırır. Aşağıdaki örnek, ilişkili tablolardan (kitaplar ve yazarları) veriyi ne kadar kolay çıktılayabildiğimizi gösteriyor. Hiçbir JOIN sorgusu yazmaya gerek olmadığına dikkat edin; onları Nette bizim için üretir: - -```php -$books = $explorer->table('book'); - -foreach ($books as $book) { - echo 'Kitap: ' . $book->title; - echo 'Yazar: ' . $book->author->name; // 'author' tablosuna bir JOIN oluşturur -} -``` - -Nette Database Explorer, sorguları en yüksek verim için iyileştirir. Yukarıdaki örnek, 10 ya da 10.000 kitap işlememizden bağımsız olarak yalnızca iki SELECT sorgusu çalıştırır. - -Ayrıca Explorer, kodda hangi sütunların kullanıldığını izler ve veritabanından yalnızca onları getirir; bu da başarımdan daha fazla kazandırır. Bu davranış tümüyle otomatik ve uyarlanabilirdir. Kodu sonradan başka sütunları kullanacak şekilde değiştirirseniz, Explorer sorguları kendiliğinden ayarlar. Hiçbir şey yapılandırmanıza ya da hangi sütunların gerekeceğini düşünmenize gerek yok; bunu Nette'ye bırakın. - - -Filtreleme ve Sıralama -====================== - -`Selection` sınıfı, veri seçimlerini filtrelemek ve sıralamak için metotlar sunar. - -.[language-php] -| `where($condition, ...$params)` | Bir WHERE koşulu ekler. Birden çok koşul AND ile birleştirilir | -| `whereOr(array $conditions)` | OR ile birleştirilen bir WHERE koşulları grubu ekler | -| `wherePrimary($value)` | Birincil anahtara dayalı bir WHERE koşulu ekler | -| `order($columns, ...$params)` | ORDER BY ile sıralamayı ayarlar | -| `select($columns, ...$params)` | Hangi sütunların getirileceğini belirtir | -| `limit($limit, $offset = null)` | Satır sayısını sınırlar (LIMIT) ve isteğe bağlı OFFSET ayarlar | -| `page($page, $itemsPerPage, &$numOfPages = null)` | Sayfalamayı ayarlar | -| `group($columns, ...$params)` | Satırları gruplar (GROUP BY) | -| `having($condition, ...$params)`| Gruplanmış satırları filtrelemek için HAVING koşulu ekler | - -Metotlar zincirlenebilir (buna [akıcı arayüz |nette:introduction-to-object-oriented-programming#Akıcı Arayüzler] denir): `$table->where(...)->order(...)->limit(...)`. - -Bu metotlarda, [ilişkili tablolardan gelen verilere |#İlişkili Tablolar Üzerinden Sorgulama] erişmek için özel yazımlar da kullanabilirsiniz. - - -Kaçışlama ve Tanımlayıcılar ---------------------------- - -Metotlar, parametreleri otomatik kaçışlar ve tanımlayıcıları (tablo ve sütun adlarını) tırnaklar; böylece SQL injection'ı önler. Düzgün çalışmasını sağlamak için birkaç kurala uyulmalıdır: - -- Anahtar sözcükleri, fonksiyon ve prosedür adlarını vb. **büyük harfle** yazın. -- Sütun ve tablo adlarını **küçük harfle** yazın. -- Dizeleri her zaman **parametrelerle** aktarın. - -```php -where('name = ' . $name); // KRİTİK AÇIK: SQL injection -where('name LIKE "%search%"'); // YANLIŞ: otomatik tırnaklamayı zorlaştırır -where('name LIKE ?', '%search%'); // DOĞRU: değer parametre olarak aktarılır - -where('name like ?', $name); // YANLIŞ: şunu üretir: `name` `like` ? -where('name LIKE ?', $name); // DOĞRU: şunu üretir: `name` LIKE ? -where('LOWER(name) = ?', $value);// DOĞRU: LOWER(`name`) = ? -``` - - -where(string|array $condition, ...$parameters): static .[method] ----------------------------------------------------------------- - -Sonuçları WHERE koşullarıyla filtreler. Gücü, çeşitli değer türlerini akıllıca ele almasında ve uygun SQL operatörlerini otomatik seçmesindedir. - -Temel kullanım: - -```php -$table->where('id', $value); // WHERE `id` = 123 -$table->where('id > ?', $value); // WHERE `id` > 123 -$table->where('id = ? OR name = ?', $id, $name); // WHERE `id` = 1 OR `name` = 'Jon Snow' -``` - -Uygun operatörlerin otomatik saptanması sayesinde çeşitli özel durumları ele almanız gerekmez; onları Nette çözer: - -```php -$table->where('id', 1); // WHERE `id` = 1 -$table->where('id', null); // WHERE `id` IS NULL -$table->where('id', [1, 2, 3]); // WHERE `id` IN (1, 2, 3) -// Operatörsüz ? yer tutucusunu da kullanabilirsiniz: -$table->where('id ?', 1); // WHERE `id` = 1 -``` - -Metot, olumsuz koşulları ve boş dizileri doğru şekilde ele alır: - -```php -$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- hiçbir şey bulmaz -$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- her şeyi bulur -$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- her şeyi bulur -// $table->where('NOT id ?', $ids); // UYARI: Bu söz dizimi desteklenmiyor -``` - -Parametre olarak başka bir tablo sorgusunun sonucunu da aktarabilir, böylece bir alt sorgu oluşturabilirsiniz: - -```php -// WHERE `id` IN (SELECT `id` FROM `tableName`) -$table->where('id', $explorer->table($tableName)); - -// WHERE `id` IN (SELECT `col` FROM `tableName`) -$table->where('id', $explorer->table($tableName)->select('col')); -``` - -Koşullar, öğeleri AND ile birleştirilen bir dizi olarak da aktarılabilir: - -```php -// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) -$table->where([ - 'price_final < price_original', - 'stock_count > min_stock', -]); -``` - -Dizide anahtar => değer çiftleri kullanabilirsiniz; Nette yine doğru operatörleri otomatik seçer: - -```php -// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3)) -$table->where([ - 'status' => 'active', - 'id' => [1, 2, 3], -]); -``` - -Dizide SQL ifadelerini yer tutucular ve birden çok parametreyle birleştirebilirsiniz. Bu, operatörleri kesin biçimde belirlenmiş karmaşık koşullar için uygundur: - -```php -// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) -$table->where([ - 'age > ?' => 18, - 'ROUND(score, ?) > ?' => [2, 75.5], // iki parametre dizi olarak aktarılır -]); -``` - -`where()` metodunun birden çok çağrısı, koşulları otomatik olarak AND ile birleştirir. - - -whereOr(array $parameters): static .[method] --------------------------------------------- - -`where()` metoduna benzer şekilde koşul ekler, ama onları OR ile birleştirir: - -```php -// WHERE (`status` = 'active') OR (`deleted` = 1) -$table->whereOr([ - 'status' => 'active', - 'deleted' => true, -]); -``` - -Burada daha karmaşık ifadeler de kullanılabilir: - -```php -// WHERE (`price` > 1000) OR (`price_with_tax` > 1500) -$table->whereOr([ - 'price > ?' => 1000, - 'price_with_tax > ?' => 1500, -]); -``` - - -wherePrimary(mixed $key): static .[method] ------------------------------------------- - -Tablonun birincil anahtarı için bir koşul ekler: - -```php -// WHERE `id` = 123 -$table->wherePrimary(123); - -// WHERE `id` IN (1, 2, 3) -$table->wherePrimary([1, 2, 3]); -``` - -Tablonun bileşik bir birincil anahtarı varsa (örneğin `foo_id`, `bar_id`), onu dizi olarak verin: - -```php -// WHERE `foo_id` = 1 AND `bar_id` = 5 -$table->wherePrimary(['foo_id' => 1, 'bar_id' => 5])->fetch(); - -// WHERE (`foo_id`, `bar_id`) IN ((1, 5), (2, 3)) -$table->wherePrimary([ - ['foo_id' => 1, 'bar_id' => 5], - ['foo_id' => 2, 'bar_id' => 3], -])->fetchAll(); -``` - - -order(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Satırların hangi sırayla döndürüleceğini belirtir. Bir ya da daha çok sütuna göre, artan ya da azalan sırada veya özel bir ifadeye göre sıralayabilirsiniz: - -```php -$table->order('created'); // ORDER BY `created` -$table->order('created DESC'); // ORDER BY `created` DESC -$table->order('priority DESC, created'); // ORDER BY `priority` DESC, `created` -$table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC -``` - - -select(string $columns, ...$parameters): static .[method] ---------------------------------------------------------- - -Veritabanından döndürülecek sütunları belirtir. Nette Database Explorer varsayılan olarak yalnızca kodda gerçekten kullanılan sütunları döndürür. Belirli ifadeleri almanız gerektiğinde `select()` metodunu kullanın: - -```php -// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` -$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); -``` - -`AS` ile tanımlanan takma adlara sonra `ActiveRow` nesnesinin özellikleri olarak erişilir: - -```php -foreach ($table as $row) { - echo $row->formatted_date; // takma ada erişim -} -``` - - -limit(?int $limit, ?int $offset = null): static .[method] ---------------------------------------------------------- - -Döndürülen satır sayısını sınırlar (LIMIT) ve isteğe bağlı olarak bir kayma ayarlamaya izin verir: - -```php -$table->limit(10); // LIMIT 10 (ilk 10 satırı döndürür) -$table->limit(10, 20); // LIMIT 10 OFFSET 20 -``` - -Sayfalama için `page()` metodunu kullanmak daha uygundur. - - -page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] -------------------------------------------------------------------------- - -Sonuçların sayfalanmasını kolaylaştırır. Sayfa numarasını (1'den başlar) ve sayfa başına öğe sayısını alır. İsteğe bağlı olarak, toplam sayfa sayısının saklanacağı bir değişkene referans verebilirsiniz: - -```php -$numOfPages = null; -$table->page(page: 3, itemsPerPage: 10, numOfPages: $numOfPages); -echo "Total pages: $numOfPages"; -``` - - -group(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Satırları belirtilen sütunlara göre gruplar (GROUP BY). Genellikle toplama fonksiyonlarıyla birlikte kullanılır: - -```php -// Her kategorideki ürün sayısını sayar -$table->select('category_id, COUNT(*) AS count') - ->group('category_id'); -``` - - -having(string $having, ...$parameters): static .[method] --------------------------------------------------------- - -Gruplanmış satırları filtrelemek için bir koşul ayarlar (HAVING). `group()` metodu ve toplama fonksiyonlarıyla birlikte kullanılabilir: - -```php -// 100'den fazla ürünü olan kategorileri bulur -$table->select('category_id, COUNT(*) AS count') - ->group('category_id') - ->having('count > ?', 100); -``` - - -Veri Okuma -========== - -Veritabanından veri okumak için çeşitli kullanışlı metotlar vardır: - -.[language-php] -| `foreach ($table as $key => $row)` | Tüm satırları dolaşır; `$key` birincil anahtar değeri, `$row` bir ActiveRow nesnesidir | -| `$row = $table->get($key)` | Birincil anahtara göre tek bir satır döndürür | -| `$row = $table->fetch()` | Geçerli satırı döndürür ve işaretçiyi sonrakine ilerletir | -| `$array = $table->fetchPairs()` | Sonuçlardan ilişkisel bir dizi oluşturur | -| `$array = $table->fetchAll()` | Tüm satırları dizi olarak döndürür | -| `count($table)` | Selection nesnesindeki satır sayısını döndürür | - -[ActiveRow |api:Nette\Database\Table\ActiveRow] nesnesi salt okunurdur. Yani özelliklerinin değerlerini değiştiremezsiniz. Bu kısıtlama veri tutarlılığını güvence altına alır ve beklenmedik yan etkileri önler. Veri veritabanından yüklenir ve her türlü değişiklik açıkça ve denetimli biçimde yapılmalıdır. - - -`foreach` - Tüm Satırları Dolaşma ---------------------------------- - -Bir sorguyu çalıştırıp satırları almanın en kolay yolu, `foreach` döngüsüyle dolaşmaktır. SQL sorgusunu otomatik olarak çalıştırır. - -```php -$books = $explorer->table('book'); -foreach ($books as $key => $book) { - // $key birincil anahtar değeri, $book bir ActiveRow - echo "$book->title ({$book->author->name})"; -} -``` - - -get($key): ?ActiveRow .[method] -------------------------------- - -SQL sorgusunu çalıştırır ve satırı birincil anahtara göre döndürür; yoksa `null` döndürür. - -```php -$book = $explorer->table('book')->get(123); // ID'si 123 olan ActiveRow'u ya da null döndürür -if ($book) { - echo $book->title; -} -``` - - -fetch(): ?ActiveRow .[method] ------------------------------ - -Geçerli satırı döndürür ve iç işaretçiyi sonrakine ilerletir. Başka satır kalmadıysa `null` döndürür. - -```php -$books = $explorer->table('book'); -while ($book = $books->fetch()) { - $this->processBook($book); -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Sonuçları ilişkisel bir dizi olarak döndürür. İlk argüman dizide anahtar olarak kullanılacak sütunun adını, ikinci argüman değer olarak kullanılacak sütunun adını belirtir: - -```php -$authors = $explorer->table('author')->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Yalnızca ilk parametre verilirse değer, satırın tamamı, yani `ActiveRow` nesnesi olur: - -```php -$authors = $explorer->table('author')->fetchPairs('id'); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - -Anahtarlar yinelenirse son satırdaki değer kullanılır. Anahtar olarak `null` kullanıldığında dizi sıfırdan başlayarak sayısal indekslenir (o zaman çakışma olmaz): - -```php -$authors = $explorer->table('author')->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Alternatif olarak parametre olarak bir callback verebilirsiniz; bu callback her satır için ya tek bir değer ya da bir anahtar-değer çifti döndürür. - -```php -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); -// ['First Book (John Novak)', ...] - -// Callback, anahtar ve değer çiftinden oluşan bir dizi de döndürebilir: -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => [$row->title, $row->author->name]); -// ['First Book' => 'John Novak', ...] -``` - - -fetchAll(): array .[method] ---------------------------- - -Tüm satırları, anahtarların birincil anahtar değerleri olduğu ilişkisel bir `ActiveRow` nesneleri dizisi olarak döndürür. - -```php -$allBooks = $explorer->table('book')->fetchAll(); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - - -count(): int .[method] ----------------------- - -Parametresiz `count()` metodu, `Selection` nesnesindeki satır sayısını döndürür: - -```php -$table->where('category', 1); -$count = $table->count(); -$count = count($table); // alternatif -``` - -Not: Parametreli `count()`, veritabanında COUNT toplama fonksiyonunu çalıştırır, aşağıya bakın. - - -ActiveRow::toArray(): array .[method] -------------------------------------- - -`ActiveRow` nesnesini, anahtarların sütun adları, değerlerin ise karşılık gelen veriler olduğu ilişkisel bir diziye dönüştürür. - -```php -$book = $explorer->table('book')->get(1); -$bookArray = $book->toArray(); -// $bookArray şu olur: ['id' => 1, 'title' => '...', 'author_id' => ..., ...] -``` - - -Toplama -======= - -`Selection` sınıfı, toplama fonksiyonlarını (COUNT, SUM, MIN, MAX, AVG vb.) kolayca çalıştırmak için metotlar sunar. - -.[language-php] -| `count($expr)` | Satır sayısını sayar | -| `min($expr)` | Bir sütundaki en küçük değeri döndürür | -| `max($expr)` | Bir sütundaki en büyük değeri döndürür | -| `sum($expr)` | Bir sütundaki değerlerin toplamını döndürür | -| `aggregation($function)` | `AVG()` ya da `GROUP_CONCAT()` gibi herhangi bir toplama fonksiyonuna izin verir | - - -count(string $expr): int .[method] ----------------------------------- - -COUNT fonksiyonuyla bir SQL sorgusu çalıştırır ve sonucu döndürür. Metot, belirli bir koşula kaç satırın uyduğunu belirlemek için kullanılır: - -```php -$count = $table->count('*'); // SELECT COUNT(*) FROM `table` -$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` -``` - -Not: Parametresiz [#count()], yalnızca `Selection` nesnesindeki satır sayısını döndürür. - - -min(string $expr) ve max(string $expr) .[method] ------------------------------------------------- - -`min()` ve `max()` metotları, belirtilen sütundaki ya da ifadedeki en küçük ve en büyük değerleri döndürür: - -```php -// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 -$maxPrice = $products->where('active', true) - ->max('price'); -``` - - -sum(string $expr): mixed .[method] ----------------------------------- - -Belirtilen sütundaki ya da ifadedeki değerlerin toplamını döndürür: - -```php -// SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 -$totalPrice = $products->where('active', true) - ->sum('price * items_in_stock'); -``` - - -aggregation(string $function, ?string $groupFunction = null): mixed .[method] ------------------------------------------------------------------------------ - -Herhangi bir toplama fonksiyonunu çalıştırmaya olanak tanır. - -```php -// bir kategorideki ürünlerin ortalama fiyatı -$avgPrice = $products->where('category_id', 1) - ->aggregation('AVG(price)'); - -// ürün etiketlerini tek bir dizede birleştirir -$tags = $products->where('id', 1) - ->aggregation('GROUP_CONCAT(tag.name) AS tags') - ->fetch() - ->tags; -``` - -Zaten bir toplama fonksiyonundan ve gruplamadan doğan sonuçları toplamamız gerekiyorsa (örneğin gruplanmış satırlarda `SUM(value)`), bu ara sonuçlara uygulanacak toplama fonksiyonunu ikinci argüman olarak belirtiriz: - -```php -// Tek tek kategorilerdeki stoktaki ürünlerin toplam fiyatını hesaplar ve sonra bu fiyatları toplar. -$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') - ->group('category_id') - ->aggregation('SUM(category_total)', 'SUM'); -``` - -Bu örnekte önce her kategorideki ürünlerin toplam fiyatını hesaplıyoruz (`SUM(price * stock) AS category_total`) ve sonuçları `category_id` değerine göre gruplandırıyoruz. Sonra bu ara toplamları `category_total` toplamak için `aggregation('SUM(category_total)', 'SUM')` kullanıyoruz. İkinci argüman `'SUM'`, ara sonuçlara SUM fonksiyonunun uygulanacağını belirtir. - - -Insert, Update ve Delete -======================== - -Nette Database Explorer, veri eklemeyi, güncellemeyi ve silmeyi kolaylaştırır. Sözü edilen tüm metotlar hata durumunda `Nette\Database\DriverException` fırlatır. - - -Selection::insert(iterable $data) .[method] -------------------------------------------- - -Tabloya yeni kayıtlar ekler. - -**Tek bir kayıt ekleme:** - -Yeni kaydı, anahtarları tablodaki sütun adlarına karşılık gelen ilişkisel bir dizi ya da iterable nesne ([formlarda |forms:] kullanılan `ArrayHash` gibi) olarak verin. - -Tablonun tanımlı bir birincil anahtarı varsa, metot bir `ActiveRow` nesnesi döndürür; bu nesne, veritabanı düzeyinde yapılan değişiklikleri (tetikleyiciler, varsayılan sütun değerleri, auto-increment sütun hesapları) yansıtmak için veritabanından yeniden yüklenir. Bu, veri tutarlılığını güvence altına alır ve nesne her zaman veritabanındaki güncel veriyi içerir. Tablonun birincil anahtarı yoksa tanımlanabilir bir satır olmadığından metot `null` döndürür. - -```php -$row = $explorer->table('users')->insert([ - 'name' => 'John Doe', - 'email' => 'john.doe@example.com', -]); -// $row bir ActiveRow örneğidir ve eklenen satırın tüm verilerini içerir; -// otomatik üretilen ID ve tetikleyicilerin yaptığı değişiklikler dahil -echo $row->id; // Yeni eklenen kullanıcının ID'sini çıktılar -echo $row->created_at; // Bir tetikleyici ayarladıysa oluşturulma zamanını çıktılar -``` - -**Birden çok kaydı tek seferde ekleme:** - -`insert()` metodu, tek bir SQL sorgusuyla birden çok kayıt eklemeye olanak tanır. Bu durumda eklenen satır sayısını döndürür. - -```php -$insertedRows = $explorer->table('users')->insert([ - [ - 'name' => 'John', - 'year' => 1994, - ], - [ - 'name' => 'Jack', - 'year' => 1995, - ], -]); -// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) -// $insertedRows değeri 2 olur -``` - -Parametre olarak, veri seçimi içeren bir `Selection` nesnesi de aktarılabilir. - -```php -$newUsers = $explorer->table('potential_users') - ->where('approved', 1) - ->select('name, email'); - -$insertedRows = $explorer->table('users')->insert($newUsers); -``` - -**Özel değerler ekleme:** - -Değer olarak dosyaları, `DateTime` nesnelerini ya da SQL sabit değerlerini de aktarabiliriz: - -```php -$explorer->table('users')->insert([ - 'name' => 'John', - 'created_at' => new DateTime, // veritabanı biçimine dönüştürür - 'avatar' => fopen('image.jpg', 'rb'), // dosyanın ikili içeriğini ekler - 'uuid' => $explorer::literal('UUID()'), // UUID() fonksiyonunu çağırır -]); -``` - - -Selection::update(iterable $data): int .[method] ------------------------------------------------- - -Tablodaki satırları belirtilen filtreye göre günceller. Gerçekten değiştirilen satır sayısını döndürür. - -Değiştirilecek sütunları, anahtarları tablodaki sütun adlarına karşılık gelen ilişkisel bir dizi ya da iterable nesne ([formlarda |forms:] kullanılan `ArrayHash` gibi) olarak verin: - -```php -$affected = $explorer->table('users') - ->where('id', 10) - ->update([ - 'name' => 'John Smith', - 'year' => 1994, - ]); -// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 -``` - -Sayısal değerleri değiştirmek için `+=` ve `-=` operatörlerini kullanabilirsiniz: - -```php -$explorer->table('users') - ->where('id', 10) - ->update([ - 'points+=' => 1, // 'points' sütununun değerini 1 artırır - 'coins-=' => 1, // 'coins' sütununun değerini 1 azaltır - ]); -// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 -``` - - -Selection::delete(): int .[method] ----------------------------------- - -Tablodaki satırları belirtilen filtreye göre siler. Silinen satır sayısını döndürür. - -```php -$count = $explorer->table('users') - ->where('id', 10) - ->delete(); -// DELETE FROM `users` WHERE `id` = 10 -``` - -.[caution] -`update()` ya da `delete()` çağırırken, değiştirilecek/silinecek satırları belirtmek için `where()` kullanmayı unutmayın. `where()` kullanılmazsa işlem tablonun tamamında gerçekleştirilir! - - -ActiveRow::update(iterable $data): bool .[method] -------------------------------------------------- - -`ActiveRow` nesnesinin temsil ettiği veritabanı satırındaki veriyi günceller. Güncellenecek veriyi içeren bir iterable alır (anahtarlar sütun adlarıdır). Sayısal değerleri değiştirmek için `+=` ve `-=` operatörlerini kullanabilirsiniz: - -Güncelleme yapıldıktan sonra `ActiveRow`, veritabanı düzeyinde yapılan değişiklikleri (örneğin tetikleyicileri) yansıtmak için veritabanından otomatik olarak yeniden yüklenir. Metot yalnızca gerçek bir veri değişikliği olduysa `true` döndürür. - -```php -$article = $explorer->table('article')->get(1); -$article->update([ - 'views += 1', // görüntülenme sayısını artırır -]); -echo $article->views; // Geçerli görüntülenme sayısını çıktılar -``` - -Bu metot veritabanında yalnızca belirli bir satırı günceller. Birden çok satırı toplu güncellemek için [#Selection::update()] metodunu kullanın. - - -ActiveRow::delete(): int .[method] ----------------------------------- - -`ActiveRow` nesnesinin temsil ettiği satırı veritabanından siler. Silinen satır sayısını döndürür; bu 1 olmalıdır. - -```php -$book = $explorer->table('book')->get(1); -$book->delete(); // ID'si 1 olan kitabı siler -``` - -Bu metot veritabanında yalnızca belirli bir satırı siler. Birden çok satırı toplu silmek için [#Selection::delete()] metodunu kullanın. - - -Tablolar Arası İlişkiler -======================== - -İlişkisel veritabanlarında veriler birden çok tabloya bölünür ve yabancı anahtarlarla birbirine bağlanır. Nette Database Explorer, bu ilişkilerle çalışmanın devrim niteliğinde bir yolunu sunar: JOIN sorguları yazmadan ve hiçbir şeyi yapılandırmaya ya da üretmeye gerek kalmadan. - -İlişkilerle çalışmayı göstermek için örnek bir kitap veritabanı kullanacağız ([GitHub'da bulabilirsiniz |https://github.com/nette-examples/books]). Veritabanında şu tablolar var: - -- `author` - yazarlar ve çevirmenler (`id`, `name`, `web`, `born` sütunları) -- `book` - kitaplar (`id`, `author_id`, `translator_id`, `title`, `sequel_id` sütunları) -- `tag` - etiketler (`id`, `name` sütunları) -- `book_tag` - kitaplarla etiketler arasındaki bağlantı tablosu (`book_id`, `tag_id` sütunları) - -[* db-schema-1-.webp *] *** Örneklerde kullanılan veritabanı yapısı .<> - -Örnek kitap veritabanımızda birkaç ilişki türü buluyoruz (model gerçeğe göre basitleştirilmiş olsa da): - -- **Bire çok (1:N)** - Her kitabın **bir** yazarı vardır; bir yazar **birden çok** kitap yazabilir. -- **Sıfıra çok (0:N)** - Bir kitabın çevirmeni **olabilir**; bir çevirmen **birden çok** kitap çevirebilir. -- **Sıfıra bir (0:1)** - Bir kitabın devamı **olabilir**. -- **Çoka çok (M:N)** - Bir kitabın **birkaç** etiketi olabilir ve bir etiket **birkaç** kitaba atanabilir. - -Bu ilişkilerde her zaman bir **üst tablo** ve bir **alt tablo** vardır. Örneğin yazarlarla kitaplar arasındaki ilişkide `author` tablosu üst, `book` tablosu alttır; bir kitabın her zaman bir yazara "ait olduğunu" düşünebilirsiniz. Bu, veritabanı yapısına da yansır: alt tablo `book`, üst tablo `author` tablosuna başvuran `author_id` yabancı anahtarını içerir. - -Kitapları yazarlarının adlarıyla birlikte listelememiz gerekiyorsa iki seçeneğimiz var. Ya veriyi JOIN kullanan tek bir SQL sorgusuyla alırız: - -```sql -SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; -``` - -Ya da veriyi iki adımda alırız (önce kitaplar, sonra yazarları) ve sonra PHP'de birleştiririz: - -```sql -SELECT * FROM book; -SELECT * FROM author WHERE id IN (1, 2, 3); -- seçilen kitapların yazar ID'leri -``` - -İkinci yaklaşım aslında **daha verimlidir**, şaşırtıcı gelse de. Veri yalnızca bir kez getirilir ve önbellekte daha iyi kullanılabilir. Nette Database Explorer tam olarak böyle çalışır; her şeyi perde arkasında halleder ve size şık bir API sunar: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo 'başlık: ' . $book->title; - echo 'yazan: ' . $book->author->name; // $book->author, 'author' tablosundan bir kayıttır - echo 'çeviren: ' . $book->translator?->name; -} -``` - - -Üst Tabloya Erişim ------------------- - -Üst tabloya erişim dolaysızdır. Bunlar *bir kitabın bir yazarı vardır* ya da *bir kitabın çevirmeni olabilir* türünden ilişkilerdir. İlişkili kayıt, ActiveRow nesnesinin bir özelliğiyle alınır; özelliğin adı, yabancı anahtar sütununun `_id` son eki olmadan yazılmış hâline karşılık gelir: - -```php -$book = $explorer->table('book')->get(1); -echo $book->author->name; // author_id sütununa göre yazarı bulur -echo $book->translator?->name; // translator_id sütununa göre çevirmeni bulur -``` - -`$book->author` özelliğine erişilirken Explorer, `book` tablosunda adında `author` dizesi geçen bir sütun arar (yani `author_id`). Bu sütundaki değere göre `author` tablosundan ilgili kaydı yükler ve onu bir `ActiveRow` olarak döndürür. `$book->translator` da benzer şekilde `translator_id` sütununu kullanır. `translator_id` sütunu `null` içerebildiğinden kodda nullsafe operatörü `?->` kullanıyoruz. - -Bir başka yaklaşımı, hedef tablonun adını ve birleştirme sütununun adını alan ve bir `ActiveRow` örneği ya da `null` döndüren `ref()` metodu sunar: - -```php -echo $book->ref('author', 'author_id')->name; // yazarla ilişki -echo $book->ref('author', 'translator_id')->name; // çevirmenle ilişki -``` - -`ref()` metodu, örneğin tablo aynı adda bir sütun içerdiği için (yani `author`) özellik erişimi kullanılamadığında işe yarar. Diğer durumlarda, daha iyi okunurluk için özellik erişimini kullanmanız önerilir. - -Explorer, veritabanı sorgularını otomatik olarak iyileştirir. Bir döngüde kitapları dolaşıp ilişkili kayıtlarına (yazarlara, çevirmenlere) erişirken Explorer her kitap için ayrı bir sorgu üretmez. Bunun yerine **her ilişki türü için yalnızca bir SELECT sorgusu** çalıştırır ve veritabanı yükünü belirgin biçimde azaltır. Örneğin: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo $book->title . ': '; - echo $book->author->name; - echo $book->translator?->name; -} -``` - -Bu kod, veritabanına yalnızca şu üç şimşek hızındaki sorguyu çalıştırır: - -```sql -SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- seçilen kitapların author_id sütunundaki ID'ler -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- seçilen kitapların translator_id sütunundaki ID'ler -``` - -.[note] -Birleştirme sütununu bulma mantığını [Conventions |api:Nette\Database\Conventions] gerçekleştirimi belirler. Yabancı anahtarları çözümleyen ve tablolar arasındaki var olan ilişkilerle kolayca çalışmanızı sağlayan [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions] kullanmanızı öneririz. - - -Alt Tabloya Erişim ------------------- - -Alt tabloya erişim ters yönde çalışır. Şimdi *bu yazar hangi kitapları yazdı* ya da *bu çevirmen hangi kitapları çevirdi* diye soruyoruz. Bu tür sorgu için, ilişkili kayıtları içeren bir `Selection` döndüren `related()` metodunu kullanırız. Bir örneğe bakalım: - -```php -$author = $explorer->table('author')->get(1); - -// Yazarın tüm kitaplarını çıktılar -foreach ($author->related('book.author_id') as $book) { - echo "Yazdı: $book->title"; -} - -// Yazarın çevirdiği tüm kitapları çıktılar -foreach ($author->related('book.translator_id') as $book) { - echo "Çevirdi: $book->title"; -} -``` - -`related()` metodu, birleştirme açıklamasını nokta yazımıyla tek argüman olarak ya da iki ayrı argüman olarak alır: - -```php -$author->related('book.translator_id'); // tek argüman -$author->related('book', 'translator_id'); // iki argüman -``` - -Explorer, doğru birleştirme sütununu üst tablonun adına göre otomatik saptayabilir. Bu durumda kaynak tablonun adı `author` olduğu için `book.author_id` sütunu üzerinden birleştirir: - -```php -$author->related('book'); // book.author_id kullanır -``` - -Birden çok olası bağlantı varsa Explorer bir [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException] fırlatır. - -`related()` metodunu elbette bir döngüde birden çok kaydı dolaşırken de kullanabiliriz; Explorer bu durumda da sorguları otomatik iyileştirir: - -```php -$authors = $explorer->table('author'); -foreach ($authors as $author) { - echo $author->name . ' şunları yazdı:'; - foreach ($author->related('book') as $book) { - echo $book->title; - } -} -``` - -Bu kod yalnızca iki şimşek hızında SQL sorgusu üretir: - -```sql -SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- seçilen yazarların ID'leri -``` - - -Çoka Çok İlişki ---------------- - -Çoka çok (M:N) ilişkisi için, iki yabancı anahtar sütunu (`book_id`, `tag_id`) içeren bir **bağlantı tablosu** (bizim durumumuzda `book_tag`) gerekir. Bu sütunların her biri, bağlanan tablolardan birinin birincil anahtarına başvurur. İlişkili veriyi almak için önce `related('book_tag')` ile bağlantı tablosundaki kayıtları alır, sonra hedef veriye geçeriz: - -```php -$book = $explorer->table('book')->get(1); -// kitaba atanan etiketlerin adlarını çıktılar -foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name; // etiket adını bağlantı tablosu üzerinden çıktılar -} - -$tag = $explorer->table('tag')->get(1); -// ya da tersi: bu etiketle işaretlenmiş kitapların adlarını çıktılar -foreach ($tag->related('book_tag') as $bookTag) { - echo $bookTag->book->title; // kitabın başlığını çıktılar -} -``` - -Explorer, SQL sorgularını yine verimli bir biçime dönüştürür: - -```sql -SELECT * FROM `book`; -SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- seçilen kitapların ID'leri -SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- book_tag içinde bulunan etiketlerin ID'leri -``` - - -İlişkili Tablolar Üzerinden Sorgulama -------------------------------------- - -`where()`, `select()`, `order()` ve `group()` metotlarında, başka tablolardaki sütunlara erişmek için özel yazımlar kullanabilirsiniz. Explorer gereken JOIN'leri otomatik oluşturur. - -**Nokta yazımı** (`ust_tablo.sutun`), alt tablonun bakış açısından 1:N ilişkilerinde kullanılır: - -```php -$books = $explorer->table('book'); - -// Yazarının adı 'Jon' ile başlayan kitapları bulur -$books->where('author.name LIKE ?', 'Jon%'); - -// Kitapları yazar adına göre azalan sırada sıralar -$books->order('author.name DESC'); - -// Kitabın başlığını ve yazarın adını çıktılar -$books->select('book.title, author.name'); -``` - -**İki nokta yazımı** (`:alt_tablo.sutun`), üst tablonun bakış açısından 1:N ilişkilerinde kullanılır: - -```php -$authors = $explorer->table('author'); - -// Başlığında 'PHP' geçen bir kitap yazan yazarları bulur -$authors->where(':book.title LIKE ?', '%PHP%'); - -// Her yazarın kitap sayısını sayar -$authors->select('*, COUNT(:book.id) AS book_count') - ->group('author.id'); -``` - -Yukarıdaki iki nokta yazımı örneğinde (`:book.title`) yabancı anahtar sütunu belirtilmemiştir. Explorer, doğru sütunu üst tablonun adına göre otomatik saptar. Bu durumda kaynak tablonun adı `author` olduğu için `book.author_id` sütunu üzerinden birleştirir. Birden çok olası bağlantı varsa Explorer bir [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException] fırlatır. - -Birleştirme sütunu parantez içinde açıkça belirtilebilir: - -```php -// Başlığında 'PHP' geçen bir kitabı çeviren yazarları bulur -$authors->where(':book(translator_id).title LIKE ?', '%PHP%'); -``` - -Yazımlar, birden çok tabloya yayılan veriye erişmek için zincirlenebilir: - -```php -// 'PHP' etiketli kitapların yazarlarını bulur -$authors->where(':book:book_tag.tag.name', 'PHP') - ->group('author.id'); -``` - - -JOIN Koşullarını Genişletme ---------------------------- - -`joinWhere()` metodu, SQL'de tabloları birleştirirken `ON` anahtar sözcüğünden sonra belirtilen koşulları genişletir. - -Diyelim ki belirli bir çevirmenin çevirdiği kitapları bulmak istiyoruz: - -```php -// 'David' adlı bir çevirmenin çevirdiği kitapları bulur -$books = $explorer->table('book') - ->joinWhere('translator', 'translator.name', 'David'); -// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') -``` - -`joinWhere()` koşulunda, `where()` metodundakiyle aynı yapıları kullanabilirsiniz: operatörler, yer tutucular, değer dizileri ya da SQL ifadeleri. - -Birden çok JOIN içeren daha karmaşık sorgularda tablo takma adları tanımlayabilirsiniz: - -```php -$tags = $explorer->table('tag') - ->joinWhere(':book_tag.book.author', 'book_author.born < ?', 1950) - ->alias(':book_tag.book.author', 'book_author'); -// LEFT JOIN `book_tag` ON `tag`.`id` = `book_tag`.`tag_id` -// LEFT JOIN `book` ON `book_tag`.`book_id` = `book`.`id` -// LEFT JOIN `author` `book_author` ON `book`.`author_id` = `book_author`.`id` -// AND (`book_author`.`born` < 1950) -``` - -`where()` metodunun koşulları `WHERE` yan tümcesine eklediğine, `joinWhere()` metodunun ise tabloları birleştirirken `ON` yan tümcesindeki koşulları genişlettiğine dikkat edin. diff --git a/database/tr/guide.texy b/database/tr/guide.texy deleted file mode 100644 index 5b6a06755e..0000000000 --- a/database/tr/guide.texy +++ /dev/null @@ -1,218 +0,0 @@ -Nette Database -************** - -.[perex] -Nette Database, basitliğe ve akıllı özelliklere odaklanan güçlü ve şık bir PHP veritabanı katmanıdır. Veritabanınızla çalışmanın iki yolunu sunar: hızlı uygulama geliştirme için [Explorer |explorer] ya da sorguları doğrudan yönetmek için [SQL yolu |SQL way]. - -<div class="grid gap-3"> -<div> - - -[SQL yolu|sql-way] -================== -- Güvenli, parametreli sorgular -- SQL sorgusunun yapısı üzerinde tam denetim -- Gelişmiş fonksiyonlarla karmaşık sorgular yazarken -- Belirli SQL fonksiyonlarıyla başarımı iyileştirme - -</div> - -<div> - - -[Explorer |explorer] -==================== -- SQL yazmadan hızlı geliştirme -- Tablolar arası ilişkilerin sezgisel yönetimi -- Otomatik sorgu iyileştirmesinden yararlanma -- Hızlı ve rahat veritabanı çalışması için uygun - -</div> - -</div> - - -Kurulum -======= - -Kütüphaneyi [Composer|best-practices:composer] ile indirip kurun: - -```shell -composer require nette/database -``` - - -Desteklenen Veritabanları -========================= - -Nette Database şu veritabanlarını destekler: - -|* Veritabanı sunucusu |* DSN adı |* Explorer desteği -|-----------------------|--------------|-----------------------| -| MySQL (>= 5.1) | mysql | EVET | -| PostgreSQL (>= 9.0) | pgsql | EVET | -| SQLite 3 (>= 3.8) | sqlite | EVET | -| Oracle | oci | HAYIR | -| MS SQL (PDO_SQLSRV) | sqlsrv | EVET | -| MS SQL (PDO_DBLIB) | mssql | HAYIR | -| ODBC | odbc | HAYIR | - - -Veritabanı Çalışmasına İki Yaklaşım -=================================== - -Nette Database size bir seçim sunar: SQL sorgularını ya doğrudan yazarsınız (SQL yolu) ya da otomatik üretilmelerini sağlarsınız (Explorer). İki yaklaşımın aynı işleri nasıl yaptığına bakalım: - -[SQL yolu|sql-way] - SQL sorguları - -```php -// Kayıt ekleme -$database->query('INSERT INTO books', [ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// Kayıtları alma: kitap yazarları -$result = $database->query(' - SELECT authors.*, COUNT(books.id) AS books_count - FROM authors - LEFT JOIN books ON authors.id = books.author_id - WHERE authors.active = 1 - GROUP BY authors.id -'); - -// Gösterim (ideal değil, N ek sorgu üretir) -foreach ($result as $author) { - $books = $database->query(' - SELECT * FROM books - WHERE author_id = ? - ORDER BY published_at DESC - ', $author->id); - - echo "$author->name adlı yazar $author->books_count kitap yazmış:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -[Explorer yolu|explorer] - otomatik SQL üretimi - -```php -// Kayıt ekleme -$database->table('books')->insert([ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// Kayıtları alma: kitap yazarları -$authors = $database->table('authors') - ->where('active', 1); - -// Gösterim (otomatik olarak yalnızca 2 iyileştirilmiş sorgu üretir) -foreach ($authors as $author) { - $books = $author->related('books') - ->order('published_at DESC'); - - echo "$author->name adlı yazar {$books->count()} kitap yazmış:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -Explorer yaklaşımı SQL sorgularını otomatik üretir ve iyileştirir. Yukarıdaki örnekte SQL yolu N+1 sorgu üretir (biri yazarlar için, sonra her yazarın kitapları için birer tane), Explorer ise sorguları otomatik iyileştirip yalnızca iki sorgu çalıştırır: biri yazarlar, biri de onların tüm kitapları için. - -İki yaklaşım uygulamanızda ihtiyaca göre serbestçe birleştirilebilir. - - -Bağlantı ve Yapılandırma -======================== - -Veritabanına bağlanmak için yalnızca [api:Nette\Database\Connection] sınıfından bir örnek oluşturun: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password); -``` - -`$dsn` (Data Source Name) parametresi, [PDO'nun kullandığıyla |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters] aynıdır, örneğin `host=127.0.0.1;dbname=test`. Başarısızlık durumunda `Nette\Database\ConnectionException` fırlatır. - -Ancak daha elverişli bir yöntemi, yalnızca bir `database` bölümü eklemeniz yeten [uygulama yapılandırması |configuration] sunar. Bu, gereken nesneleri ve ayrıca [Tracy |tracy:] çubuğunda bir veritabanı paneli oluşturur. - -```neon -database: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password -``` - -Bağlantı nesnesi sonra [DI container'dan servis olarak alınabilir |dependency-injection:passing-dependencies], örneğin: - -```php -class Model -{ - public function __construct( - // ya da Nette\Database\Explorer - private Nette\Database\Connection $database, - ) { - } -} -``` - -[Veritabanı yapılandırması|configuration] hakkında daha fazla bilgi. - - -Explorer'ı Elle Oluşturma -------------------------- - -Nette DI container kullanmıyorsanız, `Nette\Database\Explorer` örneğini elle oluşturabilirsiniz: - -```php -// veritabanı bağlantısı -$connection = new Nette\Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); -// önbellek deposu, Nette\Caching\Storage arayüzünü uygular, örneğin: -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); -// veritabanı yapısının reflection'ını üstlenir -$structure = new Nette\Database\Structure($connection, $storage); -// tablo adlarının, sütunların ve yabancı anahtarların eşlenme kurallarını tanımlar -$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); -$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage); -``` - - -Bağlantı Yönetimi -================= - -Bir `Connection` nesnesi oluşturulduğunda bağlantı otomatik olarak kurulur. Bağlantıyı geciktirmek istiyorsanız tembel kipi kullanın; onu [yapılandırmada|configuration] `lazy` ayarıyla ya da şöyle açın: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => true]); -``` - -Bağlantıyı yönetmek için `connect()`, `disconnect()` ve `reconnect()` metotlarını kullanın. -- `connect()`, henüz yoksa bir bağlantı kurar ve `Nette\Database\ConnectionException` fırlatabilir. -- `disconnect()`, geçerli veritabanı bağlantısını keser. -- `reconnect()`, bağlantıyı kesip veritabanına yeniden bağlanır. Bu metot da `Nette\Database\ConnectionException` fırlatabilir. - -Ayrıca bağlantıyla ilgili olayları, veritabanı bağlantısı kurulduktan sonra çağrılan callback'lerden oluşan bir dizi olan `onConnect` olayıyla izleyebilirsiniz. - -```php -// veritabanına bağlandıktan sonra çalışır -$database->onConnect[] = function($database) { - echo "Connected to the database"; -}; -``` - -`onQuery` olayı da benzer biçimde çalışır; çalıştırılan her sorgudan sonra (ve bir sorgu başarısız olduğunda) çağrılan callback'lerden oluşan bir dizidir ve günlükleme ya da profilleme için yararlıdır. - - -Tracy Debug Bar -=============== - -[Tracy |tracy:] kullanıyorsanız, Debug Bar'daki Database paneli otomatik olarak etkinleşir. Çalıştırılan tüm sorguları, parametrelerini, çalışma sürelerini ve koddaki çağrıldıkları yeri gösterir. - -[* db-panel.webp *] diff --git a/database/tr/reflection.texy b/database/tr/reflection.texy deleted file mode 100644 index 8681b068bc..0000000000 --- a/database/tr/reflection.texy +++ /dev/null @@ -1,127 +0,0 @@ -Yapı Reflection'ı -***************** - -.{data-version:3.2.1} -Nette Database, [api:Nette\Database\Reflection] sınıfıyla veritabanı yapısını incelemeye yarayan araçlar sunar. Tablolar, sütunlar, indeksler ve yabancı anahtarlar hakkında bilgi almanızı sağlar. Reflection'ı şema üretmek, veritabanıyla çalışan esnek uygulamalar ya da genel amaçlı veritabanı araçları yazmak için kullanabilirsiniz. - -Reflection nesnesi, veritabanı bağlantısı örneğinden alınır: - -```php -$reflection = $database->getReflection(); -``` - - -Tabloları Alma --------------- - -Salt okunur `$reflection->tables` özelliği, veritabanındaki tüm tabloların ilişkisel bir dizisini içerir: - -```php -// Tüm tabloların adlarını listeleme -foreach ($reflection->tables as $name => $table) { - echo $name . "\n"; -} -``` - -İki metot daha vardır: - -```php -// Bir tablonun varlığını denetleme -if ($reflection->hasTable('users')) { - echo "Table users exists"; -} - -// Tablo nesnesini döndürür; yoksa istisna fırlatır -$table = $reflection->getTable('users'); -``` - - -Tablo Bilgisi -------------- - -Bir tablo, şu salt okunur özellikleri sunan [Table|api:Nette\Database\Reflection\Table] nesnesiyle temsil edilir: - -- `$name: string` - tablonun adı -- `$view: bool` - bir view olup olmadığı -- `$fullName: ?string` - şema dahil tablonun tam adı (varsa) -- `$columns: array<string, Column>` - tablo sütunlarının ilişkisel dizisi -- `$indexes: Index[]` - tablo indekslerinin dizisi -- `$primaryKey: ?Index` - tablonun birincil anahtarı ya da null -- `$foreignKeys: ForeignKey[]` - tablonun yabancı anahtarlarının dizisi -- `$comment: ?string` - tablonun yorumu - - -Sütunlar --------- - -Tablonun `columns` özelliği, anahtarın sütun adı, değerin ise şu özelliklere sahip bir [Column|api:Nette\Database\Reflection\Column] örneği olduğu ilişkisel bir sütun dizisi sunar: - -- `$name: string` - sütunun adı -- `$table: ?Table` - sütunun tablosuna referans -- `$nativeType: string` - veritabanının yerel türü -- `$size: ?int` - türün boyutu/uzunluğu -- `$nullable: bool` - sütunun NULL içerip içeremeyeceği -- `$default: mixed` - sütunun varsayılan değeri -- `$autoIncrement: bool` - sütunun auto-increment olup olmadığı -- `$primary: bool` - birincil anahtarın parçası olup olmadığı -- `$vendor: array` - ilgili veritabanı sistemine özgü ek meta veriler -- `$comment: ?string` - sütunun yorumu - -```php -foreach ($table->columns as $name => $column) { - echo "Column: $name\n"; - echo "Type: {$column->nativeType}\n"; - echo "Nullable: " . ($column->nullable ? 'Yes' : 'No') . "\n"; -} -``` - - -İndeksler ---------- - -Tablonun `indexes` özelliği, her indeksin şu özelliklere sahip bir [Index|api:Nette\Database\Reflection\Index] örneği olduğu bir indeks dizisi sunar: - -- `$columns: Column[]` - indeksi oluşturan sütunların dizisi -- `$unique: bool` - indeksin benzersiz olup olmadığı -- `$primary: bool` - birincil anahtar olup olmadığı -- `$name: ?string` - indeksin adı - -Tablonun birincil anahtarı `primaryKey` özelliğiyle alınabilir; bu özellik ya bir `Index` nesnesi ya da tablonun birincil anahtarı yoksa `null` döndürür. - -```php -// İndeksleri listeleme -foreach ($table->indexes as $index) { - $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); - echo "Index" . ($index->name ? " {$index->name}" : '') . ":\n"; - echo " Columns: $columns\n"; - echo " Unique: " . ($index->unique ? 'Yes' : 'No') . "\n"; -} - -// Birincil anahtarı listeleme -if ($primaryKey = $table->primaryKey) { - $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); - echo "Primary Key: $columns\n"; -} -``` - - -Yabancı Anahtarlar ------------------- - -Tablonun `foreignKeys` özelliği, her yabancı anahtarın şu özelliklere sahip bir [ForeignKey|api:Nette\Database\Reflection\ForeignKey] örneği olduğu bir yabancı anahtar dizisi sunar: - -- `$foreignTable: Table` - başvurulan tablo -- `$localColumns: Column[]` - yerel sütunların dizisi -- `$foreignColumns: Column[]` - başvurulan sütunların dizisi -- `$name: string` - yabancı anahtarın adı - -```php -// Yabancı anahtarları listeleme -foreach ($table->foreignKeys as $fk) { - $localCols = implode(', ', array_map(fn($col) => $col->name, $fk->localColumns)); - $foreignCols = implode(', ', array_map(fn($col) => $col->name, $fk->foreignColumns)); - - echo "FK" . ($fk->name ? " {$fk->name}" : '') . ":\n"; - echo " $localCols -> {$fk->foreignTable->name}($foreignCols)\n"; -} -``` diff --git a/database/tr/security.texy b/database/tr/security.texy deleted file mode 100644 index d76581655b..0000000000 --- a/database/tr/security.texy +++ /dev/null @@ -1,185 +0,0 @@ -Güvenlik Riskleri -***************** - -<div class=perex> - -Veritabanları çoğu zaman hassas veriler içerir ve tehlikeli işlemler yapmaya olanak tanır. Nette Database ile güvenli çalışmak için şunlar can alıcı önemdedir: - -- Güvenli ve güvensiz API'ler arasındaki farkı anlamak -- Parametreli sorgular kullanmak -- Girdi verisini düzgün doğrulamak - -</div> - - -SQL Injection Nedir? -==================== - -SQL injection, veritabanlarıyla çalışırken karşılaşılan en ciddi güvenlik riskidir. Temizlenmemiş kullanıcı girdisi bir SQL sorgusunun parçası olduğunda ortaya çıkar. Saldırgan kendi SQL komutlarını ekleyebilir ve böylece: -- Verilere yetkisiz erişim sağlayabilir -- Veritabanındaki verileri değiştirebilir ya da silebilir -- Kimlik doğrulamayı atlayabilir - -```php -// ❌ TEHLİKELİ KOD - SQL injection'a açık -$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); - -// Saldırgan şöyle bir değer girebilir: ' OR '1'='1 -// Ortaya çıkan sorgu şu olurdu: SELECT * FROM users WHERE name = '' OR '1'='1' -// Bu da tüm kullanıcıları döndürür -``` - -Aynısı Database Explorer için de geçerlidir: - -```php -// ❌ TEHLİKELİ KOD - SQL injection'a açık -$table->where('name = ' . $_GET['name']); -$table->where("name = '$_GET[name]'"); -``` - - -Parametreli Sorgular -==================== - -SQL injection'a karşı temel savunma parametreli sorgulardır. Nette Database onları kullanmanın çeşitli yollarını sunar. - -En basit yol, **soru işareti yer tutucuları** kullanmaktır: - -```php -// ✅ Güvenli parametreli sorgu -$database->query('SELECT * FROM users WHERE name = ?', $name); - -// ✅ Explorer'da güvenli koşul -$table->where('name = ?', $name); -``` - -Bu, [Database Explorer|explorer] içinde soru işareti yer tutucuları ve parametrelerle ifade eklemeye izin veren tüm diğer metotlar için de geçerlidir. - -INSERT, UPDATE komutlarında ya da WHERE yan tümcesinde değerleri bir dizide aktarabiliriz: - -```php -// ✅ Güvenli INSERT -$database->query('INSERT INTO users', [ - 'name' => $name, - 'email' => $email, -]); - -// ✅ Explorer'da güvenli INSERT -$table->insert([ - 'name' => $name, - 'email' => $email, -]); -``` - - -Parametre Değerlerinin Doğrulanması -=================================== - -Parametreli sorgular güvenli veritabanı çalışmasının temel taşıdır. Ancak onlara koyduğumuz değerler birkaç düzey denetimden geçmelidir: - - -Tür Denetimi ------------- - -**En önemlisi, parametrelerin veri türünün doğru olmasını sağlamaktır**; bu, Nette Database'in güvenli kullanımının zorunlu koşuludur. Veritabanı, tüm girdi verilerinin ilgili sütuna karşılık gelen doğru veri türünde olduğunu varsayar. - -Örneğin önceki örneklerdeki `$name`, dize yerine beklenmedik biçimde bir dizi olsaydı, Nette Database tüm öğelerini SQL sorgusuna koymaya çalışır ve bu hataya yol açardı. Bu yüzden `$_GET`, `$_POST` ya da `$_COOKIE` içinden gelen doğrulanmamış verileri veritabanı sorgularında **asla doğrudan kullanmayın**. - - -Biçim Doğrulaması ------------------ - -İkinci düzeyde verinin biçimini denetleriz; örneğin dizelerin UTF-8 kodlamasında olup olmadığını ve uzunluklarının sütun tanımına uyup uymadığını ya da sayısal değerlerin ilgili sütunun veri türü için izin verilen aralıkta olup olmadığını. - -Bu doğrulama düzeyinde kısmen veritabanının kendisine güvenebiliriz; pek çok veritabanı geçersiz veriyi reddeder. Ancak davranış değişebilir; bazıları uzun dizeleri sessizce kısaltabilir ya da aralık dışındaki sayıları kırpabilir. - - -Alana Özgü Doğrulama --------------------- - -Üçüncü düzey, uygulamanıza özgü mantıksal denetimleri içerir. Örneğin seçim kutularından gelen değerlerin sunulan seçeneklerle eşleşip eşleşmediğini, sayıların beklenen aralıkta olup olmadığını (örneğin yaş 0-150) ya da değerler arasındaki karşılıklı bağımlılıkların anlamlı olup olmadığını doğrulamak. - - -Önerilen Doğrulama Yöntemleri ------------------------------ - -- Tüm girdilerin düzgün doğrulanmasını otomatik olarak sağlayan [Nette Forms|forms:] kullanın. -- [Presenter'ları|application:] kullanın ve `action*()` ile `render*()` metotlarında parametrelerin veri türlerini belirtin. -- Ya da `filter_var()` gibi standart PHP araçlarıyla kendi doğrulama katmanınızı yazın. - - -Sütunlarla Güvenli Çalışma -========================== - -Önceki bölümde parametre değerlerinin nasıl düzgün doğrulanacağını gösterdik. Ancak SQL sorgularında dizi kullanırken anahtarlarına da aynı ölçüde dikkat etmeliyiz. - -```php -// ❌ TEHLİKELİ KOD - dizideki anahtarlar temizlenmemiş -$database->query('INSERT INTO users', $_POST); -``` - -INSERT ve UPDATE komutlarında bu kritik bir güvenlik kusurudur; saldırgan veritabanındaki herhangi bir sütunu ekleyebilir ya da değiştirebilir. Örneğin `is_admin = 1` yapabilir ya da hassas sütunlara istediği veriyi koyabilir (Mass Assignment Vulnerability denen açık). - -WHERE koşullarında bu daha da tehlikelidir; çünkü operatör içerebilirler: - -```php -// ❌ TEHLİKELİ KOD - dizideki anahtarlar temizlenmemiş -$_POST['salary >'] = 100000; -$database->query('SELECT * FROM users WHERE', $_POST); -// WHERE (`salary` > 100000) sorgusunu çalıştırır -``` - -Saldırgan bu yaklaşımı, çalışanların maaşlarını dizgeli biçimde keşfetmek için kullanabilir. Örneğin 100.000 üzerindeki maaşlar için bir sorguyla başlayabilir, sonra 50.000 altındakilerle sürdürebilir ve aralığı yavaş yavaş daraltarak tüm çalışanların yaklaşık maaşlarını ortaya çıkarabilir. Bu tür saldırıya SQL enumeration denir. - -`where()` ve `whereOr()` metotları [çok daha esnektir |explorer#where()] ve anahtarlarda ile değerlerde operatörler ve fonksiyonlar dahil SQL ifadelerini destekler. Bu da saldırgana SQL injection yapma olanağı verir: - -```php -// ❌ TEHLİKELİ KOD - saldırgan kendi SQL'ini ekleyebilir -$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1']; -$table->where($_POST); -// WHERE (0) UNION SELECT name, salary FROM users WHERE (1) sorgusunu çalıştırır -``` - -Bu saldırı, `0)` ile özgün koşulu sonlandırır, `UNION` ile kendi `SELECT` sorgusunu ekleyerek `users` tablosundan hassas veriler alır ve `WHERE (1)` ile söz dizimi açısından doğru sorguyu kapatır. - - -Sütun Beyaz Listesi -------------------- - -Sütun adlarıyla güvenli çalışmak için, kullanıcının yalnızca izin verilen sütunlarla çalışabilmesini ve kendi sütununu ekleyememesini sağlayan bir düzeneğe ihtiyacımız var. Tehlikeli sütun adlarını saptayıp engellemeyi (kara liste) deneyebilirdik, ama bu yaklaşım güvenilmezdir; saldırgan her zaman öngörmediğimiz, tehlikeli bir sütun adını yazmanın yeni bir yolunu bulabilir. - -Bu yüzden mantığı tersine çevirip izin verilen sütunların açık bir listesini (beyaz liste) tanımlamak çok daha güvenlidir: - -```php -// Kullanıcının değiştirmesine izin verilen sütunlar -$allowedColumns = ['name', 'email', 'active']; - -// İzin verilmeyen tüm sütunları girdiden çıkar -$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); - -// ✅ Artık sorgularda güvenle kullanılabilir, örneğin: -$database->query('INSERT INTO users', $filteredData); -$table->update($filteredData); -$table->where($filteredData); -``` - - -Dinamik Tanımlayıcılar -====================== - -Dinamik tablo ve sütun adları için `?name` yer tutucusunu kullanın. Bu, tanımlayıcıların ilgili veritabanının söz dizimine göre düzgün kaçışlanmasını sağlar (örneğin MySQL'de ters tırnaklarla): - -```php -// ✅ Güvenilir tanımlayıcıların güvenli kullanımı -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name', $column, $table); -// MySQL'de sonuç: SELECT `name` FROM `users` -``` - -Önemli: `?name` simgesini yalnızca uygulama kodunda tanımlanmış güvenilir değerler için kullanın. Kullanıcıdan gelen değerlerde yine bir [beyaz liste |#Sütun Beyaz Listesi] kullanın. Aksi hâlde kendinizi güvenlik risklerine açarsınız: - -```php -// ❌ TEHLİKELİ - kullanıcı girdisini asla kullanmayın -$database->query('SELECT ?name FROM users', $_GET['column']); -``` diff --git a/database/tr/sql-way.texy b/database/tr/sql-way.texy deleted file mode 100644 index 53ac83b0ed..0000000000 --- a/database/tr/sql-way.texy +++ /dev/null @@ -1,513 +0,0 @@ -SQL Yolu -******** - -.[perex] -Nette Database iki çalışma yolu sunar: SQL sorgularını kendiniz yazabilirsiniz (SQL yolu) ya da otomatik üretilmelerini sağlayabilirsiniz (bkz. [Explorer |explorer]). SQL yolu, sorguların güvenli biçimde kurulmasını güvence altına alırken size sorgular üzerinde tam denetim verir. - -.[note] -Veritabanı bağlantısı ve yapılandırmasının ayrıntıları [Bağlantı ve yapılandırma |guide#Bağlantı ve Yapılandırma] bölümünde bulunabilir. - - -Temel Sorgulama -=============== - -Veritabanını sorgulamak için `query()` metodu kullanılır. Sorgu sonucunu temsil eden bir [ResultSet |api:Nette\Database\ResultSet] nesnesi döndürür. Sorgu başarısız olursa metot [istisna fırlatır|exceptions]. Sorgu sonucunu bir `foreach` döngüsüyle dolaşabilir ya da [yardımcı metotlardan |#Veri Alma] birini kullanabilirsiniz. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; -} -``` - -Değerleri SQL sorgularına güvenle koymak için parametreli sorgular kullanın. Nette Database bunu son derece basit kılar: SQL sorgusundan sonra yalnızca bir virgül ve değeri ekleyin: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Birden çok parametrede iki seçeneğiniz var: SQL sorgusuyla parametreleri iç içe geçirebilirsiniz: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age); -``` - -Ya da önce tüm SQL sorgusunu yazıp sonra tüm parametreleri ekleyebilirsiniz: - -```php -$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); -``` - - -SQL Injection'a Karşı Koruma -============================ - -Parametreli sorgular kullanmak neden önemli? Çünkü sizi SQL injection denen bir saldırıdan korurlar; bu saldırıda bir saldırgan kendi SQL komutlarını ekleyip veritabanındaki verilere erişebilir ya da onlara zarar verebilir. - -.[warning] -**Değişkenleri asla doğrudan bir SQL sorgusuna koymayın!** Her zaman, sizi SQL injection'dan koruyan parametreli sorgular kullanın. - -```php -// ❌ TEHLİKELİ KOD - SQL injection'a açık -$database->query("SELECT * FROM users WHERE name = '$name'"); - -// ✅ Güvenli parametreli sorgu -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -[Olası güvenlik risklerini |security] öğrenin. - - -Sorgulama Teknikleri -==================== - - -WHERE Koşulları ---------------- - -`WHERE` koşullarını, anahtarların sütun adları, değerlerin ise karşılaştırılacak veriler olduğu ilişkisel bir dizi olarak yazabilirsiniz. Nette Database, değerin türüne göre en uygun SQL operatörünü otomatik seçer. - -```php -$database->query('SELECT * FROM users WHERE', [ - 'name' => 'John', - 'active' => true, -]); -// WHERE `name` = 'John' AND `active` = 1 -``` - -Karşılaştırma operatörünü anahtarda açıkça da belirtebilirsiniz: - -```php -$database->query('SELECT * FROM users WHERE', [ - 'age >' => 25, // > operatörünü kullanır - 'name LIKE' => '%John%', // LIKE operatörünü kullanır - 'email NOT LIKE' => '%example.com%', // NOT LIKE operatörünü kullanır -]); -// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' -``` - -Nette, `null` değerler ya da diziler gibi özel durumları otomatik olarak ele alır. - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name' => 'Laptop', // = operatörünü kullanır - 'category_id' => [1, 2, 3], // IN kullanır - 'description' => null, // IS NULL kullanır -]); -// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL -``` - -Olumsuz koşullar için `NOT` operatörünü kullanın: - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // != operatörünü kullanır - 'category_id NOT' => [1, 2, 3], // NOT IN kullanır - 'description NOT' => null, // IS NOT NULL kullanır - 'id NOT' => [], // atlanır -]); -// WHERE `name` != 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL -``` - -Koşullar varsayılan olarak `AND` operatörüyle birleştirilir. Bu, [?or yer tutucusuyla |#SQL Kurma İpuçları] değiştirilebilir. - - -ORDER BY Kuralları ------------------- - -`ORDER BY` yan tümcesi bir dizi kullanılarak yazılabilir. Sütunları anahtarlarda belirtin ve artan (`true`) ya da azalan (`false`) sıralamayı boolean bir değerle gösterin: - -```php -$database->query('SELECT id FROM author ORDER BY', [ - 'id' => true, // artan - 'name' => false, // azalan -]); -// SELECT id FROM author ORDER BY `id`, `name` DESC -``` - - -Veri Ekleme (INSERT) --------------------- - -Kayıt eklemek için SQL `INSERT` komutu kullanılır. - -```php -$values = [ - 'name' => 'John Doe', - 'email' => 'john@example.com', -]; -$database->query('INSERT INTO users ?', $values); -$userId = $database->getInsertId(); -``` - -`getInsertId()` metodu, son eklenen satırın ID'sini döndürür. Bazı veritabanlarında (örneğin PostgreSQL), ID'nin üretileceği dizinin adını `$database->getInsertId($sequenceId)` biçiminde parametre olarak belirtmek gerekir. - -Parametre olarak dosyalar, DateTime nesneleri ya da enum türleri gibi [#Özel Değerler] de aktarabilirsiniz. - -Birden çok kaydı tek seferde ekleme: - -```php -$database->query('INSERT INTO users ?', [ - ['name' => 'User 1', 'email' => 'user1@mail.com'], - ['name' => 'User 2', 'email' => 'user2@mail.com'], -]); -``` - -Çok kayıtlı bir INSERT çok daha hızlıdır; çünkü pek çok ayrı sorgu yerine yalnızca tek bir veritabanı sorgusu çalıştırılır. - -**Güvenlik notu:** Doğrulanmamış verileri asla `$values` olarak kullanmayın. [Olası riskleri |security#Sütunlarla Güvenli Çalışma] öğrenin. - - -Veri Güncelleme (UPDATE) ------------------------- - -Kayıtları güncellemek için SQL `UPDATE` komutu kullanılır. - -```php -// Tek bir kaydı güncelle -$values = [ - 'name' => 'John Smith', -]; -$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); -``` - -Etkilenen satır sayısını `$result->getRowCount()` döndürür. - -`UPDATE` içinde `+=` ve `-=` operatörlerini kullanabiliriz: - -```php -$database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // login_count değerini artır -], 1); -``` - -Kayıt zaten varsa güncelleyen, yoksa ekleyen bir örnek. `ON DUPLICATE KEY UPDATE` tekniğini kullanıyoruz: - -```php -$values = [ - 'name' => $name, - 'year' => $year, -]; -$database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', - $values + ['id' => $id], - $values, -); -// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978) -// ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 -``` - -Nette Database'in, bir dizi parametresinin SQL komutunda hangi bağlamda kullanıldığını tanıdığına ve SQL kodunu buna göre kurduğuna dikkat edin. Yani ilk diziden `(id, name, year) VALUES (123, 'Jim', 1978)` kurdu, ikincisini ise `name = 'Jim', year = 1978` biçimine çevirdi. Bunu [#SQL Kurma İpuçları] bölümünde daha ayrıntılı ele alıyoruz. - - -Veri Silme (DELETE) -------------------- - -Kayıtları silmek için SQL `DELETE` komutu kullanılır. Silinen satır sayısını alma örneği: - -```php -$count = $database->query('DELETE FROM users WHERE id = ?', 1) - ->getRowCount(); -``` - - -SQL Kurma İpuçları ------------------- - -İpucu (hint), SQL sorgusunda parametre değerinin bir SQL ifadesine nasıl dönüştürüleceğini belirten özel bir yer tutucudur: - -| İpucu | Açıklama | Otomatik kullanıldığı yer -|-----------|-------------------------------------------------|----------------------------- -| `?name` | Tablo ya da sütun adı eklemek için kullanılır | - -| `?values` | `(key, ...) VALUES (value, ...)` üretir | `INSERT ... ?`, `REPLACE ... ?` -| `?set` | `key = value, ...` atamalarını üretir | `SET ?`, `KEY UPDATE ?` -| `?and` | Dizideki koşulları `AND` ile birleştirir | `WHERE ?`, `HAVING ?` -| `?or` | Dizideki koşulları `OR` ile birleştirir | - -| `?order` | `ORDER BY` yan tümcesini üretir | `ORDER BY ?`, `GROUP BY ?` - -`?name` yer tutucusu, sorguya tablo ve sütun adlarını dinamik olarak eklemek için kullanılır. Nette Database, tanımlayıcıların veritabanı uzlaşımlarına göre doğru tırnaklanmasını üstlenir (örneğin MySQL'de ters tırnak içine alma). - -```php -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); -// SELECT `name` FROM `users` WHERE id = 1 (MySQL'de) -``` - -**Uyarı:** `?name` yer tutucusunu yalnızca doğrulanmış tablo ve sütun adlarında kullanın. Aksi hâlde [güvenlik açıkları |security#Dinamik Tanımlayıcılar] riskini alırsınız. - -Diğer ipuçlarını genellikle belirtmeye gerek yoktur; çünkü Nette, SQL sorgusunu kurarken akıllı otomatik saptama kullanır (tablonun üçüncü sütununa bakın). Ama örneğin koşulları `AND` yerine `OR` ile birleştirmek istediğinizde kullanabilirsiniz: - -```php -$database->query('SELECT * FROM users WHERE ?or', [ - 'name' => 'John', - 'email' => 'john@example.com', -]); -// SELECT * FROM users WHERE `name` = 'John' OR `email` = 'john@example.com' -``` - - -Özel Değerler -------------- - -Yaygın skaler türlerin (string, int, bool) yanı sıra parametre olarak özel değerler de aktarabilirsiniz: - -- dosyalar: `fopen('image.gif', 'r')` dosyanın ikili içeriğini ekler -- tarih ve saat: `DateTimeInterface` nesneleri veritabanı biçimine dönüştürülür -- enum türleri: `enum` örnekleri değerlerine dönüştürülür -- SQL sabit değerleri: `Connection::literal('NOW()')` ile oluşturulanlar doğrudan sorguya eklenir - -```php -$database->query('INSERT INTO articles ?', [ - 'title' => 'My Article', - 'published_at' => new DateTimeImmutable, // ya da new DateTime - 'content' => fopen('image.png', 'r'), - 'state' => Status::Draft, -]); -``` - -`datetime` veri türünü yerel olarak desteklemeyen veritabanlarında (SQLite ve Oracle gibi), `DateTime` ve `DateTimeImmutable` nesneleri, [veritabanı yapılandırmasında|configuration] `formatDateTime` öğesiyle belirtilen bir değere dönüştürülür (varsayılan değer `U`, yani Unix zaman damgasıdır). - - -SQL Sabit Değerleri -------------------- - -Bazı durumlarda, dize olarak ele alınıp kaçışlanmaması gereken ham SQL kodunu değer olarak aktarmanız gerekir. Bunun için `Nette\Database\SqlLiteral` sınıfının nesneleri kullanılır. `Connection::literal()` metoduyla oluşturulurlar. - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - 'year >' => $database::literal('YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) -``` - -Alternatif olarak: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) -``` - -SQL sabit değerleri parametre içerebilir: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > ? AND year < ?', $min, $max), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) -``` - -Bu, ilginç bileşimlere olanak tanır: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('?or', [ - 'active' => true, - 'role' => $role, - ]), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (`active` = 1 OR `role` = 'admin') -``` - - -Veri Alma -========= - - -SELECT Sorguları İçin Kısayollar --------------------------------- - -Veri almayı kolaylaştırmak için `Connection`, bir `query()` çağrısıyla ardından gelen bir `fetch*()` çağrısını birleştiren çeşitli kısayollar sunar. Bu metotlar `query()` ile aynı parametreleri, yani bir SQL sorgusunu ve isteğe bağlı parametreleri alır. `fetch*()` metotlarının tam açıklaması [aşağıda |#fetch()] bulunabilir. - -| `fetch($sql, ...$params): ?Row` | Sorguyu çalıştırır ve ilk satırı `Row` nesnesi ya da `null` olarak döndürür. -| `fetchAll($sql, ...$params): array` | Sorguyu çalıştırır ve tüm satırları `Row` nesnelerinden oluşan bir dizi olarak döndürür. -| `fetchPairs($sql, ...$params): array` | Sorguyu çalıştırır ve ilişkisel bir dizi (anahtar => değer çiftleri) döndürür. -| `fetchField($sql, ...$params): mixed` | Sorguyu çalıştırır ve ilk satırdaki ilk sütunun değerini döndürür. -| `fetchList($sql, ...$params): ?array` | Sorguyu çalıştırır ve ilk satırı indeksli bir dizi ya da `null` olarak döndürür. - -Örnek: - -```php -// fetchField() - ilk hücrenin değerini döndürür -$count = $database->query('SELECT COUNT(*) FROM articles') - ->fetchField(); -``` - - -`foreach` - Satırlar Üzerinde Yineleme --------------------------------------- - -Bir sorgu çalıştırıldıktan sonra, sonuçları çeşitli yollarla dolaşmayı sağlayan bir [ResultSet|api:Nette\Database\ResultSet] nesnesi döndürülür. Bir sorguyu çalıştırıp satırları almanın en kolay yolu, `foreach` döngüsüyle dolaşmaktır. Bu yöntem bellek açısından en verimli olanıdır; veriyi satır satır getirir ve sonuç kümesinin tamamını belleğe bir kerede yüklemez. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; - // ... -} -``` - -.[note] -`ResultSet` yalnızca bir kez dolaşılabilir. Defalarca dolaşmanız gerekiyorsa, veriyi önce örneğin `fetchAll()` metoduyla bir diziye yüklemelisiniz. - - -fetch(): ?Row .[method] ------------------------ - -Bir satırı `Row` nesnesi olarak döndürür. Başka satır kalmadıysa `null` döndürür. İç işaretçiyi sonraki satıra ilerletir. - -```php -$result = $database->query('SELECT * FROM users'); -$row = $result->fetch(); // ilk satırı yükler -if ($row) { - echo $row->name; -} -``` - - -fetchAll(): array .[method] ---------------------------- - -`ResultSet` içinde kalan tüm satırları `Row` nesnelerinden oluşan bir dizi olarak döndürür. - -```php -$result = $database->query('SELECT * FROM users'); -$rows = $result->fetchAll(); // tüm satırları yükler -foreach ($rows as $row) { - echo $row->name; -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Sonuç kümesini ilişkisel bir dizi olarak döndürür. İlk argüman anahtar olarak kullanılacak sütunu, ikinci argüman ise değer olarak kullanılacak sütunu belirtir: - -```php -$result = $database->query('SELECT id, name FROM users'); -$names = $result->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Yalnızca ilk parametre (`$key`) verilirse, değer olarak satırın tamamı (`Row` nesnesi) kullanılır: - -```php -$rows = $result->fetchPairs('id'); -// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...] -``` - -Anahtarlar yinelenirse son satırdaki değer kullanılır. Anahtar olarak `null` kullanmak, sayısal indeksli (sıfırdan başlayan) bir dizi verir ve anahtar çakışmalarını önler: - -```php -$names = $result->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Alternatif olarak, her satırı işleyen bir callback verebilirsiniz. Callback tek bir değer ya da bir anahtar-değer çifti döndürebilir. - -```php -$result = $database->query('SELECT * FROM users'); -$items = $result->fetchPairs(fn($row) => "$row->id - $row->name"); -// ['1 - John', '2 - Jane', ...] - -// Callback, anahtar ve değer çiftinden oluşan bir dizi de döndürebilir: -$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); -// ['John' => 46, 'Jane' => 21, ...] -``` - - -fetchField(): mixed .[method] ------------------------------ - -Geçerli satırdaki ilk sütunun değerini döndürür. Başka satır kalmadıysa `null` döndürür. İç işaretçiyi sonraki satıra ilerletir. - -```php -$result = $database->query('SELECT name FROM users'); -$name = $result->fetchField(); // ilk satırdaki name değerini yükler -``` - - -fetchList(): ?array .[method] ------------------------------ - -Satırı indeksli bir dizi olarak döndürür. Başka satır kalmadıysa `null` döndürür. İç işaretçiyi sonraki satıra ilerletir. - -```php -$result = $database->query('SELECT name, email FROM users'); -$row = $result->fetchList(); // ['John', 'john@example.com'] -``` - - -getRowCount(): ?int .[method] ------------------------------ - -Son `UPDATE` ya da `DELETE` sorgusundan etkilenen satır sayısını döndürür. `SELECT` sorgularında sonuç kümesindeki satır sayısını döndürür. Ancak bu her zaman bilinmeyebilir; o durumda metot `null` döndürür. - - -getColumnCount(): ?int .[method] --------------------------------- - -`ResultSet` içindeki sütun sayısını döndürür. - - -Sorgu Bilgisi -============= - -Hata ayıklama amacıyla, son çalıştırılan sorgu hakkında bilgi alabiliriz: - -```php -echo $database->getLastQueryString(); // SQL sorgusunu yazdırır - -$result = $database->query('SELECT * FROM articles'); -echo $result->getQueryString(); // SQL sorgusunu yazdırır -echo $result->getTime(); // çalışma süresini saniye cinsinden yazdırır -``` - -Sonucu bir HTML tablosu olarak göstermek için şunu kullanabilirsiniz: - -```php -$result = $database->query('SELECT * FROM articles'); -$result->dump(); -``` - -`ResultSet`, sütun türleri hakkında bilgi sağlar: - -```php -$result = $database->query('SELECT * FROM articles'); -$types = $result->getColumnTypes(); - -foreach ($types as $column => $type) { - echo "$column is of type $type"; // örneğin 'id is of type int' -} -``` - - -Sorgu Günlükleme ----------------- - -Kendi sorgu günlüklememizi gerçekleştirebiliriz. `onQuery` olayı, çalıştırılan her sorgudan sonra çağrılan callback'lerden oluşan bir dizidir: - -```php -$database->onQuery[] = function ($database, $result) use ($logger) { - $logger->info('Query: ' . $result->getQueryString()); - $logger->info('Time: ' . $result->getTime()); - - if ($result->getRowCount() > 1000) { - $logger->warning('Large result set: ' . $result->getRowCount() . ' rows'); - } -}; -``` diff --git a/database/tr/transactions.texy b/database/tr/transactions.texy deleted file mode 100644 index b71d4d4f87..0000000000 --- a/database/tr/transactions.texy +++ /dev/null @@ -1,45 +0,0 @@ -Transaction'lar -*************** - -.[perex] -Transaction'lar, transaction içindeki işlemlerin ya hepsinin çalıştırılmasını ya da hiçbirinin çalıştırılmamasını güvence altına alır. Karmaşık işlemlerde veri tutarlılığını sağlamak için işe yararlar. - -Transaction kullanmanın en basit yolu şöyle görünür: - -```php -$database->beginTransaction(); -try { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); - $database->commit(); -} catch (\Exception $e) { - $database->rollBack(); - throw $e; -} -``` - -Aynı sonuca `transaction()` metoduyla çok daha şık biçimde ulaşabilirsiniz. Bu metot, transaction içinde çalıştırılan bir callback alır. Callback istisna fırlatmadan çalışırsa transaction otomatik olarak commit edilir. Bir istisna oluşursa transaction geri alınır ve istisna daha ileriye yayılır. - -```php -$database->transaction(function ($database) use ($id) { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); -}); -``` - -`transaction()` çağrıları iç içe geçebilir; bu da her biri kendi transaction'ını yöneten metotları birleştirmeyi kolaylaştırır. Veritabanına `BEGIN`/`COMMIT` olarak yalnızca en dıştaki transaction gönderilir; içteki çağrılar yalnızca iç içe geçme derinliğini izler. Bir `transaction()` callback'inin içinde `beginTransaction()`, `commit()` ya da `rollBack()` metotlarını elle çağırmak `LogicException` fırlatır. - -`transaction()` metodu değer de döndürebilir: - -```php -$count = $database->transaction(function ($database) { - $result = $database->query('UPDATE users SET active = ?', true); - return $result->getRowCount(); // güncellenen satır sayısını döndürür -}); -``` diff --git a/database/tr/type-conversion.texy b/database/tr/type-conversion.texy deleted file mode 100644 index 11efe21987..0000000000 --- a/database/tr/type-conversion.texy +++ /dev/null @@ -1,55 +0,0 @@ -Tür Dönüşümü -************ - -.[perex] -Nette Database, veritabanından dönen değerleri karşılık gelen PHP türlerine otomatik olarak dönüştürür. - - -Tarih ve Saat -------------- - -Zaman değerleri `Nette\Utils\DateTime` nesnelerine dönüştürülür. Zaman değerlerinin değişmez `Nette\Database\DateTime` nesnelerine dönüştürülmesini istiyorsanız, [yapılandırmada |configuration] `newDateTime` seçeneğini true yapın. - -```php -$row = $database->fetch('SELECT created_at FROM articles'); -echo $row->created_at instanceof DateTime; // true -echo $row->created_at->format('j. n. Y'); -``` - -MySQL'de `TIME` veri türü `DateInterval` nesnelerine dönüştürülür. - - -Boolean Değerler ----------------- - -Boolean değerler otomatik olarak `true` ya da `false` değerine dönüştürülür. MySQL'de, [yapılandırmada |configuration] `convertBoolean` ayarlanırsa `TINYINT(1)` dönüştürülür. - -```php -$row = $database->fetch('SELECT is_published FROM articles'); -echo gettype($row->is_published); // 'boolean' -``` - - -Sayısal Değerler ----------------- - -Sayısal değerler, veritabanındaki sütun türüne göre `int` ya da `float` değerine dönüştürülür: - -```php -$row = $database->fetch('SELECT id, price FROM products'); -echo gettype($row->id); // integer -echo gettype($row->price); // float -``` - - -Özel Normalleştirme -------------------- - -`setRowNormalizer(?callable $normalizer)` metoduyla, veritabanından gelen satırları dönüştürmek için özel bir fonksiyon ayarlayabilirsiniz. Bu, örneğin otomatik veri türü dönüşümü için işe yarar. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // tür dönüşümü burada gerçekleşir - return $row; -}); -``` diff --git a/database/tr/upgrading.texy b/database/tr/upgrading.texy deleted file mode 100644 index 85eee5b017..0000000000 --- a/database/tr/upgrading.texy +++ /dev/null @@ -1,36 +0,0 @@ -Yükseltme -********* - - -Sürüm 3.2'ye Yükseltme -====================== - -Gereken en düşük PHP sürümü 8.1'dir. - -Kod, PHP 8.1 için özenle ayarlandı. Metotlar ve özellikler için tüm yeni tür bildirimleri eklendi. Değişiklikler küçüktür: - -- MySQL: sıfır tarih `0000-00-00` artık `null` olarak döndürülüyor -- MySQL: ondalık basamağı olmayan decimal, float yerine int olarak döndürülüyor -- `time` türü, tarihi geçerli tarih yerine `0001-01-01` olarak ayarlanmış bir `DateTime` nesnesi olarak döndürülüyor - - -Sürüm 3.1'e Yükseltme -===================== - -- `Nette\Database\Context` sınıfının adı, [Database Explorer|explorer] adıyla tutarlı olsun diye `Nette\Database\Explorer` olarak değişti -- `Nette\Database\IRow` ve `Nette\Database\IRowContainer` arayüzleri gereksiz oldukları için kullanımdan kaldırıldı sayılıyor -- `MySqlDriver` sürücüsü alt sorgular kullanıyor -- SQL deyimi çevirici, dizilerin nerelere aktarılabileceğini daha iyi denetliyor - - -Sürüm 3.0'a Yükseltme -===================== - -`fetch()` ya da `fetchField()` gibi bazı metotlar, sonraki satır yoksa artık `false` yerine `null` döndürüyor. - - -Sürüm 2.3'e Yükseltme -===================== - -- `MySqlDriver`, MySQL >= 5.5.3 için varsayılan olarak `utf8` yerine `utf8mb4` kodlamasını kullanıyor -- `IReflection`, ikiz arayüzler `IStructure` ve `IConventions` olarak bölündü diff --git a/dependency-injection/cs/@home.texy b/dependency-injection/cs/@home.texy deleted file mode 100644 index be7a748b7b..0000000000 --- a/dependency-injection/cs/@home.texy +++ /dev/null @@ -1,25 +0,0 @@ -Nette DI -******** - -.[perex] -Dependency Injection je návrhový vzor, který zásadně změní váš pohled na kód a vývoj. Otevře vám cestu do světa čistě navržených a udržitelných aplikací. - -- [Co je Dependency Injection? |introduction] -- [Globální stav a singletony |global-state] -- [Předávání závislostí |passing-dependencies] -- [Co je DI kontejner? |container] -- [Často kladené otázky|faq] - - -Balíček `nette/di` poskytuje nesmírně pokročilý kompilovaný DI kontejner pro PHP. - -- [Nette DI Container |nette-container] -- [Konfigurace |configuration] -- [Definování služeb |services] -- [Autowiring |autowiring] -- [Generované továrny |factory] -- [Tvorba rozšíření pro Nette DI |extensions] -- [Kompilace kontejneru do hloubky |compilation-internals] - -{{maintitle: Nette DI: Dependency Injection kontejner pro PHP}} -{{description: Zjistěte, co je dependency injection a proč změní váš pohled na kód, a pak využijte Nette DI, vyspělý kompilovaný DI kontejner pro PHP.}} diff --git a/dependency-injection/cs/@left-menu.texy b/dependency-injection/cs/@left-menu.texy deleted file mode 100644 index 9f2bef9040..0000000000 --- a/dependency-injection/cs/@left-menu.texy +++ /dev/null @@ -1,30 +0,0 @@ -Dependency Injection -******************** -- [Co je DI? |introduction] -- [Globální stav a singletony |global-state] -- [Předávání závislostí |passing-dependencies] -- [Co je DI kontejner? |container] -- [Často kladené otázky|faq] - - -Nette DI --------- -- [Nette DI Container |nette-container] -- [Konfigurace |configuration] -- [Definování služeb |services] -- [Autowiring |autowiring] -- [Generované továrny |factory] -- [Tvorba rozšíření pro Nette DI |extensions] -- [Kompilace do hloubky |compilation-internals] -- [Upgrade |upgrading] - -- "Kurz: DI by Example .[link-external]":https://github.com/nette-examples/di-by-example .{padding-top:1em} - - -Další četba -*********** -- [Dokumentace Nette |nette:] -- [Aplikace v Nette |application:how-it-works] -- [Utilities |utils:] -- [Návody a postupy |best-practices:] -- [Řešení problémů |nette:troubleshooting] diff --git a/dependency-injection/cs/@meta.texy b/dependency-injection/cs/@meta.texy deleted file mode 100644 index 462d9add80..0000000000 --- a/dependency-injection/cs/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokumentace}} diff --git a/dependency-injection/cs/autowiring.texy b/dependency-injection/cs/autowiring.texy deleted file mode 100644 index baea4a2a6f..0000000000 --- a/dependency-injection/cs/autowiring.texy +++ /dev/null @@ -1,282 +0,0 @@ -Autowiring -********** - -.[perex] -Autowiring je skvělá vlastnost, která umí automaticky předávat do konstruktoru a dalších metod požadované služby, takže je nemusíme vůbec psát. Ušetří vám spoustu času. - -Díky tomu můžeme vynechat naprostou většinu argumentů při psaní definic služeb. Místo: - -```neon -services: - articles: Model\ArticleRepository(@database, @cache.storage) -``` - -Stačí napsat: - -```neon -services: - articles: Model\ArticleRepository -``` - -Autowiring se řídí podle typů, takže aby fungoval, musí být třída `ArticleRepository` definována asi takto: - -```php -namespace Model; - -class ArticleRepository -{ - public function __construct(\PDO $db, \Nette\Caching\Storage $storage) - {} -} -``` - -Autowiring nikdy nepoužívá názvy služeb. Řídí se výhradně typovým systémem PHP, takže ví i to, že třída vyhovuje rozhraním, která implementuje, a třídám, ze kterých dědí. Díky tomu je název služby jen pomocný identifikátor a jeho přejmenování v aplikaci nic nerozbije. - -Aby bylo možné použít autowiring, musí pro každý typ být v kontejneru **právě jedna služba**. Pokud by jich bylo víc, autowiring by nevěděl, kterou z nich předat, a vyhodil by výjimku: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - tempDb: PDO('sqlite::memory:') - articles: Model\ArticleRepository # VYHODÍ VÝJIMKU, vyhovuje mainDb i tempDb -``` - -Řešením by bylo autowiring obejít a explicitně uvést název služby (např. `articles: Model\ArticleRepository(@mainDb)`). Šikovnější ale je autowirování jedné ze služeb [vypnout |#Vypnutí autowiringu], nebo jednu ze služeb [upřednostnit |#Preference autowiringu]. - - -Vypnutí autowiringu -------------------- - -Autowirování služby můžeme vypnout pomocí volby `autowired: false`: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - - tempDb: - create: PDO('sqlite::memory:') - autowired: false # služba tempDb je vyřazena z autowiringu - - articles: Model\ArticleRepository # tudíž předá do konstruktoru mainDb -``` - -Služba `articles` nevyhodí výjimku, že existují dvě vyhovující služby typu `PDO` (tj. `mainDb` a `tempDb`), které lze do konstruktoru předat, protože vidí jen službu `mainDb`. - -Autowiring lze také globálně vypnout pro celé typy pomocí konfigurační volby [`di › excluded` |configuration#DI], která vyjmenovává typy (a jejich potomky), které se nikdy nemají autowirovat. - -.[note] -Konfigurace autowiringu v Nette funguje odlišně než v Symfony, kde volba `autowire: false` říká, že se nemá autowiring používat pro argumenty konstruktoru dané služby. V Nette se autowiring používá vždy, ať už pro argumenty konstruktoru, nebo kterékoliv jiné metody. Volba `autowired: false` říká, že instance dané služby nemá být pomocí autowiringu nikam předávána. - - -Preference autowiringu ----------------------- - -Pokud máme více služeb stejného typu a u jedné z nich uvedeme volbu `autowired`, stává se tato služba preferovanou: - -```neon -services: - mainDb: - create: PDO(%dsn%, %user%, %password%) - autowired: PDO # stává se preferovanou - - tempDb: - create: PDO('sqlite::memory:') - - articles: Model\ArticleRepository -``` - -Služba `articles` nevyhodí výjimku, že existují dvě vyhovující služby typu `PDO` (tj. `mainDb` a `tempDb`), ale použije preferovanou službu, tedy `mainDb`. - - -Pole služeb ------------ - -Autowiring umí předávat i pole služeb určitého typu. Protože v PHP nelze nativně zapsat typ položek pole, je třeba kromě typu `array` doplnit i phpDoc komentář s typem položky ve tvaru `ClassName[]`: - -```php -namespace Model; - -class ShipManager -{ - /** - * @param Shipper[] $shippers - */ - public function __construct(array $shippers) - {} -} -``` - -DI kontejner pak automaticky předá pole služeb odpovídajících danému typu. Vynechá služby, které mají [vypnutý autowiring |#Vypnutí autowiringu], a nikdy do kolekce nezahrne službu, která se právě vytváří. Na rozdíl od předávání jednotlivé služby zde [zúžení |#Zúžení autowiringu] autowiringu na konkrétní typ ani označení služby jako [preferované |#Preference autowiringu] nehraje roli; pole vždy obsahuje všechny služby daného typu. - -Typ v komentáři může být také ve tvaru `array<int, Class>` nebo `list<Class>`. Pokud nemůžete ovlivnit podobu phpDoc komentáře, můžete předat pole služeb přímo v konfiguraci pomocí [`typed()` |services#Speciální funkce]. - - -Skalární argumenty ------------------- - -Autowiring umí dosazovat pouze objekty a pole objektů. Skalární argumenty (např. řetězce, čísla, booleany) [zapíšeme v konfiguraci |services#Argumenty]. Alternativou je vytvořit [settings-objekt |best-practices:passing-settings-to-presenters], který skalární hodnotu (nebo více hodnot) zapouzdří do podoby objektu, a ten pak lze opět předávat pomocí autowiringu. - -```php -class MySettings -{ - public function __construct( - // readonly je možné použít od PHP 8.1 - public readonly bool $value, - ) - {} -} -``` - -Vytvoříte z něj službu přidáním do konfigurace: - -```neon -services: - - MySettings('any value') -``` - -Ostatní třídy si jej pak mohou vyžádat pomocí autowiringu. - - -Volitelné závislosti --------------------- - -Pokud má parametr konstruktoru nebo metody výchozí hodnotu a v kontejneru neexistuje žádná služba požadovaného typu, autowiring nevyhodí výjimku, ale argument jednoduše vynechá, takže se použije výchozí hodnota. Takto deklarujete volitelné závislosti: - -```php -class Foo -{ - public function __construct( - private ?Logger $logger = null, - ) {} -} -``` - -Naproti tomu u parametru bez výchozí hodnoty chybějící služba vždy způsobí výjimku. - - -Zúžení autowiringu ------------------- - -Jednotlivým službám lze autowiring zúžit jen na určité třídy nebo rozhraní. - -Normálně autowiring službu předá do každého parametru metody, jehož typu služba odpovídá. Zúžení znamená, že stanovíme podmínky, kterým musí typy uvedené u parametrů metod vyhovovat, aby jim byla služba předána. - -Ukážeme si to na příkladu: - -```php -class ParentClass -{} - -class ChildClass extends ParentClass -{} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Pokud bychom je všechny zaregistrovali jako služby, tak by autowiring selhal: - -```neon -services: - parent: ParentClass - child: ChildClass - parentDep: ParentDependent # VYHODÍ VÝJIMKU, vyhovují služby parent i child - childDep: ChildDependent # autowiring předá do konstruktoru službu child -``` - -Služba `parentDep` vyhodí výjimku `Multiple services of type ParentClass found: child, parent`, protože do jejího konstruktoru pasují obě služby `parent` i `child`, a autowiring nemůže rozhodnout, kterou z nich zvolit. - -U služby `child` můžeme proto zúžit její autowirování na typ `ChildClass`: - -```neon -services: - parent: ParentClass - child: - create: ChildClass - autowired: ChildClass # lze napsat i 'autowired: self' - - parentDep: ParentDependent # autowiring předá do konstruktoru službu parent - childDep: ChildDependent # autowiring předá do konstruktoru službu child -``` - -Nyní se do konstruktoru služby `parentDep` předá služba `parent`, protože teď je to jediný vyhovující objekt. Službu `child` už tam autowiring nepředá. Ano, služba `child` je stále typu `ParentClass`, ale zužující podmínka `autowired: ChildClass` znamená, že se předá jen do parametrů explicitně typovaných jako `ChildClass` (nebo jeho podtypy). Protože `ParentDependent` vyžaduje `ParentClass`, služba `child` už tam pro autowiring nepřipadá v úvahu. - -U služby `child` by bylo možné `autowired: ChildClass` zapsat také jako `autowired: self`, jelikož `self` je zástupné označení pro třídu aktuální služby. - -V klíči `autowired` je možné uvést i několik tříd nebo interfaců jako pole: - -```neon -autowired: [ParentClass, FooInterface] -``` - -Zkusme příklad doplnit ještě o rozhraní: - -```php -interface FooInterface -{} - -interface BarInterface -{} - -class ParentClass implements FooInterface -{} - -class ChildClass extends ParentClass implements BarInterface -{} - -class FooDependent -{ - function __construct(FooInterface $obj) - {} -} - -class BarDependent -{ - function __construct(BarInterface $obj) - {} -} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Když službu `child` nijak neomezíme, bude pasovat do konstruktorů všech tříd `FooDependent`, `BarDependent`, `ParentDependent` i `ChildDependent` a autowiring ji tam předá. - -Pokud její autowiring ale zúžíme na `ChildClass` pomocí `autowired: ChildClass` (nebo `self`), předá ji autowiring pouze do konstruktoru `ChildDependent`, protože vyžaduje argument typu `ChildClass` a platí, že `ChildClass` *je typu* `ChildClass`. Žádný z požadovaných typů ostatních parametrů není `ChildClass` ani jeho podtyp, takže se jim služba nepředá. - -Pokud jej omezíme na `ParentClass` pomocí `autowired: ParentClass`, předá ji autowiring opět do konstruktoru `ChildDependent` (protože vyžadovaný `ChildClass` je podtyp `ParentClass`) a nově i do konstruktoru `ParentDependent`, protože vyžadovaný typ `ParentClass` je taktéž vyhovující. - -Pokud jej omezíme na `FooInterface`, bude stále autowirovaná do `ParentDependent` (vyžadovaný `ParentClass` je podtyp `FooInterface`) a `ChildDependent`, a navíc i do konstruktoru `FooDependent`, nikoliv však do `BarDependent`, neboť `BarInterface` není podtyp `FooInterface`. - -```neon -services: - child: - create: ChildClass - autowired: FooInterface - - fooDep: FooDependent # autowiring předá do konstruktoru child - barDep: BarDependent # VYHODÍ VÝJIMKU, žádná služba nevyhovuje - parentDep: ParentDependent # autowiring předá do konstruktoru child - childDep: ChildDependent # autowiring předá do konstruktoru child -``` - -{{maintitle: Autowiring v Nette DI: automatické předávání služeb}} -{{description: Nette DI za vás předá služby do konstruktorů a dalších metod. Jak autowiring vypnout nebo zúžit, upřednostnit některou službu a předat kolekci služeb.}} diff --git a/dependency-injection/cs/compilation-internals.texy b/dependency-injection/cs/compilation-internals.texy deleted file mode 100644 index 4c353646c1..0000000000 --- a/dependency-injection/cs/compilation-internals.texy +++ /dev/null @@ -1,225 +0,0 @@ -Kompilace kontejneru do hloubky -******************************* - -.[perex] -Tato stránka odkrývá, co se děje, když Nette sestavuje váš DI kontejner: jakými fázemi prochází, kdy se rozbalují konfigurační parametry, kdy se z řetězců `@service` stávají skutečné reference a - to je otázka, kterou si autoři rozšíření kladou nejčastěji - ve které fázi můžete bezpečně hledat služby podle typu. Je to hlubší doplněk k [tvorbě rozšíření |extensions]. - -Nic z toho nepotřebujete k napsání běžné aplikace, ani běžného rozšíření. Jakmile ale vaše rozšíření začne zkoumat nebo přetvářet graf služeb, začne na načasování záležet všechno: totéž volání `getByType()` dá v jedné fázi spolehlivou odpověď a v jiné zavádějící. Tato stránka vysvětluje proč, abyste vždy věděli, kam váš kód patří. - - -Dva světy: kompilace vs. běh aplikace -===================================== - -Nejdůležitější věc k pochopení je, že se Nette kontejner **nesestavuje při každém requestu**. Sestaví se jednou do optimalizované PHP třídy, ta se uloží na disk a každý další request pak hotový soubor už jen načte přes `include`. Celý mechanismus popsaný níže - rozšíření, resolvery, generátor kódu - běží **jen při (re)kompilaci**. - -To rozděluje svět na dvě reprezentace, které nikdy neexistují zároveň: - -| | při kompilaci | za běhu -|---|---|--- -| Co existuje | **definice** (recepty) v `ContainerBuilder` | **instance** služeb v `Container` -| Klíčové třídy | `Compiler`, `ContainerBuilder`, `Resolver`, `PhpGenerator` | `Container` (rodič vygenerované třídy) -| `%param%`, `@service` | textové značky, které se stále překládají | už přeložené / zapečené do kódu - -Vygenerovaná třída dědí z `Nette\DI\Container` a pro každou službu má metodu `createServiceXxx()`. Její parametry i autowiring metadata jsou předpočítané, takže za běhu už není co řešit - jen na požádání vytvořit instance služeb. - -.[note] -V debug módu se kontejner překompiluje automaticky, kdykoli se změní konfigurační soubor nebo třída rozšíření; obojí se sleduje jako závislost. V produkci se zkompiluje jednou a už se nikdy nekontroluje, a právě odtud plyne rychlost. - - -Fáze v kostce -============= - -Kompilaci řídí `Compiler::compile()` a scvrkává se na tři kroky: - -```php -public function compile(): string -{ - $this->processExtensions(); // FÁZE A: schémata + loadConfiguration() - $this->processBeforeCompile(); // FÁZE B: resolve + beforeCompile() + complete - return $this->generateCode(); // FÁZE C: generování kódu + afterCompile() -} -``` - -Celý mentální model se vejde do jediné myšlenky - **každá fáze ví víc než ta předchozí:** - -- **Fáze A** naplní graf definicemi. Typy služeb **ještě nejsou spolehlivě známé**, protože typ může plynout z návratové hodnoty továrny, na kterou se zatím nikdo nepodíval. -- **Fáze B** nejdřív vyřeší všechny typy (`resolve`), pak dá rozšířením šanci graf přetvořit (`beforeCompile`) a nakonec [autowiruje |autowiring] argumenty (`complete`). -- **Fáze C** z hotového grafu vygeneruje PHP a nechá rozšíření sáhnout do vygenerovaného kódu. - -Právě tato rostoucí znalost je důvod, proč je táž operace v jedné fázi bezpečná a v jiné nespolehlivá. Zbytek stránky prochází fáze právě touto optikou. - - -Fáze A: registrace definic -========================== - -V této fázi Nette volá na každém rozšíření tři metody - `getConfigSchema()`, pak `setConfig()` a nakonec `loadConfiguration()` - ale v **pečlivě řízeném pořadí**, protože tady na pořadí opravdu záleží. - - -Proč na pořadí záleží ---------------------- - -- **`ParametersExtension` a `ExtensionsExtension` jdou první.** První musí proběhnout dřív než cokoli jiného, aby mohlo rozbalit `%param%` v celé konfiguraci - každé další rozšíření pak dostane svoji sekci s už dosazenými hodnotami. Druhé registruje další rozšíření uvedená v sekci `extensions:`, takže také musí být na světě dřív, než přijdou na řadu ostatní. -- **`ServicesExtension` jde poslední.** Uživatelská sekce `services:` má tak vždy poslední slovo a může přepsat cokoli, co rozšíření nastavila. -- **`InjectExtension` se přesune úplně na konec**, aby jeho práce viděla setupy přidané všemi ostatními rozšířeními. - -Co si z toho odnést: v okamžiku, kdy běží `loadConfiguration()` vašeho rozšíření, jsou parametry už rozbalené, ale uživatelské služby tam ještě nejsou. Tenhle jediný fakt řídí většinu časových pravidel níže. - - -Ze services: na objekty definic -------------------------------- - -Uživatelská sekce `services:` se právě tady, v posledním kroku fáze A, převede na [objekty definic |extensions#Typy definic]. Každý NEON záznam se znormalizuje (sjednotí se zkratkové zápisy), rozpozná se jeho druh (běžná služba, továrna, accessor, ...) a v builderu vznikne odpovídající definice. Tady také poprvé jednoduché argumenty `@name` / `@Type` získávají podobu reference - viz [dále |#Reference: kdy se @service mění na Reference]. - -Na konci fáze A je graf **kompletní co do počtu** - všechna rozšíření i uživatel zaregistrovali, co chtěli - ale obraz ještě není ostrý: - -- **typy nejsou vyřešené** u definic, jejichž typ plyne z návratové hodnoty továrny, -- **argumenty nejsou autowirované**, -- některé reference `@service` jsou stále jen stringy. - -Přesně proto je hledání podle typu tady nespolehlivé - o tom více [dále |#Introspekce ContainerBuilder: kdy je bezpečná]. - - -Parametry: kdy se rozbalují %param% -=================================== - -Jedna ze dvou hlavních otázek. Odpověď je krátká: **jednorázově, na úplném začátku fáze A, přes celý konfigurační strom.** - -`ParametersExtension` běží první a jednou z prvních věcí, které dělá, je rozbalení placeholderů `%param%` - nejdřív uvnitř samotných parametrů (parametr smí odkazovat na jiný), pak v celém zbytku konfigurace. Takže než jakékoli jiné rozšíření, včetně `ServicesExtension`, dostane svou sekci, jsou placeholdery už pryč. Rozšíření pracují s konkrétními hodnotami, nikdy s `%...%`. - -Když placeholder tvoří celý řetězec, vrátí se jeho hodnota *jak je* - včetně polí a objektů - takže se `%mailer%` může rozbalit na celé pole. Kdekoli jinde se konkatenuje do stringu a tečková notace `%foo.bar%` sahá do vnořených polí. - - -Statické vs. dynamické parametry --------------------------------- - -Ne každou hodnotu lze zapéct do kódu. Parametr, jehož hodnota se liší podle prostředí - proměnná prostředí, `baseUrl` odvozená z requestu - musí zůstat **dynamický**. Takové parametry ohlásíte přes `setDynamicParameterNames()` nebo `Expect::...->dynamic()` ve schématu; více v [dynamických parametrech |application:bootstrapping#Dynamické parametry]. - -Dynamický parametr se nenahradí hodnotou, ale výrazem, který ji přečte *až za běhu*. Takže `%env.DB_HOST%` se nezapeče do stringu; stane se z něj runtime přístup ve vygenerovaném kontejneru. Všechno ostatní je statické a zapeče se v čase kompilace - a odtud plyne obvyklé překvapení "moje hodnota z `getenv()` je v každém prostředí stejná": ten parametr byl prostě statický. - -Opačná operace je **escapování**: aby se doslovné `%` nebo `@` nebralo jako placeholder či reference, zdvojí se (`%%`, `@@`). Nette to dělá automaticky u parametrů, které vkládá za vás, takže se jejich hodnoty nikdy nezamění za placeholder nebo referenci. - - -Reference: kdy se @service mění na Reference -============================================ - -Druhá hlavní otázka. Překlad `@service` probíhá **v několika krocích napříč různými fázemi**, podle toho, jak složitý ten řetězec je. Málokdy potřebujete tohle sledovat ručně, ale znalost tvaru vysvětluje, proč se některé reference vyřeší dřív než jiné. - -- **Parsování (načtení configu).** `@service` použitý *jako entita* - to, co službu vytváří, jako ve `Foo(@bar)` - se stane referencí okamžitě. `@service` použitý *jako argument* zůstává prozatím stringem. `@` v uvozovkách se escapuje na `@@`, takže se bere jako doslovný text, ne jako reference. -- **Fáze A (`loadConfiguration`).** Když se zpracovávají definice, čistý argument `@name` nebo `@Type` se překlopí na objekt `Reference`. To chytí jen jednoduché tvary; `@service::CONST` nebo `@` uvnitř složitějšího výrazu jde na později. -- **Fáze B (`complete`).** Tady proběhne skutečný "chytrý" překlad: `@service` → reference, `@service::CONSTANT` → literál konstanty třídy, `@service::property` → čtení té property, `@@x` → doslovný text `@x`. - -Ve slově *reference* se skrývá druhý překlad. `Reference` může ukazovat buď podle **jména**, nebo podle **typu** (`@Namespace\Type`). Typová reference **ještě není jméno služby** - na konkrétní jméno se vyřeší autowiringem, a to až v kroku **complete**, jakmile je postavený autowiring index. To je můstek k další sekci: vyhledávání autowiringem se záměrně odkládá, dokud není index hotový. - -| Tvar | Na referenci/výraz ve fázi | Na konkrétní službu ve fázi -|---|---|--- -| entita (`@foo` jako továrna) | parsování | complete -| argument `@foo`, `@Type` | fáze A | complete -| `@foo::CONST`, `@foo::prop` | fáze B | complete -| typová reference `@Type` | fáze A/B | complete (autowiring) - - -Introspekce ContainerBuilder: kdy je bezpečná -============================================= - -Teď otázka, kterou si autoři rozšíření kladou nejčastěji: **ve které metodě můžu hledat služby podle typu?** Odpověď plyne z jednoho prostého pravidla o tom, jak si builder hlídá svůj vlastní stav. - -Hledání **podle typu** (`getByType()`, `getDefinitionByType()`, `findByType()`) vyžaduje, aby byl graf služeb *vyřešený* - každý typ známý, autowiring index postavený. Takže kdykoli některé z nich zavoláte a graf se od posledního resolve změnil, builder **na místě vyřeší celý dosud známý graf**. Během samotného resolve je jakékoli hledání podle typu zakázané a vyhodí `NotAllowedDuringResolvingException`. - -Hledání **podle tagu** (`findByTag()`) takový požadavek nemá - tagy na typech nezávisí, takže funguje **v každé fázi**. - -Fáze po fázi: - -- **`loadConfiguration()` (fáze A) - hledání podle typu je nespolehlivé.** Graf je neúplný: rozšíření, která běží později, ještě neregistrovala své služby, a hlavně tu ještě není uživatelská `services:` (ta běží poslední). Volání `getByType()` sice funguje - spustí předčasný resolve části grafu - ale odpověď je z neúplného obrazu a předčasný resolve stojí výkon. Pravidlo: **v `loadConfiguration()` jen registrujte definice; nehledejte podle typu.** `findByTag()` je v pořádku. -- **`beforeCompile()` (fáze B) - správné místo pro introspekci.** Teď už existují **všechny** definice (i uživatelské), **typy jsou vyřešené** a **autowiring index je postavený**, takže `getByType()`, `findByType()` i `findByTag()` vrací **spolehlivé** odpovědi. Argumenty *ještě nejsou* autowirované - to je až úplně další krok (`complete`), po všech voláních `beforeCompile()`. Když tu definici změníte, další `getByType()` graf transparentně přeresolvuje, takže můžete volně střídat úpravy a dotazy. -- **`afterCompile()` (fáze C) - už jen kód.** Pracuje nad vygenerovanou třídou, ne nad builderem. Graf je hotový; tady tvarujete výsledné PHP. - -| Chci... | Fáze -|---|--- -| zaregistrovat službu | `loadConfiguration()` -| hledat podle **tagu** a upravit definice | `loadConfiguration()` nebo `beforeCompile()` -| hledat podle **typu** (`getByType`/`findByType`) | **`beforeCompile()`** -| zjistit, které služby autowiring dosadil do argumentů | při kompilaci ne - až za běhu -| sáhnout do generovaného kódu | `afterCompile()` -| spustit kód po startu kontejneru | [inicializační kód |extensions#Inicializační kód] - - -Uvnitř fáze B: resolve a complete -================================= - -Fáze B jsou dva průchody s voláními `beforeCompile()` vloženými mezi ně: - -```php -$this->builder->resolve(); // typy vyřešené, autowiring index postavený -foreach ($this->extensions as $extension) { - $extension->beforeCompile(); -} -$this->builder->complete(); // AŽ TEĎ se autowirují argumenty -``` - -**`resolve()`** určí typ každé služby - vezme se z jejího `type`, nebo se odvodí z její továrny: z návratového typu tovární metody, z vytvářené třídy nebo ze služby, na kterou míří reference - a pak postaví autowiring index, který mapuje každý typ (třídu plus její rodiče a rozhraní) na jméno služby. Služba označená `autowired: false` se do indexu nezanese; `autowired: [A, B]` zúží typy, pod kterými je viditelná. Klíčové: resolve řeší *typy*, ne *argumenty* - autowiring argumentů by potřeboval hotový index, který existuje až po tomto průchodu. - -**`complete()`** je místo, kde se autowiring argumentů skutečně provede. Pro každou definici doplní chybějící argumenty konstruktoru a setupů tím, že jejich typy dohledá v nyní už hotovém indexu. Právě proto se typové reference nechávaly během resolve nevyřešené: to dohledání patří sem, jakmile je do čeho spolehlivě nahlížet. - - -Fáze C: generování kódu -======================= - -`generateCode()` předá hotový graf `PhpGenerator`u, který vytvoří třídu dědící z `Container` s metodou `createServiceXxx()` pro každou službu a s předpočítanými metadaty `aliases`, `tags` a `wiring`. Každý `Statement` se stane PHP textem (`new Foo(...)`, volání metod, přístup k property) a každá `Reference` voláním `$this->getService(...)`. - -Rozšíření pak dostanou závěrečný průchod `afterCompile()` nad vygenerovanou třídou - tady se například vygenerují gettery statických a dynamických parametrů - plus možnost přidat [inicializační kód |extensions#Inicializační kód], který běží při každém requestu. - - -Časová osa na jednom obrázku -============================ - -``` -KOMPILACE (jednou, do cache) -│ -├─ načtení config souborů NEON -> Statement/pole; merge souborů -│ @ v uvozovkách -> @@ ; entity -> Statement -│ -▼ Compiler::compile() -│ -├─ FÁZE A processExtensions() -│ ├─ ParametersExtension (PRVNÍ) ── %param% ROZBALENY v celém configu -│ │ dynamické -> runtime výraz -│ ├─ ExtensionsExtension (PRVNÍ) ── registruje další rozšíření -│ ├─ ...ostatní rozšíření... ── loadConfiguration(): jen registrujte definice -│ └─ ServicesExtension (POSLEDNÍ)── services: -> objekty Definition -│ @name/@Type -> Reference -│ [graf úplný co do počtu; TYPY a ARGUMENTY ještě ne; hledání podle typu nespolehlivé] -│ -├─ FÁZE B processBeforeCompile() -│ ├─ builder.resolve() ── vyřeš všechny typy; postav autowiring index -│ │ [typy hotové; index hotový] -│ ├─ beforeCompile() rozšíření ── ZDE bezpečné getByType/findByType/findByTag -│ │ (argumenty ještě nejsou autowirované) -│ └─ builder.complete() ── autowiruj ARGUMENTY; dokonči překlad referencí -│ typové reference -> jména služeb -│ -└─ FÁZE C generateCode() - ├─ PhpGenerator.generate() ── Statement -> PHP; metody createServiceXxx() - ├─ afterCompile() rozšíření ── úpravy kódu; gettery parametrů - └─ toString() ── výsledný PHP kód -> cache - -──────────────────────────────────────────────────────────── - -BĚH (každý request) -│ -├─ new Container($dynamicParams) -├─ initialize() ── boot kód rozšíření (session, hlavičky, validace) -└─ getService()/getByType() ── lazy instance z předpočítaných metadat -``` - - -Nejčastější omyly -================= - -- "V `loadConfiguration()` si najdu služby podle typu." Ne - graf je neúplný (uživatelská `services:` běží až po vás) a `getByType()` spustí předčasný resolve neúplného grafu. Přesuňte to do `beforeCompile()`. `findByTag()` je v pořádku i zde. -- "Hodnota z `getenv()` v parametru bude v každém prostředí jiná." Jen když je parametr dynamický. Jinak se zapeče v čase kompilace a je všude stejná. -- "Reference `@Type` je hned jméno služby." Není - je to typová reference, na konkrétní jméno se vyřeší autowiringem až v kroku complete. -- "Moje rozšíření čte pomocný soubor, ale změny se neprojeví." Registrujte ho přes `$builder->addDependency($file)`, jinak o něm cache neví a nepřekompiluje se. -- "Během `resolve()` můžu volat `getByType()`." Ne - vyhodí výjimku. Hledání podle typu patří do `beforeCompile()` nebo později, nikdy ne doprostřed resolvingu. - -{{maintitle: Kompilace DI kontejneru v Nette: jednotlivé fáze}} -{{description: Co se děje, když Nette sestavuje DI kontejner: jeho fáze, kdy se rozbalují parametry, kdy se řetězec @service mění na referenci a kdy lze bezpečně hledat služby.}} diff --git a/dependency-injection/cs/configuration.texy b/dependency-injection/cs/configuration.texy deleted file mode 100644 index 7a60f0c926..0000000000 --- a/dependency-injection/cs/configuration.texy +++ /dev/null @@ -1,336 +0,0 @@ -Konfigurace DI kontejneru -************************* - -.[perex] -Přehled konfiguračních voleb pro Nette DI kontejner. - - -Konfigurační soubor -=================== - -Nette DI kontejner se snadno ovládá pomocí konfiguračních souborů. Ty se obvykle zapisují ve [formátu NEON|neon:format]. K editaci doporučujeme [editory s podporou |tools:ide] tohoto formátu. - -<pre> -"decorator .[prism-token prism-atrule]":[#decorator]: "Dekorátor .[prism-token prism-comment]"<br> -"di .[prism-token prism-atrule]":[#DI]: "DI kontejner .[prism-token prism-comment]"<br> -"extensions .[prism-token prism-atrule]":[#Rozšíření]: "Instalace dalších DI rozšíření .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#Vkládání souborů]: "Vkládání souborů .[prism-token prism-comment]"<br> -"parameters .[prism-token prism-atrule]":[#parametry]: "Parametry .[prism-token prism-comment]"<br> -"search .[prism-token prism-atrule]":[#Search]: "Automatická registrace služeb .[prism-token prism-comment]"<br> -"services .[prism-token prism-atrule]":[services]: "Služby .[prism-token prism-comment]" -</pre> - -.[note] -Chcete-li zapsat řetězec obsahující znak `%`, musíte jej escapovat zdvojením na `%%`. - - -Parametry -========= - -V konfiguraci můžete definovat parametry, které lze pak použít jako součást definic služeb. Můžete tak zpřehlednit konfiguraci nebo sjednotit a vyčlenit hodnoty, které se budou měnit. - -```neon -parameters: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: secret -``` - -Na parametr `dsn` se odkážeme kdekoliv v konfiguraci zápisem `%dsn%`. Parametry lze používat i uvnitř řetězců jako `'%wwwDir%/images'`. - -Parametry nemusí být jen řetězce nebo čísla, mohou také obsahovat pole: - -```neon -parameters: - mailer: - host: smtp.example.com - secure: ssl - user: franta@gmail.com - languages: [cs, en, de] -``` - -Na konkrétní klíč se odkážeme jako `%mailer.user%`. - -Pokud potřebujete ve vašem kódu, například třídě, zjistit hodnotu jakéhokoliv parametru, tak jej do této třídy předejte. Například v konstruktoru. Neexistuje žádný globální objekt představující konfiguraci, kterého by se třídy dotazovaly na hodnoty parametrů. To by bylo porušením principu dependency injection. - - -Služby -====== - -Viz [samostatná kapitola|services]. - - -Decorator -========= - -Jak upravit hromadně všechny služby určitého typu? Třeba zavolat určitou metodu u všech presenterů, které dědí od konkrétního společného předka? Od toho je tu decorator. - -```neon -decorator: - # u všech služeb, co jsou instancí této třídy nebo rozhraní - App\Presentation\BasePresenter: - setup: - - setProjectId(10) # zavolej tuto metodu - - $absoluteUrls = true # a nastav proměnnou -``` - -Decorator se dá používat také pro nastavení [tagů |services#Tagy] nebo zapnutí režimu [inject |services#Režim Inject]. - -```neon -decorator: - InjectableInterface: - tags: [mytag: 1] - inject: true -``` - - -DI -=== - -Technické nastavení DI kontejneru. - -```neon -di: - # zobrazit DIC v Tracy Bar? - debugger: ... # (bool) výchozí je autodetekce (zapne se, když je přítomná Tracy) - - # typy parametrů, které nikdy neautowirovat - excluded: ... # (string[]) - - # povolit lazy vytváření služeb? - lazy: ... # (bool) výchozí je false - - # třída, od které dědí DI kontejner - parentClass: ... # (string) výchozí je Nette\DI\Container -``` - - -Lazy služby .{data-version:3.2.4} ---------------------------------- - -Nastavení `lazy: true` aktivuje lazy (odložené) vytváření služeb. To znamená, že služby nejsou skutečně vytvořeny v okamžiku, kdy si je vyžádáme z DI kontejneru, ale až ve chvíli jejich prvního použití. Což může zrychlit start aplikace a snížit paměťové nároky, protože se vytváří jen ty služby, které jsou v daném požadavku skutečně potřeba. - -U konkrétní služby lze lazy vytváření [změnit |services#Lazy služby]. - -.[note] -Lazy objekty lze použít pouze pro uživatelské třídy, nikoliv pro interní PHP třídy. Vyžaduje PHP 8.4 nebo novější. - - -Export metadat --------------- - -Třída DI kontejneru obsahuje i spoustu metadat. Můžete ji zmenšit tím, že export metadat zredukujete. - -```neon -di: - export: - # exportovat parametry? - parameters: false # (bool) výchozí je true - - # exportovat tagy a které? - tags: # (string[]|bool) výchozí jsou všechny - - event.subscriber - - # exportovat data pro autowiring a které? - types: # (string[]|bool) výchozí jsou všechny - - Nette\Database\Connection - - Symfony\Component\Console\Application -``` - -Pokud nevyužíváte pole `$container->getParameters()`, můžete vypnout export parametrů. Dále můžete exportovat jen ty tagy, přes které získáváte služby metodou `$container->findByTag(...)`. Pokud metodu nevoláte vůbec, můžete zcela vypnout export tagů pomocí `false`. - -Výrazně můžete zredukovat metadata pro [autowiring] tím, že uvedete třídy, které používáte jako parametr metody `$container->getByType()`. A opět, pokud metodu nevoláte vůbec (resp. jen v [bootstrapu|application:bootstrapping] pro získání `Nette\Application\Application`), můžete export úplně vypnout pomocí `false`. - - -Rozšíření -========= - -Registrace dalších DI rozšíření. Tímto způsobem přidáme např. DI rozšíření `Dibi\Bridges\Nette\DibiExtension3` pod názvem `dibi`: - -```neon -extensions: - dibi: Dibi\Bridges\Nette\DibiExtension3 -``` - -Následně ho tedy konfigurujeme v sekci `dibi`: - -```neon -dibi: - host: localhost -``` - -Jako rozšíření lze přidat i třídu, která má parametry: - -```neon -extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, [%appDir%], %tempDir%/cache) -``` - - -Vkládání souborů -================ - -Další konfigurační soubory můžeme vložit v sekci `includes`: - -```neon -includes: - - parameters.php - - services.neon - - presenters.neon -``` - -Název `parameters.php` není překlep, konfigurace může být zapsaná také v PHP souboru, který ji vrátí jako pole: - -```php -<?php -return [ - 'database' => [ - 'main' => [ - 'dsn' => 'sqlite::memory:', - ], - ], -]; -``` - -Pokud se v konfiguračních souborech objeví prvky se stejnými klíči, budou přepsány, nebo v případě [polí sloučeny |#Slučování]. Později vkládaný soubor má vyšší prioritu než předchozí. Soubor, ve kterém je sekce `includes` uvedena, má vyšší prioritu než v něm vkládané soubory. - - -Search -====== - -Automatické přidávání služeb do DI kontejneru nesmírně zpříjemňuje práci. Nette automaticky přidává do kontejneru presentery, lze však snadno přidávat i jakékoliv jiné třídy. - -Stačí uvést, ve kterých adresářích (a podadresářích) má třídy hledat: - -```neon -search: - - in: %appDir%/Forms - - in: %appDir%/Model -``` - -Pokud vám stačí jediné pravidlo hledání, můžete seznam vynechat a jeho klíče zapsat přímo pod `search`: - -```neon -search: - in: %appDir% -``` - -Obvykle ovšem nechceme přidávat úplně všechny třídy a rozhraní, proto je můžeme filtrovat: - -```neon -search: - - in: %appDir%/Forms - - # filtrování podle názvu souboru (string|string[]) - files: - - *Factory.php - - # filtrování podle názvu třídy (string|string[]) - classes: - - *Factory -``` - -Nebo můžeme vybírat třídy, které dědí či implementují alespoň jednu z uvedených tříd: - - -```neon -search: - - in: %appDir% - extends: - - App\*Form - implements: - - App\*FormInterface -``` - -Lze definovat i vylučující pravidla, tj. masky názvu třídy nebo dědičné předky; pokud jim třída vyhovuje, služba se do DI kontejneru nepřidá: - -```neon -search: - - in: %appDir% - exclude: - files: ... - classes: ... - extends: ... - implements: ... -``` - -Všem automaticky registrovaným službám lze nastavit tagy: - -```neon -search: - - in: %appDir% - tags: ... -``` - -Kromě tříd zaregistruje hledání i rozhraní, která mají jedinou metodu `create()` nebo `get()`, a to jako [generované továrny nebo accessory |factory]. Třídy, pro které už je v kontejneru zaregistrovaná služba stejného typu, se přeskočí, takže nevznikají duplicity. - - -Slučování -========= - -Pokud se ve více konfiguračních souborech objeví prvky se stejnými klíči, budou přepsány, nebo v případě polí sloučeny. Později vkládaný soubor má vyšší prioritu než předchozí. - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>výsledek</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> - <td> -```neon -items: - - 1 - - 2 - - 3 -``` - </td> -</tr> -</table> - -U polí lze zabránit slučování uvedením vykřičníku za názvem klíče: - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>výsledek</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items!: - - 3 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> -</tr> -</table> - -{{maintitle: Konfigurace DI kontejneru v Nette: služby a parametry}} -{{description: Konfigurace DI kontejneru Nette v NEON: parametry, služby, decorator, rozšíření, vkládání souborů, vyhledávání služeb a slučování konfigurací.}} diff --git a/dependency-injection/cs/container.texy b/dependency-injection/cs/container.texy deleted file mode 100644 index e226195134..0000000000 --- a/dependency-injection/cs/container.texy +++ /dev/null @@ -1,142 +0,0 @@ -Co je DI kontejner? -******************* - -.[perex] -Dependency injection kontejner (DIC nebo DI kontejner) je objekt, který umí instancovat a konfigurovat jiné objekty (tzv. služby). - -Možná vás to překvapí, ale v mnoha případech nepotřebujete dependency injection kontejner, abyste mohli využívat výhod dependency injection (krátce DI). Vždyť i v [úvodní kapitole|introduction] jsme si na konkrétních příkladech DI ukázali a žádný kontejner nebyl potřeba. - -Pokud však potřebujete spravovat velké množství různých objektů s mnoha závislostmi, bude dependency injection container opravdu užitečný. Což je třeba případ webových aplikací postavených na frameworku. - -V předchozí kapitole jsme si představili třídy `Article` a `EditController`. Obě mají nějaké závislosti, a to databázi a továrnu `ArticleFactory`. A pro tyto třídy si nyní vytvoříme kontejner. Samozřejmě pro tak jednoduchý příklad nemá smysl mít kontejner. Ale vytvoříme ho, abychom si ukázali, jak vypadá a funguje. - -Zde je jednoduchý kontejner s natvrdo zapsanými hodnotami pro uvedený příklad: - -```php -class Container -{ - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection('mysql:', 'root', '***'); - } - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->createDatabase()); - } - - public function createEditController(): EditController - { - return new EditController($this->createArticleFactory()); - } -} -``` - -Použití by vypadalo následovně: - -```php -$container = new Container; -$controller = $container->createEditController(); -``` - -Kontejneru se pouze zeptáme na objekt a již nemusíme vědět nic o tom, jak jej vytvořit a jaké má závislosti; to všechno ví kontejner. Závislosti jsou kontejnerem injektovány automaticky. V tom je jeho síla. - -Kontejner má zatím zapsané všechny údaje natvrdo. Uděláme tedy další krok a přidáme parametry, aby byl kontejner skutečně užitečný: - -```php -class Container -{ - public function __construct( - private array $parameters, - ) { - } - - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection( - $this->parameters['db.dsn'], - $this->parameters['db.user'], - $this->parameters['db.password'], - ); - } - - // ... -} - -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); -``` - -Bystří čtenáři si možná všimli jistého problému. Pokaždé, když získáme objekt `EditController`, vytvoří se také nová instance `ArticleFactory` a databáze. To rozhodně nechceme. - -Přidáme proto metodu `getService()`, která bude vracet stále stejné instance: - -```php -class Container -{ - private array $services = []; - - public function __construct( - private array $parameters, - ) { - } - - public function getService(string $name): object - { - if (!isset($this->services[$name])) { - // getService('Database') bude volat createDatabase() - $method = 'create' . $name; - $this->services[$name] = $this->$method(); - } - return $this->services[$name]; - } - - // ... -} -``` - -Při prvním volání např. `$container->getService('Database')` si nechá od `createDatabase()` vytvořit objekt databáze, který uloží do pole `$services` a při příštím volání jej rovnou vrátí. - -Upravíme i zbytek kontejneru, aby používal `getService()`: - -```php -class Container -{ - // ... - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->getService('Database')); - } - - public function createEditController(): EditController - { - return new EditController($this->getService('ArticleFactory')); - } -} -``` - -Mimochodem, termínem služba se označuje jakýkoliv objekt spravovaný kontejnerem. Proto i ten název metody `getService()`. - -Hotovo. Máme plně funkční DI kontejner! A můžeme ho použít: - -```php -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); - -$controller = $container->getService('EditController'); -$database = $container->getService('Database'); -``` - -Jak vidíte, napsat DIC není nic složitého. Za připomenutí stojí, že samotné objekty nevědí, že je vytváří nějaký kontejner. Tím pádem je možné takto vytvářet jakýkoliv objekt v PHP bez zásahu do jeho zdrojového kódu. - -Ruční vytváření a údržba třídy kontejneru se může poměrně rychle stát noční můrou. V další kapitole si proto povíme o [Nette DI Containeru|nette-container], který se umí generovat a aktualizovat téměř sám. - -{{maintitle: Co je dependency injection kontejner?}} -{{description: DI kontejner vytváří a konfiguruje vaše služby. Podívejte se, co umí, jak snadno si ho napíšete ručně a proč budete brzy chtít, aby se generoval sám.}} diff --git a/dependency-injection/cs/extensions.texy b/dependency-injection/cs/extensions.texy deleted file mode 100644 index 18a4c15e3d..0000000000 --- a/dependency-injection/cs/extensions.texy +++ /dev/null @@ -1,446 +0,0 @@ -Tvorba rozšíření pro Nette DI -***************************** - -.[perex] -Rozšíření je třída, která se zapojuje do kompilace DI kontejneru. Umí programově registrovat služby, validovat vlastní konfigurační sekci, upravovat služby definované ostatními a dokonce zasáhnout do vygenerovaného kódu kontejneru. Na této stránce se naučíte, jak rozšíření napsat, co se kdy děje a na co si dát pozor. - -Rozšíření jsou způsob, jakým se balíčky integrují do Nette nativní cestou: používají je všechny balíčky `nette/*` a může je používat i ten váš. Typické rozšíření dělá jednu nebo více z těchto věcí: - -- **integruje knihovnu** - zaregistruje její služby v kontejneru a nabídne přívětivou, validovanou konfigurační sekci (přesně odtud pocházejí sekce `mail:` nebo `database:`) -- **automatizuje registraci** - zaregistruje mnoho podobných služeb ve smyčce nebo podle pravidla tam, kde by jejich vypisování v `services:` bylo úmorné -- **provádí plošné změny** - najde služby zaregistrované ostatními a doplní je, např. připojí logger ke každé službě s určitým tagem - -Pro každodenní práci na aplikaci rozšíření většinou nepotřebujete - registraci a propojování vašich tříd pokryje sekce [services |services] v konfiguraci. Po rozšíření sáhněte, když samotná konfigurace přestane stačit. - -Rozšíření se aktivuje v sekci `extensions`. Takto přidáme rozšíření reprezentované třídou `BlogExtension` pod názvem `blog`: - -```neon -extensions: - blog: BlogExtension -``` - -Pokud jeho konstruktor přijímá argumenty, předáme je rovnou tam: - -```neon -extensions: - blog: BlogExtension(%debugMode%) -``` - - -Jak probíhá kompilace -===================== - -Abyste psali rozšíření s jistotou, potřebujete vědět jednu klíčovou věc: **kdy váš kód běží.** Nette nepropojuje služby během obsluhy požadavků. Místo toho kontejner předem *zkompiluje*: přečte všechny konfigurační soubory, nechá rozšíření udělat svou práci a vygeneruje optimalizovanou PHP třídu, kterou uloží na disk. Každý další požadavek už jen načte tuto hotovou třídu. Kód vašeho rozšíření tedy běží jen ve chvíli, kdy se kontejner (znovu) sestavuje - ne při každém požadavku. - -Z toho plyne důležitý důsledek: během kompilace ještě žádné služby neexistují. Existují jen **definice** - recepty popisující, jakou třídu bude každá služba mít, jak se vytvoří a co se na ní potom zavolá. Definice drží objekt [ContainerBuilder |#ContainerBuilder]. Rozšíření je v podstatě *skriptovatelná konfigurace*: cokoli lze deklarovat v sekci `services:`, můžete stejně tak poskládat v PHP - podmíněně, ve smyčkách nebo v reakci na to, co zaregistrovali ostatní. - -Kompilace probíhá po fázích a rozšíření může vstoupit do každé z nich: - -1) zvaliduje se konfigurační sekce všech rozšíření (`getConfigSchema()`) -2) každé rozšíření zaregistruje své služby (`loadConfiguration()`); uživatelská sekce `services:` se zpracovává jako poslední, takže aplikace má vždy poslední slovo -3) jakmile jsou všechny definice na místě a typy služeb vyřešené, mohou je rozšíření upravovat (`beforeCompile()`) -4) vygeneruje se třída kontejneru; rozšíření mohou ještě zasáhnout do jejího kódu (`afterCompile()`) a přidat kód, který poběží při startu aplikace ([inicializace |#Inicializační kód]) - -.[note] -Ve vývojářském režimu se kontejner automaticky překompiluje, kdykoli změníte konfigurační soubor nebo samotnou třídu rozšíření - obojí se sleduje jako závislost. Rozšíření tak můžete vyvíjet, aniž byste kdy mazali cache. - -.[tip] -Hlubší pohled na to, co se v každé fázi děje - kdy se rozbalují parametry, kdy se z `@service` stává reference a přesně kdy je bezpečné hledat služby podle typu - najdete v [kompilaci kontejneru do hloubky |compilation-internals]. - - -První rozšíření -=============== - -Tady je malé, ale kompletní rozšíření. Aktivujeme ho a nakonfigurujeme ve stejném souboru: - -```neon -extensions: - blog: BlogExtension - -blog: - postsPerPage: 5 -``` - -A toto je celá třída: - -```php -use Nette\Schema\Expect; - -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function getConfigSchema(): Nette\Schema\Schema - { - return Expect::structure([ - 'postsPerPage' => Expect::int(10), - 'allowComments' => Expect::bool(true), - ]); - } - - - public function loadConfiguration(): void - { - $builder = $this->getContainerBuilder(); - - $builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class, ['postsPerPage' => $this->config->postsPerPage]); - - if ($this->config->allowComments) { - $builder->addDefinition($this->prefix('comments')) - ->setFactory(Blog\Comments::class); - } - } -} -``` - -`getConfigSchema()` popisuje, co smí obsahovat sekce `blog:` (pojmenovaná podle klíče, pod kterým jsme rozšíření zaregistrovali), včetně typů a výchozích hodnot - zvalidované hodnoty jsou pak k dispozici v `$this->config`. V `loadConfiguration()` registrujeme služby. Všimněte si názvů: `$this->prefix('articles')` vytvoří `blog.articles`, takže se služby různých rozšíření nemohou srazit. - -A poslední řádky ukazují, proč rozšíření vůbec existují: služba `comments` se zaregistruje jen tehdy, když jsou komentáře povolené. Obyčejný konfigurační soubor takhle rozhodovat neumí. - -Takto zaregistrované služby se chovají úplně stejně, jako by byly zapsané v `services:` - vytvářejí se líně na vyžádání a autowiring je předá všude tam, kde je type-hint `Blog\Articles`. - -Následující kapitoly popisují podrobně životní cyklus rozšíření, dále API [ContainerBuilderu |#ContainerBuilder], které budete v rozšíření používat, a nakonec [úskalí |#Tipy a úskalí], která stojí za to znát. - - -Životní cyklus rozšíření -======================== - -Rozšíření dědí od [api:Nette\DI\CompilerExtension] a přepisuje některé ze čtyř metod `getConfigSchema()`, `loadConfiguration()`, `beforeCompile()` a `afterCompile()`, které kompilátor během kompilace volá v tomto pořadí. - - -getConfigSchema(): Nette\Schema\Schema .[method] ------------------------------------------------- - -Definuje schéma konfigurační sekce rozšíření. Díky němu dostanou uživatelé validaci a jasné chybové hlášky zadarmo: překlep nebo špatný typ v sekci `blog:` se ohlásí srozumitelnou zprávou, aniž byste napsali jedinou kontrolu. - -Schéma se popisuje knihovnou [Schema |schema:] a umí vyjádřit typy, výchozí hodnoty, povolené hodnoty a mnohem víc: - -```php -public function getConfigSchema(): Nette\Schema\Schema -{ - return Expect::structure([ - 'postsPerPage' => Expect::int(10), - 'storage' => Expect::anyOf('files', 'database')->firstIsDefault(), - ]); -} -``` - -Zvalidovaná konfigurace je k dispozici v `$this->config` jako objekt `stdClass` (nebo jako pole, pokud ke schématu připojíte `castTo('array')`). - -Pokud hodnotu volby nelze znát v čase kompilace - pochází třeba z proměnné prostředí - označte ji pomocí `dynamic()`, např. `Expect::int()->dynamic()`. Více v [dynamických parametrech |application:bootstrapping#Dynamické parametry]. - - -loadConfiguration() .[method] ------------------------------ - -Místo, kde rozšíření registruje své služby, a to pomocí [ContainerBuilderu |#ContainerBuilder]: - -```php -public function loadConfiguration(): void -{ - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class); -} -``` - -Má-li být služba dostupná i pod krátkým názvem, přidejte alias. Podle konvence se to dělá jen tehdy, když je rozšíření zaregistrované pod svým obvyklým názvem, aby se o alias nemohlo přetahovat víc instancí rozšíření: - -```php -if ($this->name === 'blog') { - $builder->addAlias('articles', $this->prefix('articles')); -} -``` - -Když je služeb hodně, může být pohodlnější definovat je v samostatném NEON souboru známou syntaxí [services |services]. Prefix `@extension` odkazuje na aktuální rozšíření: - -```neon -services: - articles: - create: MyBlog\ArticlesModel(@connection) - - comments: - create: MyBlog\CommentsModel(@connection, @extension.articles) -``` - -Tyto definice načteme metodou `loadDefinitionsFromConfig()`; názvy se oprefixují automaticky a soubor se sleduje jako závislost, takže jeho změna vyvolá překompilování: - -```php -public function loadConfiguration(): void -{ - $this->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/services.neon')['services'], - ); -} -``` - - -beforeCompile() .[method] -------------------------- - -Ve chvíli volání této metody už builder drží **všechny** definice: vaše, definice ostatních rozšíření i ty z uživatelských konfiguračních souborů. Typy služeb jsou také vyřešené, takže vyhledávání podle typu je spolehlivé. Tato fáze je proto ideální pro prozkoumání a doplnění výsledného grafu služeb. - -Typicky vyhledáte služby podle tagu nebo typu a nalezené definice doplníte: - -```php -public function beforeCompile(): void -{ - $builder = $this->getContainerBuilder(); - - foreach ($builder->findByTag('logaware') as $name => $attrs) { - $builder->getDefinition($name)->addSetup('setLogger'); - } -} -``` - -Volání `setLogger()` nemá žádné explicitní argumenty - dodá je autowiring, stejně jako to dělá u továren. - -Můžete také spolupracovat s ostatními zaregistrovanými rozšířeními, která získáte přes `$this->compiler->getExtensions()`, volitelně filtrovaná podle třídy nebo rozhraní: - -```php -foreach ($this->compiler->getExtensions(FooExtension::class) as $extension) { - // ... -} -``` - - -afterCompile(Nette\PhpGenerator\ClassType $class) .[method] ------------------------------------------------------------ - -V poslední fázi je třída kontejneru vygenerovaná jako objekt [ClassType |php-generator:#Třídy] knihovny [PHP Generator |php-generator:]. Obsahuje tovární metodu pro každou službu a čeká na zápis do cache. Její kód můžete ještě upravit: - -```php -public function afterCompile(Nette\PhpGenerator\ClassType $class): void -{ - $method = $class->getMethod('__construct'); - // ... -} -``` - -Tuto fázi budete potřebovat jen výjimečně. Pro přidání kódu, který poběží při startu aplikace, použijte raději inicializaci: - - -Inicializační kód ------------------ - -Všechny předchozí fáze ovlivňují, jak se kontejner *sestaví*. Kromě toho může rozšíření vygenerovat kód, který poběží za *běhu aplikace*, hned po vytvoření kontejneru - třeba nastartovat session nebo spustit služby. Kód se zapisuje do objektu `$this->initialization` jeho metodou [addBody() |php-generator:#Těla metod a funkcí]: - -```php -public function loadConfiguration(): void -{ - // služby s tagem 'run' se musí vytvořit hned po startu kontejneru - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $attrs) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } -} -``` - -Samotné Nette používá inicializaci například k automatickému startu session nebo k odeslání bezpečnostních HTTP hlaviček. A pamatujte: na rozdíl od všeho ostatního v rozšíření tento kód běží při **každém požadavku**, takže ho udržujte malý. - - -ContainerBuilder -================ - -[api:Nette\DI\ContainerBuilder] je objekt, kterým rozšíření mluví s kompilátorem. Drží [definice |#Jak probíhá kompilace] všech služeb a nabízí metody pro jejich přidávání, vyhledávání a úpravy. Získáte ho v `loadConfiguration()` a `beforeCompile()`: - -```php -$builder = $this->getContainerBuilder(); -``` - - -Přidávání služeb ----------------- - -Registrace služby je totéž, co děláte v sekci `services:` NEON souboru - jen zapsané v PHP. Každý konfigurační klíč má odpovídající metodu na definici, takže tyto dva zápisy jsou rovnocenné: - -```neon -services: - articles: - create: Blog\Articles(@connection) - setup: - - setLogger(@logger) - tags: [logaware] -``` - -```php -$builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class, ['@connection']) - ->addSetup('setLogger', ['@logger']) - ->addTag('logaware'); -``` - -Definice vrácená metodou `addDefinition()` je [ServiceDefinition |#Typy definic] nabízející protějšky konfiguračních klíčů: `setType()` (třída služby), `setFactory()` (jak ji vytvořit), `setArguments()`, `addSetup()`, `addTag()` a `setAutowired()`. - -`addSetup()` odpovídá seznamu `setup:` a přijímá stejné formy: volání metody `addSetup('setLogger', ['@logger'])`, přiřazení do vlastnosti `addSetup('$cache', ['@cache'])` nebo volání na jiné službě `addSetup('@Tracy\Bar::addPanel', [$panel])`. - -Kromě běžných služeb umí builder zaregistrovat i [generované |factory] továrny, accessory a locatory - každý svou metodou, která vrací odpovídající [typ definice |#Typy definic]: - -| Metoda | Registruje -|--------|---------- -| `addDefinition()` | běžnou službu (vrací `ServiceDefinition`) -| `addFactoryDefinition()` | generovanou [továrnu |factory] (rozhraní s metodou `create()`) -| `addAccessorDefinition()` | generovaný [accessor |factory#Accessor] (rozhraní s metodou `get()`) -| `addLocatorDefinition()` | [multifactory / locator |factory#Vícenásobná továrna/accessor] sdružující více továren -| `addImportedDefinition()` | službu předanou kontejneru zvenčí za běhu -| `addAlias()` | druhý název pro existující službu - -U továrny nastavíte vytvářený objekt přes `getResultDefinition()`; accessor místo toho odkazuje na existující službu přes `setReference()`: - -```php -$builder->addFactoryDefinition($this->prefix('latteFactory')) - ->setImplement(LatteFactory::class) - ->getResultDefinition() - ->setFactory(Latte\Engine::class) - ->addSetup('setStrictTypes', [true]); -``` - -`addLocatorDefinition()` a `addImportedDefinition()` potřebujete jen zřídka - takové služby obvykle vznikají z klíčů `implement:` a imported služeb v NEONu, ne ručním zápisem. - - -Vyhledávání a úprava služeb ---------------------------- - -Pro vyhledávání a procházení existujících definic builder nabízí: - -| Metoda | Popis -|--------|------ -| `getDefinition(string $name)` | definici daného názvu (vyhodí výjimku, pokud chybí) -| `hasDefinition(string $name)` | zda definice nebo alias daného názvu existuje -| `getDefinitions()` | všechny definice -| `removeDefinition(string $name)` | odstraní definici -| `getByType(string $type)` | název autowirované služby daného typu, nebo `null` -| `getDefinitionByType(string $type)` | autowirovanou definici daného typu -| `findByType(string $type)` | všechny definice daného typu jako dvojice `name => definice` -| `findByTag(string $tag)` | služby nesoucí daný tag jako dvojice `name => hodnota tagu` -| `addExcludedClasses(array $types)` | vyloučí třídy a rozhraní z autowiringu - -Šikovný obrat je zjistit pomocí `getByType()`, jestli nějaká služba vůbec existuje - třeba napojit se na logger jen tehdy, když ho aplikace má: - -```php -if ($builder->getByType(Psr\Log\LoggerInterface::class)) { - $builder->getDefinition($this->prefix('articles')) - ->addSetup('setLogger'); -} -``` - - -Typy definic ------------- - -Každá metoda `add*Definition()` vrací jiný druh definice. Všechny dědí od společného předka `Nette\DI\Definitions\Definition`: - -- **`ServiceDefinition`** - běžná služba; nastavuje se přes `setType()`, `setFactory()`, `addSetup()`, `addTag()` a `setAutowired()` -- **`FactoryDefinition`** - [generovaná továrna |factory]: rozhraní, jehož metoda `create()` vrací při každém volání nový objekt -- **`AccessorDefinition`** - [generovaný accessor |factory#Accessor]: rozhraní, jehož metoda `get()` vrací existující službu -- **`LocatorDefinition`** - [multifactory / locator |factory#Vícenásobná továrna/accessor] sdružující více továren nebo accessorů v jednom rozhraní -- **`ImportedDefinition`** - služba, kterou kontejner sám nevytváří, ale dostává zvenčí za běhu - -Pamatujte, že `getDefinition()` vrací takový druh definice, jaký se pod daným názvem skrývá. Pokud může váš kód narazit na generovanou továrnu, nejdřív zkontrolujte typ a vytvářený objekt nastavte přes `getResultDefinition()`: - -```php -$def = $builder->getDefinition($name); -if ($def instanceof Nette\DI\Definitions\FactoryDefinition) { - $def = $def->getResultDefinition(); -} -$def->addSetup('setLogger'); -``` - - -Tipy a úskalí -============= - - -Kompilace vs. běh aplikace --------------------------- - -Nejčastější zdroj zmatení: kód rozšíření běží, když se kontejner **kompiluje**, ne když aplikace obsluhuje požadavky. V praxi to znamená: - -- Rozšíření nikdy nepracuje s instancemi služeb - ty ještě neexistují. Nevytvářejte služby přes `new`; zaregistrujte definici a nechte kontejner, ať je vytvoří sám. -- Všechny hodnoty z konfigurace se zapečou do vygenerovaného kódu. Hodnota, která se může lišit mezi prostředími (cesta, heslo z `getenv()`), musí být označená jako [dynamická |application:bootstrapping#Dynamické parametry], jinak zamrzne v čase kompilace. -- Řetězce předané do `$this->initialization->addBody()` se nespouštějí teď - je to PHP kód vygenerovaný do kontejneru, který se spouští při každém požadavku. - - -Závislosti na souborech ------------------------ - -Kontejner se překompiluje při změně konfiguračních souborů nebo tříd rozšíření. Pokud ale vaše rozšíření čte jakýkoli jiný soubor - seznam entit, XML konfiguraci knihovny - kontejner o něm nemá jak vědět. Takové soubory zaregistrujte pomocí: - -```php -$builder->addDependency($file); -``` - -Jinak se dočkáte klasické záhady: upravíte soubor, ale aplikace se dál chová postaru - změna se projeví až ve chvíli, kdy se kontejner přestaví z nějakého jiného důvodu. (Soubory načtené přes `loadFromFile()` se sledují automaticky.) - - -Podmíněná registrace --------------------- - -Rozšíření se umí přizpůsobit svému prostředí. Volitelné integrace se typicky hlídají přes `class_exists()`: - -```php -if (class_exists(Symfony\Component\Console\Command\Command::class)) { - $builder->addDefinition($this->prefix('command')) - ->setFactory(Blog\Console\SitemapCommand::class); -} -``` - -A hodnoty jako `%debugMode%` je nejlepší předávat konstruktorem rozšíření: - -```neon -extensions: - blog: BlogExtension(%debugMode%) -``` - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function __construct( - private bool $debugMode = false, - ) {} -} -``` - -Typickým využitím je registrace Tracy panelu jen ve vývojářském režimu. - - -Složitější argumenty --------------------- - -Někdy argument továrny nebo setup volání není obyčejná hodnota, název třídy ani reference `@service`. Pro tyto případy existují: - -- `new Nette\DI\Definitions\Statement(Blog\Panel::class, [$args])` - objekt vytvořený na místě, "anonymní služba" použitá jako argument -- `new Nette\DI\Definitions\Reference('blog.articles')` - reference na službu, objektový protějšek zápisu `@name` -- `$builder::literal('PHP_SAPI')` - kus surového PHP kódu vloženého tak, jak je, do vygenerovaného kontejneru - -Příklad - registrace Tracy panelu: - -```php -$builder->getDefinition($this->prefix('articles')) - ->addSetup('@Tracy\Bar::addPanel', [ - new Nette\DI\Definitions\Statement(Blog\ArticlesPanel::class), - ]); -``` - - -Exportované tagy a typy ------------------------ - -[Export metadat |configuration#Export metadat] lze v konfiguraci omezit tak, aby si zkompilovaný kontejner ponechal jen tagy a typy pro autowiring, které aplikace skutečně používá. Pokud vaše rozšíření získává služby za běhu pomocí `$container->findByTag()` nebo `$container->getByType()`, takové omezení může odstranit právě ta metadata, na kterých závisíte. - -Abyste tomu předešli, řekněte kompilátoru, které tagy a typy se musí vždy exportovat: - -```php -public function loadConfiguration(): void -{ - // tento tag se bude exportovat vždy, i když je export omezený - $this->compiler->addExportedTag('event.subscriber'); - - // tento typ bude vždy dostupný pro getByType() - $this->compiler->addExportedType(Nette\Database\Connection::class); -} -``` - -Obě metody metadata pouze přidávají; nikdy nepřepíšou nastavení `di › export` z aplikace. Když tedy aplikace omezí export na výčet, tagy a typy, které vaše rozšíření potřebuje, zůstanou zachovány; jen úplné vypnutí exportu tagů (`tags: false`) je zahodí spolu se vším ostatním. - -{{maintitle: Tvorba rozšíření pro Nette DI: registrace služeb v kódu}} -{{description: Napište rozšíření pro Nette DI, které v kódu registruje služby, validuje vlastní sekci konfigurace, upravuje cizí služby a mění vygenerovaný kontejner.}} diff --git a/dependency-injection/cs/factory.texy b/dependency-injection/cs/factory.texy deleted file mode 100644 index a15d45b7f3..0000000000 --- a/dependency-injection/cs/factory.texy +++ /dev/null @@ -1,236 +0,0 @@ -Generované továrny -****************** - -.[perex] -Nette DI umí automaticky generovat kód továren na základě rozhraní, což vám ušetří psaní kódu. - -Továrna je třída, která vyrábí objekty. Předává jim tedy i jejich závislosti. Nezaměňujte prosím s návrhovým vzorem *factory method*, který popisuje specifický způsob využití továren a s tímto tématem nesouvisí. - -Jak taková továrna vypadá, jsme si ukázali v [úvodní kapitole |introduction#Továrna]: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Nette DI umí kód továren automaticky generovat. Vše, co musíte udělat, je vytvořit rozhraní a Nette DI vygeneruje implementaci. Rozhraní musí mít přesně jednu metodu s názvem `create` a deklarovat návratový typ: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Tedy továrna `ArticleFactory` má metodu `create`, která vytváří objekty `Article`. Třída `Article` může vypadat třeba následovně: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } -} -``` - -Továrnu přidáme do konfiguračního souboru: - -```neon -services: - - ArticleFactory -``` - -Nette DI vygeneruje odpovídající implementaci továrny. - -V kódu, který továrnu používá, si tak vyžádáme objekt podle rozhraní a Nette DI použije vygenerovanou implementaci: - -```php -class UserController -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function foo() - { - // necháme továrnu vytvořit objekt - $article = $this->articleFactory->create(); - } -} -``` - - -Parametrizovaná továrna -======================= - -Tovární metoda `create` může přijímat parametry, které poté předá do konstruktoru. Doplňme například třídu `Article` o ID autora článku: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - private int $authorId, - ) { - } -} -``` - -Parametr přidáme také do továrny: - -```php -interface ArticleFactory -{ - function create(int $authorId): Article; -} -``` - -Díky tomu, že se parametr v konstruktoru i parametr v továrně jmenují stejně, Nette DI je zcela automaticky předá. - - -Pokročilá definice -================== - -Definici lze zapsat i ve víceřádkové podobě za použití klíče `implement`: - -```neon -services: - articleFactory: - implement: ArticleFactory -``` - -Při zápisu tímto delším způsobem je možné uvést další argumenty pro konstruktor v klíči `arguments` a doplňující konfiguraci pomocí `setup`, stejně jako u běžných služeb. - -Příklad: pokud by metoda `create()` nepřijímala parametr `$authorId`, mohli bychom uvést pevnou hodnotu v konfiguraci, která by se předávala do konstruktoru `Article`: - -```neon -services: - articleFactory: - implement: ArticleFactory - arguments: - authorId: 123 -``` - -Nebo naopak pokud by `create()` parametr `$authorId` přijímala, ale nebyl by součástí konstruktoru a předával se metodou `Article::setAuthorId()`, odkázali bychom se na něj v sekci `setup`: - -```neon -services: - articleFactory: - implement: ArticleFactory - setup: - - setAuthorId($authorId) -``` - - -Accessor -======== - -Nette umí krom továren generovat i tzv. accessory. Jde o objekty s metodou `get()`, která vrací určitou službu z DI kontejneru. Opakované volání `get()` vrací stále tutéž instanci. - -Accessory poskytují závislostem lazy-loading. Mějme třídu, která zapisuje chyby do speciální databáze. Když by si tato třída nechávala připojení k databázi předávat jako závislost konstruktorem, muselo by se připojení vždycky vytvořit, ačkoliv v praxi se chyba objeví jen výjimečně, takže by spojení povětšinou zůstalo nevyužité. Místo toho si třída nechá předat accessor a teprve když se zavolá jeho `get()`, dojde k vytvoření objektu databáze. - -Jak accessor vytvořit? Stačí napsat rozhraní a Nette DI vygeneruje implementaci. Rozhraní musí mít přesně jednu bezparametrickou metodu s názvem `get` a deklarovat návratový typ: - -```php -interface PDOAccessor -{ - function get(): PDO; -} -``` - -Accessor přidáme do konfiguračního souboru, kde je také definice služby, kterou bude vracet: - -```neon -services: - - PDOAccessor - - PDO(%dsn%, %user%, %password%) -``` - -Protože accessor vrací službu typu `PDO` a v konfiguraci je jediná taková služba, bude vracet právě ji. Pokud by služeb daného typu bylo víc, určíme vracenou službu pomocí názvu, např. `- PDOAccessor(@db1)`. - - -Vícenásobná továrna/accessor -============================ - -Naše továrny a accessory uměly zatím vždy vyrábět nebo vracet jen jeden objekt. Lze ale velmi snadno vytvořit i vícenásobné továrny kombinované s accessory. Rozhraní takové třídy bude obsahovat libovolný počet metod s názvy `create<Name>()` a `get<Name>()`, např.: - -```php -interface MultiFactory -{ - function createArticle(): Article; - function getDb(): PDO; -} -``` - -Takže místo toho, abychom si předávali několik generovaných továren a accessorů, předáme jednu komplexnější továrnu, která toho umí víc. - -Alternativně lze místo několika metod použít `get()` s parametrem: - -```php -interface MultiFactoryAlt -{ - function get($name): PDO; -} -``` - -Pak platí, že `MultiFactory::getDb()` dělá totéž jako `MultiFactoryAlt::get('db')`. Nicméně alternativní zápis má tu nevýhodu, že není zřejmé, jaké hodnoty `$name` jsou podporované a logicky také nelze v rozhraní odlišit různé návratové typy pro různé hodnoty `$name`. - -Místo `get($name)` může rozhraní deklarovat `create($name)`, které při každém volání vrací novou instanci (zatímco `get()` vrací sdílenou). Rozhraní smí obsahovat jen jednu takovou parametrickou metodu. Pokud je návratový typ metody nullable (např. `?PDO`), vrátí pro neznámé `$name` místo vyhození výjimky `null`. - - -Definice seznamem ------------------ -Vícenásobnou továrnu lze v konfiguraci definovat seznamem, ve kterém jsou služby zapsané přímo: .{data-version:3.2.0} - -```neon -services: - - MultiFactory( - article: Article() # definuje createArticle() - db: PDO(%dsn%, %user%, %password%) # definuje getDb() - ) -``` - -Nebo se můžeme v definici továrny odkázat na existující služby pomocí reference: - -```neon -services: - article: Article - - PDO(%dsn%, %user%, %password%) - - MultiFactory( - article: @article # definuje createArticle() - db: @\PDO # definuje getDb() - ) -``` - - -Definice pomocí tagů --------------------- - -Další možností, jak definovat vícenásobnou továrnu, je využít [tagy |services#Tagy]. Hodnota tagu určuje název odpovídající metody: - -```neon -services: - article: - create: Article - tags: {multi: article} # definuje createArticle() - db: - create: PDO(%dsn%, %user%, %password%) - tags: {multi: db} # definuje getDb() - - - MultiFactory(tagged: multi) -``` - -{{maintitle: Generované továrny v Nette DI: kód podle rozhraní}} -{{description: Stačí napsat rozhraní a Nette DI vygeneruje kód továrny za vás. Parametrizované továrny, accessory, vícenásobné továrny a pokročilé definice.}} diff --git a/dependency-injection/cs/faq.texy b/dependency-injection/cs/faq.texy deleted file mode 100644 index 7f3c54a195..0000000000 --- a/dependency-injection/cs/faq.texy +++ /dev/null @@ -1,126 +0,0 @@ -Často kladené otázky o DI (FAQ) -******************************* - - -Je DI jiným názvem pro IoC? ---------------------------- - -*Inversion of Control* (IoC) je princip zaměřený na způsob, jakým je kód spouštěn - zda váš kód spouští cizí nebo je váš kód integrován do cizího, který jej následně volá. IoC je široký pojem zahrnující [události |nette:glossary#události], takzvaný [Hollywoodský princip |application:components#Hollywood style] a další aspekty. Součástí tohoto konceptu jsou i továrny, o kterých hovoří [Pravidlo č. 3: nech to na továrně |introduction#Pravidlo č. 3: nech to na továrně], a které představují inverzi pro operátor `new`. - -*Dependency Injection* (DI) se zaměřuje na způsob, jakým se jeden objekt dozví o jiném objektu, tedy o jeho závislosti. Jde o návrhový vzor, který požaduje explicitní předávání závislostí mezi objekty. - -Lze tedy říci, že DI je specifickou formou IoC. Nicméně ne všechny formy IoC jsou vhodné z hlediska čistoty kódu. Například mezi antivzory patří techniky, které pracují s [globálním stavem |global-state] nebo takzvaný [Service Locator |#Co je to Service Locator]. - - -Co je to Service Locator? -------------------------- - -Jde o alternativu k Dependency Injection. Funguje tak, že vytvoří centrální úložiště, kde jsou registrovány všechny dostupné služby nebo závislosti. Když objekt potřebuje závislost, požádá o ni Service Locator. - -Oproti Dependency Injection však ztrácí na transparentnosti: závislosti nejsou objektům předávány přímo a nejsou tak snadno identifikovatelné, což vyžaduje prozkoumání kódu, aby byly všechny vazby odhaleny a pochopeny. Testování je také složitější, protože nemůžeme jednoduše předávat mock objekty testovaným objektům, ale musíme na to jít přes Service Locator. Navíc Service Locator narušuje návrh kódu, jelikož jednotlivé objekty musí o jeho existenci vědět, což se liší od Dependency Injection, kde objekty nemají povědomí o DI kontejneru. - - -Kdy je lepší DI nepoužít? -------------------------- - -Nejsou známy žádné obtíže spojené s použitím návrhového vzoru Dependency Injection. Naopak získávání závislostí z globálně dostupných míst (např. statické proměnné nebo singletony) vede k [celé řadě komplikací |global-state], stejně tak používání Service Locatoru. Proto je vhodné využívat DI vždy. To není dogmatický přístup, ale jednoduše nebyla nalezena lepší alternativa. - -Přesto existují určité situace, kdy si objekty nepředáváme a získáme je z globálního prostoru. Například při ladění kódu, kdy potřebujete v konkrétním bodě programu vypsat hodnotu proměnné, změřit dobu trvání určité části programu nebo zaznamenat zprávu. V takových případech, kdy jde o dočasné úkony, které budou později z kódu odstraněny, je legitimní využít globálně dostupný dumper, stopky nebo logger. Tyto nástroje totiž nepatří k návrhu kódu. - - -Má používání DI své stinné stránky? ------------------------------------ - -Obnáší použití Dependency Injection nějaké nevýhody, jako například zvýšenou náročnost na psaní kódu nebo zhoršený výkon? Co ztrácíme, když začneme psát kód v souladu s DI? - -DI nemá na výkon nebo paměťové nároky aplikace vliv. Určitou roli může hrát výkon DI Containeru, avšak v případě [Nette DI |nette-container] je kontejner kompilován do čistého PHP, takže jeho režie při běhu aplikace je v podstatě nulová. - -Při psaní kódu bývá nutné vytvářet konstruktory přijímající závislosti. Dříve to mohlo být zdlouhavé, avšak díky moderním IDE a [constructor property promotion |https://blog.nette.org/cs/php-8-0-kompletni-prehled-novinek#toc-constructor-property-promotion] je to nyní otázkou několika sekund. Továrny navíc často dokáže Nette DI generovat automaticky, což dále snižuje množství opakujícího se kódu. Na druhou stranu odpadá potřeba psát singletony a statické přístupové body. - -Lze konstatovat, že správně navržená aplikace využívající DI není v porovnání s aplikací využívající singletony ani kratší, ani delší. Části kódu pracující se závislostmi jsou pouze vyňaty z jednotlivých tříd a přesunuty na nová místa, tedy do DI kontejneru a továren. - - -Jak legacy aplikaci přepsat na DI? ----------------------------------- - -Přechod z legacy aplikace na Dependency Injection může být náročný proces, zejména u velkých a komplexních aplikací. Je důležité přistupovat k tomuto procesu systematicky. - -- Při přechodu na Dependency Injection je důležité, aby všichni členové týmu rozuměli principům a postupům, které se používají. -- Nejprve proveďte analýzu stávající aplikace a identifikujte klíčové komponenty a jejich závislosti. Vytvořte plán, které části budou refaktorovány a v jakém pořadí. -- Implementujte DI kontejner nebo ještě lépe použijte existující knihovnu, například Nette DI. -- Postupně refaktorujte jednotlivé části aplikace, aby používaly Dependency Injection. To může zahrnovat úpravy konstruktorů nebo metod tak, aby přijímaly závislosti jako parametry. -- Upravte místa v kódu, kde se vytvářejí objekty se závislostmi, aby místo toho byly závislosti injektovány kontejnerem. To může zahrnovat použití továren. - -Pamatujte, že přechod na Dependency Injection je investice do kvality kódu a dlouhodobé udržovatelnosti aplikace. Ačkoli může být náročné provést tyto změny, výsledkem by měl být čistší, modulárnější a snadno testovatelný kód, který je připraven pro budoucí rozšíření a údržbu. - - -Proč se upřednostňuje kompozice před dědičností? ------------------------------------------------- -Je vhodnější používat [kompozici |nette:introduction-to-object-oriented-programming#Kompozice] místo [dědičnosti |nette:introduction-to-object-oriented-programming#Dědičnost], protože slouží k opětovnému použití kódu, aniž bychom se museli starat o důsledky změn. Poskytuje tedy volnější vazbu, kdy nemusíme mít obavy, že změna nějakého kódu způsobí potřebu změny jiného závislého kódu. Typickým příkladem je situace označovaná jako [constructor hell |passing-dependencies#Constructor hell]. - - -Lze použít Nette DI Container mimo Nette? ------------------------------------------ - -Rozhodně. Nette DI Container je součástí Nette, ale je navržen jako samostatná knihovna, která může být použita nezávisle na ostatních částech frameworku. Stačí ji nainstalovat pomocí Composeru, vytvořit konfigurační soubor s definicí vašich služeb a poté pomocí několika řádků PHP kódu vytvořit DI kontejner. A ihned můžete začít využívat výhody Dependency Injection ve svých projektech. - -Jak vypadá konkrétní použití včetně kódů popisuje kapitola [Nette DI Container |nette-container]. - - -Proč je konfigurace v NEON souborech? -------------------------------------- - -NEON je jednoduchý a snadno čitelný konfigurační jazyk, který byl vyvinut v rámci Nette pro nastavení aplikací, služeb a jejich závislostí. Ve srovnání s JSONem nebo YAMLem nabízí pro tento účel mnohem intuitivnější a flexibilnější možnosti. V NEONu lze přirozeně popsat vazby, které by v JSONu nebo YAMLu nebylo možné zapsat buď vůbec, nebo jen prostřednictvím složitého opisu. - - -Nezpomaluje aplikaci parsování NEON souborů? --------------------------------------------- - -Byť se soubory NEON parsují velmi rychle, na tomto hledisku vůbec nezáleží. Důvodem je, že parsování souborů proběhne pouze jednou při prvním spuštění aplikace. Poté se vygeneruje kód DI kontejneru, uloží se na disk a spustí se při každém dalším požadavku, aniž by bylo nutné provádět další parsování. - -Takto to funguje v produkčním prostředí. Během vývoje se NEON soubory parsují pokaždé, když dojde ke změně jejich obsahu, aby vývojář měl vždy aktuální DI kontejner. Samotné parsování je, jak bylo řečeno, otázkou okamžiku. - - -Jak se dostanu ze své třídy k parametrům v konfiguračním souboru? ------------------------------------------------------------------ - -Mějme na paměti [Pravidlo č. 1: nech si to předat |introduction#Pravidlo č. 1: nech si to předat]. Pokud třída vyžaduje informace z konfiguračního souboru, nemusíme přemýšlet, jak se k těm informacím dostat, místo toho si o ně jednoduše požádáme - například prostřednictvím konstruktoru třídy. A předání uskutečníme v konfiguračním souboru. - -V této ukázce je `%myParameter%` zástupný symbol pro hodnotu parametru `myParameter`, který se předá do konstruktoru třídy `MyClass`: - -```neon -# config.neon -parameters: - myParameter: Some value - -services: - - MyClass(%myParameter%) -``` - -Chcete-li předávat více parametrů nebo využít autowiring, je vhodné [parametry zabalit do objektu |best-practices:passing-settings-to-presenters]. - - -Podporuje Nette PSR-11: Container interface? --------------------------------------------- - -Ano. Třída `Nette\Bridges\DIPsr\PsrContainer` zpřístupní [Nette DI Container |api:Nette\DI\Container] přes PSR-11 `ContainerInterface`, viz [PSR-11 Container |nette-container#PSR-11 Container]. - - -Co znamenají pojmy container, compiler, definice atd.? ------------------------------------------------------- - -Krátký slovníček slov, která se kolem Nette DI často objevují, většina z nich při [psaní rozšíření |extensions]: - -- **Container** (kontejner) - zkompilovaný objekt (`Nette\DI\Container`), který na vyžádání vytváří služby a za běhu je drží. Vygeneruje se jednou jako optimalizovaný PHP kód. -- **Compiler** (kompilátor) - mechanismus, který z konfiguračních souborů a rozšíření vytvoří třídu kontejneru. -- **ContainerBuilder** - měnitelný model kontejneru používaný během kompilace; drží definice služeb, ještě než jakákoli skutečná služba existuje. Viz [Tvorba rozšíření |extensions#ContainerBuilder]. -- **Service** (služba) - objekt spravovaný kontejnerem, obvykle vytvořený jednou a sdílený (singleton) - databázové spojení, mailer, logger. -- **Definition** (definice) - recept na službu: její typ, jak ji vytvořit a co udělat potom. Nette z definic vytvoří tovární metody kontejneru; existuje jich několik druhů (viz [typy definic |extensions#Typy definic]). -- **Type** (typ) - třída nebo rozhraní služby, podle kterého autowiring přiřazuje služby na místa, která je vyžadují. -- **Autowiring** - automatické předávání služeb do konstruktorů a metod podle jejich typu, takže závislosti nemusíte propojovat ručně. -- **Tag** - štítek připojený k definici (volitelně s hodnotou); rozšíření pak všechny služby s ním najde přes `findByTag()`. -- **Setup** - další volání provedená na službě hned po jejím vytvoření - volání metod nebo nastavení vlastností, přidávaná přes `addSetup()`. -- **Alias** - alternativní název existující služby. - -{{maintitle: Dependency Injection: časté otázky, IoC a service locator}} -{{description: Je DI jiný název pro IoC? Co je service locator? Kdy je lepší DI nepoužívat? Srozumitelné odpovědi na nejčastější otázky o dependency injection.}} diff --git a/dependency-injection/cs/global-state.texy b/dependency-injection/cs/global-state.texy deleted file mode 100644 index 38e3b992fe..0000000000 --- a/dependency-injection/cs/global-state.texy +++ /dev/null @@ -1,299 +0,0 @@ -Globální stav a singletony -************************** - -.[perex] -Varování: Následující konstrukce jsou příznakem špatně navrženého kódu: - -- `Foo::getInstance()` -- `DB::insert(...)` -- `Article::setDb($db)` -- `ClassName::$var` nebo `static::$var` - -Vyskytují se některé z těchto konstrukcí ve vašem kódu? Pak máte příležitost k jeho zlepšení. Možná si říkáte, že jde o běžné konstrukce, které vídáte třeba i v ukázkových řešeních různých knihoven a frameworků. Pokud je tomu tak, pak návrh jejich kódu není dobrý. - -Nyní rozhodně nemluvíme o jakési akademické čistotě. Všechny tyto konstrukce mají jedno společné: využívají globální stav. A ten má destruktivní dopad na kvalitu kódu. Třídy lžou o svých závislostech. Kód se stává nepředvídatelným. Mate programátory a snižuje jejich efektivitu. - -V této kapitole si vysvětlíme, proč tomu tak je, a jak se globálnímu stavu vyhnout. - - -Globální provázání ------------------- - -V ideálním světě by měl být objekt schopen komunikovat pouze s objekty, které mu byly [přímo předány |passing-dependencies]. Pokud vytvořím dva objekty `A` a `B` a nikdy nepředám referenci mezi nimi, pak se ani `A`, ani `B` nemohou dostat k druhému objektu nebo změnit jeho stav. To je velmi žádoucí vlastnost kódu. Je to podobné, jako když máte baterii a žárovku; žárovka nebude svítit, dokud ji s baterií nepropojíte drátem. - -To ale neplatí u globálních (statických) proměnných nebo singletonů. Objekt `A` by se mohl *bezdrátově* dostat k objektu `C` a modifikovat jej bez jakéhokoliv předání reference, tím, že zavolá `C::changeSomething()`. Pokud se objekt `B` také chopí globálního `C`, pak se `A` a `B` mohou navzájem ovlivňovat prostřednictvím `C`. - -Použití globálních proměnných do systému vnáší novou formu *bezdrátové* provázanosti, která není zvenčí vidět. Vytváří kouřovou clonu komplikující pochopení a používání kódu. Aby vývojáři závislostem skutečně porozuměli, musí přečíst každý řádek zdrojového kódu. Místo pouhého seznámení se s rozhraním tříd. Jde navíc o provázanost zcela zbytečnou. Globální stav se používá kvůli tomu, že je snadno odkudkoliv přístupný a umožňuje třeba zapsat do databáze přes globální (statickou) metodu `DB::insert()`. Ale jak si ukážeme, výhoda, kterou to přináší, je nepatrná, naopak komplikace to způsobuje fatální. - -.[note] -Z hlediska chování není rozdíl mezi globální a statickou proměnnou. Jsou stejně škodlivé. - - -Strašidelné působení na dálku ------------------------------ - -"Strašidelné působení na dálku" - tak slavně nazval Albert Einstein jev v kvantové fyzice, který mu naháněl husí kůži. -Jedná se o kvantové propojení, jehož zvláštností je, že když změříte informaci o jedné částici, okamžitě tím ovlivníte částici druhou, i když jsou od sebe vzdáleny miliony světelných let. Což zdánlivě porušuje základní zákon vesmíru, že nic se nemůže šířit rychleji než světlo. - -V softwarovém světě můžeme "strašidelným působením na dálku" nazvat situaci, kdy spustíme nějaký proces, o kterém se domníváme, že je izolovaný (protože jsme mu nepředali žádné reference), ale ve vzdálených místech systému dojde k neočekávaným interakcím a změnám stavu, o kterých jsme neměli tušení. K tomu může dojít pouze prostřednictvím globálního stavu. - -Představte si, že se připojíte k týmu vývojářů projektu, který má rozsáhlou vyspělou kódovou základnu. Váš nový vedoucí vás požádá o implementaci nové funkce a vy jako správný vývojář začnete psaním testu. Protože jste ale v projektu noví, děláte spoustu průzkumných testů typu "co se stane, když zavolám tuto metodu". A zkusíte napsat následující test: - -```php -function testCreditCardCharge() -{ - $cc = new CreditCard('1234567890123456', 5, 2028); // číslo vaší karty - $cc->charge(100); -} -``` - -Spustíte kód, třeba několikrát, a po nějaké době si všimnete na mobilu notifikací z banky, že při každém spuštění se strhlo 100 dolarů z vaší platební karty 🤦‍♂️ - -Jak proboha mohl test způsobit skutečné stržení peněz? Operovat s platební kartou není snadné. Musíte komunikovat s webovou službou třetí strany, musíte znát URL této webové služby, musíte se přihlásit a tak dále. Žádná z těchto informací není v testu obsažena. Ba co hůř, ani nevíte, kde jsou tyto informace přítomny, a tedy ani jak mockovat externí závislosti, aby každé spuštění nevedlo k tomu, že se znovu strhne 100 dolarů. A jak jste měli jako noví vývojáři vědět, že to, co se chystáte udělat, povede k tomu, že budete o 100 dolarů chudší? - -To je strašidelné působení na dálku! - -Nezbývá vám, než se dlouze hrabat ve spoustě zdrojových kódů, ptát se starších a zkušenějších kolegů, než pochopíte, jak vazby v projektu fungují. To je způsobeno tím, že při pohledu na rozhraní třídy `CreditCard` nelze zjistit globální stav, který je třeba inicializovat. Dokonce ani pohled do zdrojového kódu třídy vám neprozradí, kterou inicializační metodu máte zavolat. V nejlepším případě můžete najít globální proměnnou, ke které se přistupuje, a z ní se pokusit odhadnout, jak ji inicializovat. - -Třídy v takovém projektu jsou patologickými lháři. Platební karta předstírá, že ji stačí instancovat a zavolat metodu `charge()`. Ve skrytu však spolupracuje s jinou třídou `PaymentGateway`, která představuje platební bránu. I její rozhraní říká, že ji lze inicializovat samostatně, ale ve skutečnosti si vytáhne přihlašovací údaje z nějakého konfiguračního souboru a tak dále. Vývojářům, kteří tento kód napsali, je jasné, že `CreditCard` potřebuje `PaymentGateway`. Napsali kód tímto způsobem. Ale pro každého, kdo je v projektu nový, je to naprostá záhada a brání to učení. - -Jak situaci opravit? Snadno. **Nechte API deklarovat závislosti.** - -```php -function testCreditCardCharge() -{ - $gateway = new PaymentGateway(/* ... */); - $cc = new CreditCard('1234567890123456', 5, 2028); - $cc->charge($gateway, 100); -} -``` - -Všimněte si, jak jsou najednou provázanosti uvnitř kódu zřejmé. Tím, že metoda `charge()` deklaruje, že potřebuje `PaymentGateway`, nemusíte se na to, jak je kód provázaný, nikoho ptát. Víte, že musíte vytvořit její instanci, a když se o to pokusíte, narazíte na to, že musíte dodat přístupové parametry. Bez nich by kód nešel ani spustit. - -A hlavně nyní můžete platební bránu mockovat, takže se vám při každém spuštění testu nebude účtovat 100 dolarů. - -Globální stav způsobuje, že se vaše objekty mohou tajně dostat k věcem, které nejsou deklarovány v jejich API, a v důsledku toho dělají z vašich API patologické lháře. - -Možná jste o tom dříve takto nepřemýšleli, ale kdykoli používáte globální stav, vytváříte tajné bezdrátové komunikační kanály. Strašidelné působení na dálku nutí vývojáře číst každý řádek kódu, aby pochopili potenciální interakce, snižuje produktivitu vývojářů a mate nové členy týmu. Pokud jste vy ten, kdo kód vytvořil, znáte skutečné závislosti, ale každý, kdo přijde po vás, je bezradný. - -Nepište kód, který využívá globální stav, dejte přednost předávání závislostí. Tedy dependency injection. - - -Křehkost globálního stavu -------------------------- - -V kódu, který používá globální stav a singletony, není nikdy jisté, kdy a kdo tento stav změnil. Toto riziko se objevuje již při inicializaci. Následující kód má vytvořit databázové spojení a inicializovat platební bránu, avšak neustále vyhazuje výjimku a hledání příčiny je nesmírně zdlouhavé: - -```php -PaymentGateway::init(); -DB::init('mysql:', 'user', 'password'); -``` - -Musíte podrobně procházet kód, abyste zjistili, že objekt `PaymentGateway` přistupuje bezdrátově k dalším objektům, z nichž některé vyžadují databázové připojení. Tedy je nutné inicializovat databázi dříve než `PaymentGateway`. Nicméně kouřová clona globálního stavu toto před vámi skrývá. Kolik času byste ušetřili, kdyby API jednotlivých tříd neklamalo a deklarovalo své závislosti? - -```php -$db = new DB('mysql:', 'user', 'password'); -$gateway = new PaymentGateway($db, /* ... */); -``` - -Podobný problém se objevuje i při použití globálního přístupu k databázovému spojení: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public function save(): void - { - DB::insert(/* ... */); - } -} -``` - -Při volání metody `save()` není jisté, zda bylo již vytvořeno připojení k databázi a kdo nese odpovědnost za jeho vytvoření. Pokud chceme například měnit databázové připojení za běhu, třeba kvůli testům, museli bychom nejspíš vytvořit další metody jako například `DB::reconnect(...)` nebo `DB::reconnectForTest()`. - -Zvažme příklad: - -```php -$article = new Article; -// ... -DB::reconnectForTest(); -Foo::doSomething(); -$article->save(); -``` - -Kde máme jistotu, že při volání `$article->save()` se opravdu používá testovací databáze? Co když metoda `Foo::doSomething()` změnila globální databázové připojení? Pro zjištění bychom museli prozkoumat zdrojový kód třídy `Foo` a pravděpodobně i mnoha dalších tříd. Tento přístup by však přinesl pouze krátkodobou odpověď, jelikož se situace může v budoucnu změnit. - -A co když připojení k databázi přesuneme do statické proměnné uvnitř třídy `Article`? - -```php -class Article -{ - private static DB $db; - - public static function setDb(DB $db): void - { - self::$db = $db; - } - - public function save(): void - { - self::$db->insert(/* ... */); - } -} -``` - -Tím se vůbec nic nezměnilo. Problémem je globální stav a je úplně jedno, ve které třídě se skrývá. V tomto případě, stejně jako v předchozím, nemáme při volání metody `$article->save()` žádné vodítko k tomu, do jaké databáze se zapíše. Kdokoliv na druhém konci aplikace mohl kdykoliv pomocí `Article::setDb()` databázi změnit. Nám pod rukama. - -Globální stav činí naši aplikaci **nesmírně křehkou**. - -Existuje však jednoduchý způsob, jak s tímto problémem naložit. Stačí nechat API deklarovat závislosti, čímž se zajistí správná funkčnost. - -```php -class Article -{ - public function __construct( - private DB $db, - ) { - } - - public function save(): void - { - $this->db->insert(/* ... */); - } -} - -$article = new Article($db); -// ... -Foo::doSomething(); -$article->save(); -``` - -Díky tomuto přístupu odpadá obava o skryté a neočekávané změny připojení k databázi. Nyní máme jistotu, kam se článek ukládá, a žádné úpravy kódu uvnitř jiné nesouvisející třídy již nemohou situaci změnit. Kód už není křehký, ale stabilní. - -Nepište kód, který využívá globální stav, dejte přednost předávání závislostí. Tedy dependency injection. - - -Singleton ---------- - -Singleton je návrhový vzor, který podle [definice |https://en.wikipedia.org/wiki/Singleton_pattern] ze známé publikace Gang of Four omezuje třídu na jedinou instanci a nabízí k ní globální přístup. Implementace tohoto vzoru se obvykle podobá následujícímu kódu: - -```php -class Singleton -{ - private static self $instance; - - public static function getInstance(): self - { - self::$instance ??= new self; - return self::$instance; - } - - // a další metody plnící funkce dané třídy -} -``` - -Bohužel, singleton zavádí do aplikace globální stav. A jak jsme si ukázali výše, globální stav je nežádoucí. Proto je singleton považován za antipattern. - -Nepoužívejte ve svém kódu singletony a nahraďte je jinými mechanismy. Singletony opravdu nepotřebujete. Pokud však potřebujete zaručit existenci jediné instance třídy pro celou aplikaci, nechte to na [DI kontejneru |container]. Vytvořte tak aplikační singleton, neboli službu. Tím se třída přestane věnovat zajištění své vlastní jedinečnosti (tj. nebude mít metodu `getInstance()` a statickou proměnnou) a bude plnit pouze své funkce. Tak přestane porušovat princip jediné odpovědnosti. - - -Globální stav versus testy --------------------------- - -Při psaní testů předpokládáme, že každý test je izolovanou jednotkou a že do něj nevstupuje žádný externí stav. A žádný stav testy neopouští. Po dokončení testu by měl být veškerý související stav s testem odstraněn automaticky garbage collectorem. Díky tomu jsou testy izolované. Proto můžeme testy spouštět v libovolném pořadí. - -Pokud jsou však přítomny globální stavy/singletony, všechny tyto příjemné předpoklady se rozpadají. Stav může do testu vstupovat a vystupovat z něj. Najednou může záležet na pořadí testů. - -Abychom vůbec mohli testovat singletony, vývojáři často musí rozvolnit jejich vlastnosti, třeba tím, že dovolí instanci nahradit jinou. Taková řešení jsou v nejlepším případě hackem, který vytváří obtížně udržovatelný a srozumitelný kód. Každý test nebo metoda `tearDown()`, která ovlivní jakýkoli globální stav, musí tyto změny vrátit zpět. - -Globální stav je největší bolestí hlavy při unit testování! - -Jak situaci opravit? Snadno. Nepište kód, který využívá singletony, dejte přednost předávání závislostí. Tedy dependency injection. - - -Globální konstanty ------------------- - -Globální stav se neomezuje pouze na používání singletonů a statických proměnných, ale může se týkat také globálních konstant. - -Konstanty, jejichž hodnota představuje univerzální pravdu (`M_PI`) nebo nese samonosnou informaci (`PREG_BACKTRACK_LIMIT_ERROR`), jsou jednoznačně v pořádku. Naopak konstanty, které slouží jako způsob, jak *bezdrátově* předat informaci dovnitř kódu, nejsou ničím jiným než skrytou závislostí. Jako třeba `LOG_FILE` v následujícím příkladu. Použití konstanty `FILE_APPEND` je zcela korektní. - -```php -const LOG_FILE = '...'; - -class Foo -{ - public function doSomething() - { - // ... - file_put_contents(LOG_FILE, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -V tomto případě bychom měli cestu k souboru s logem deklarovat jako parametr v konstruktoru třídy `Foo`, aby se stala součástí API: - -```php -class Foo -{ - public function __construct( - private string $logFile, - ) { - } - - public function doSomething() - { - // ... - file_put_contents($this->logFile, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Nyní můžeme předat informaci o cestě k souboru pro logování a snadno ji měnit podle potřeby, což usnadňuje testování a údržbu kódu. - - -Globální funkce a statické metody ---------------------------------- - -Chceme zdůraznit, že samotné používání statických metod a globálních funkcí není problematické. Vysvětlovali jsme, v čem spočívá nevhodnost použití `DB::insert()` a podobných metod, ale vždy se jednalo pouze o záležitost globálního stavu, který je uložen v nějaké statické proměnné. Metoda `DB::insert()` vyžaduje existenci statické proměnné, protože v ní je uloženo připojení k databázi. Bez této proměnné by bylo nemožné metodu implementovat. - -Používání deterministických statických metod a funkcí, jako například `Closure::fromCallable()`, `strlen()` a mnoha dalších, je v naprostém souladu s dependency injection. Tyto funkce vždy vracejí stejné výsledky ze stejných vstupních parametrů a jsou tedy předvídatelné. Nepoužívají žádný globální stav. - -Existují ovšem i funkce v PHP, které nejsou deterministické. K nim patří například funkce `htmlspecialchars()`. Její třetí parametr `$encoding`, pokud není uveden, jako výchozí hodnotu má hodnotu konfigurační volby `ini_get('default_charset')`. Proto se doporučuje tento parametr vždy uvádět a předejít tak případnému nepředvídatelnému chování funkce. Nette to důsledně dělá. - -Některé funkce, jako například `strtolower()`, `strtoupper()` a podobné, se v nedávné minulosti nedeterministicky chovaly a byly závislé na nastavení `setlocale()`. To způsobovalo mnoho komplikací, nejčastěji při práci s tureckým jazykem. Ten totiž rozlišuje malé i velké písmeno `I` s tečkou i bez tečky. Takže `strtolower('I')` vracelo znak `ı` a `strtoupper('i')` znak `İ`, což vedlo k tomu, že aplikace začaly způsobovat řadu záhadných chyb. Tento problém byl však odstraněn v PHP verze 8.2 a funkce již nejsou závislé na locale. - -Jde o pěkný příklad, jak globální stav potrápil tisíce vývojářů na celém světě. Řešením nakonec bylo učinit funkce nezávislými na locale, tedy odstranit skrytou závislost. - - -Kdy je možné použít globální stav? ----------------------------------- - -Existují určité specifické situace, kdy je možné využít globální stav. Například při ladění kódu, když potřebujete vypsat hodnotu proměnné nebo změřit dobu trvání určité části programu. V takových případech, které se týkají dočasných akcí, jež budou později odstraněny z kódu, je možné legitimně využít globálně dostupný dumper nebo stopky. Tyto nástroje totiž nejsou součástí návrhu kódu. - -Dalším příkladem jsou funkce pro práci s regulárními výrazy `preg_*`, které interně ukládají zkompilované regulární výrazy do statické cache v paměti. Když tedy voláte stejný regulární výraz vícekrát na různých místech kódu, zkompiluje se pouze jednou. Cache šetří výkon a zároveň je pro uživatele zcela neviditelná, proto lze takové využití považovat za legitimní. - - -Shrnutí -------- - -Probrali jsme si, proč má smysl: - -1) Odstranit z kódu veškeré měnitelné statické proměnné (globální stav) -2) Deklarovat závislosti -3) A používat dependency injection - -Když promýšlíte návrh kódu, myslete na to, že každé měnitelné `static $foo` představuje potenciální zdroj problémů. Aby váš kód byl prostředím respektujícím DI, je nezbytné úplně vymýtit globální stav a nahradit ho pomocí dependency injection. - -Během tohoto procesu možná zjistíte, že je třeba třídu rozdělit, protože má více než jednu odpovědnost. Nebojte se toho; usilujte o princip jedné odpovědnosti. - -Chcete si to všechno raději zažít, než o tom jen číst? Kurz [Dependency Injection by Example |https://github.com/nette-examples/di-by-example] má kapitolu, ve které nevinně vypadající test potichu odčerpá peníze z účtu, a ten test si můžete spustit. - -*Rád bych poděkoval Miškovi Heverymu, jehož články, jako je [Flaw: Brittle Global State & Singletons |https://web.archive.org/web/20230321084133/http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/], jsou základem této kapitoly.* - -{{maintitle: Singleton a globální stav v PHP: proč se jim vyhnout}} -{{description: Singletony, statické metody a globální konstanty jsou příznakem špatného návrhu. Proč je kód s globálním stavem křehký a špatně se testuje a kdy to nevadí.}} diff --git a/dependency-injection/cs/introduction.texy b/dependency-injection/cs/introduction.texy deleted file mode 100644 index 41a9b6df63..0000000000 --- a/dependency-injection/cs/introduction.texy +++ /dev/null @@ -1,528 +0,0 @@ -Co je Dependency Injection? -*************************** - -.[perex] -Tato kapitola vás seznámí se základními programátorskými postupy, které byste měli dodržovat při psaní všech aplikací. Jde o základy nutné pro psaní čistého, srozumitelného a udržovatelného kódu. - -Pokud si tato pravidla osvojíte a budete je dodržovat, bude vám Nette v každém kroku vycházet vstříc. Bude za vás řešit rutinní úlohy a poskytne vám maximální pohodlí, abyste se mohli soustředit na samotnou logiku. - -Principy, které si zde ukážeme, jsou přitom celkem prosté. Nemusíte se ničeho obávat. - - -Pamatujete na svůj první program? ---------------------------------- - -Netušíme sice, v jakém jazyce jste ho psali, ale kdyby to bylo PHP, nejspíš by vypadal nějak takto: - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} - -echo soucet(23, 1); // vypíše 24 -``` - -Pár triviálních řádků kódu, ale přitom se v nich skrývá tolik klíčových konceptů. Že existují proměnné. Že se kód člení do menších jednotek, což jsou kupříkladu funkce. Že jim předáváme vstupní argumenty a ony vracejí výsledky. Chybí tam už jen podmínky a cykly. - -To, že funkci předáme vstupní data a ona vrátí výsledek, je perfektně srozumitelný koncept, který se používá i v jiných oborech, jako je třeba matematika. - -Funkce má svoji signaturu, kterou tvoří její název, přehled parametrů a jejich typů, a nakonec typ návratové hodnoty. Jako uživatele nás zajímá signatura, o vnitřní implementaci obvykle nepotřebujeme nic vědět. - -Teď si představte, že by signatura funkce vypadala takto: - -```php -function soucet(float $x): float -``` - -Součet s jedním parametrem? To je divné… A co třeba takto? - -```php -function soucet(): float -``` - -Tak to už je opravdu hodně divné, že? Jak se funkce asi používá? - -```php -echo soucet(); // co asi vypíše? -``` - -Při pohledu na takový kód bychom byli zmateni. Nejen že by mu nerozuměl začátečník, takovému kódu nerozumí ani zdatný programátor. - -Přemýšlíte, jak by vlastně taková funkce vypadala uvnitř? Kde vezme sčítance? Zřejmě by si je *nějakým způsobem* obstarala sama, třeba takto: - -```php -function soucet(): float -{ - $a = Input::get('a'); - $b = Input::get('b'); - return $a + $b; -} -``` - -V těle funkce jsme objevili skryté vazby na další globální funkce či statické metody. Abychom zjistili, odkud se skutečně sčítance berou, musíme pátrat dál. - - -Tudy ne! --------- - -Návrh, který jsme si právě ukázali, je esencí mnoha negativních rysů: - -- signatura funkce se tvářila, že nepotřebuje sčítance, což nás mátlo -- vůbec nevíme, jak přimět funkci sečíst jiná dvě čísla -- museli jsme se podívat do kódu, abychom zjistili, kde sčítance bere -- objevili jsme skryté vazby -- pro plné pochopení je třeba prozkoumat i tyto vazby - -A je vůbec úkolem sčítací funkce obstarávat si vstupy? Samozřejmě, že není. Její zodpovědností je pouze samotné sčítání. - - -S takovým kódem se nechceme setkat a rozhodně ho nechceme psát. Náprava je přitom jednoduchá: vrátit se k základům a prostě použít parametry: - - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} -``` - - -Pravidlo č. 1: nech si to předat --------------------------------- - -Nejdůležitější pravidlo zní: **všechna data, která funkce nebo třídy potřebují, jim musí být předána**. - -Místo toho, abyste vymýšleli skryté způsoby, pomocí kterých by se k nim mohly nějak dostat samy, jednoduše parametry předejte. Ušetříte čas potřebný na vymýšlení skrytých cest, které rozhodně váš kód nevylepší. - -Pokud budete toto pravidlo vždy a všude dodržovat, jste na cestě ke kódu bez skrytých vazeb. Ke kódu, který je srozumitelný nejen autorovi, ale i každému, kdo jej po něm bude číst. Kde je vše pochopitelné ze signatur funkcí a tříd a není třeba pátrat po skrytých tajemstvích v implementaci. - -Této technice se odborně říká **dependency injection**. A těm datům se říká **závislosti.** Přitom je to prachobyčejné předávání parametrů, nic víc. - -.[note] -Nezaměňujte prosím dependency injection, což je návrhový vzor, s "dependency injection container", což je zase nástroj, tedy něco diametrálně odlišného. Kontejnerům se budeme věnovat později. - - -Od funkcí ke třídám -------------------- - -A jak s tím souvisí třídy? Třída je komplexnější celek než jednoduchá funkce, nicméně pravidlo č. 1 platí bezezbytku i tady. Jen existuje [víc možností, jak argumenty předat|passing-dependencies]. Kupříkladu docela podobně jako v případě funkce: - -```php -class Matematika -{ - public function soucet(float $a, float $b): float - { - return $a + $b; - } -} - -$math = new Matematika; -echo $math->soucet(23, 1); // 24 -``` - -Nebo pomocí jiných metod, či přímo konstruktoru: - -```php -class Soucet -{ - public function __construct( - private float $a, - private float $b, - ) { - } - - public function spocti(): float - { - return $this->a + $this->b; - } -} - -$soucet = new Soucet(23, 1); -echo $soucet->spocti(); // 24 -``` - -Obě ukázky jsou zcela v souladu s dependency injection. - - -Reálné příklady ---------------- - -V reálném světě nebudete psát třídy pro sčítání čísel. Pojďme se přesunout k příkladům z praxe. - -Mějme třídu `Article` reprezentující článek na blogu: - -```php -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - // uložíme článek do databáze - } -} -``` - -a použití bude následující: - -```php -$article = new Article; -$article->title = '10 Things You Need to Know About Losing Weight'; -$article->content = 'Every year millions of people in ...'; -$article->save(); -``` - -Metoda `save()` uloží článek do databázové tabulky. Implementovat ji za pomoci [Nette Database |database:] bude hračka, nebýt jednoho zádrhelu: kde má `Article` vzít připojení k databázi, tj. objekt třídy `Nette\Database\Connection`? - -Zdá se, že máme spoustu možností. Může jej vzít odněkud ze statické proměnné. Nebo dědit od třídy, která spojení s databází zajistí. Nebo využít tzv. [singletonu |global-state#Singleton]. Nebo tzv. facades, které se používají v Laravelu: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - DB::insert( - 'INSERT INTO articles (title, content) VALUES (?, ?)', - [$this->title, $this->content], - ); - } -} -``` - -Skvělé, problém jsme vyřešili. - -Nebo ne? - -Připomeňme [#pravidlo č. 1: nech si to předat]: všechny závislosti, které třída potřebuje, jí musí být předány. Protože pokud pravidlo porušíme, nastoupíme cestu ke špinavému kódu plnému skrytých vazeb a nesrozumitelnosti a výsledkem bude aplikace, kterou bude bolest udržovat a vyvíjet. - -Uživatel třídy `Article` netuší, kam metoda `save()` článek ukládá. Do databázové tabulky? Do které, ostré nebo testovací? A jak to lze změnit? - -Uživatel se musí podívat, jak je implementovaná metoda `save()`, a najde použití metody `DB::insert()`. Takže musí pátrat dál, jak si tato metoda obstarává databázové spojení. A skryté vazby mohou tvořit docela dlouhý řetězec. - -V čistém a dobře navrženém kódu se nikdy nevyskytují skryté vazby, Laravelovské facades nebo statické proměnné. V čistém a dobře navrženém kódu se předávají argumenty: - -```php -class Article -{ - public function save(Nette\Database\Connection $db): void - { - $db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -Ještě praktičtější, jak uvidíme dále, to bude konstruktorem: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function save(): void - { - $this->db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -.[note] -Pokud jste zkušený programátor, možná si říkáte, že `Article` by vůbec neměl mít metodu `save()`, měl by představovat čistě datovou komponentu a o ukládání by se měl starat oddělený repozitář. To dává smysl. Ale tím bychom se dostali hodně daleko nad rámec tématu, kterým je dependency injection, i nad rámec snahy uvádět jednoduché příklady. - -Budete-li psát třídu vyžadující ke své činnosti např. databázi, nevymýšlejte, odkud ji získat, ale nechte si ji předat. Třeba jako parametr konstruktoru nebo jiné metody. Přiznejte závislosti. Přiznejte je v API vaší třídy. Získáte srozumitelný a předvídatelný kód. - -A co třeba tato třída, která loguje chybové zprávy: - -```php -class Logger -{ - public function log(string $message): void - { - $file = LOG_DIR . '/log.txt'; - file_put_contents($file, $message . "\n", FILE_APPEND); - } -} -``` - -Co myslíte, dodrželi jsme [#pravidlo č. 1: nech si to předat]? - -Nedodrželi. - -Klíčovou informaci, tedy adresář se souborem s logem, si třída *obstarává sama* z konstanty. - -Podívejte se na příklad použití: - -```php -$logger = new Logger; -$logger->log('Teplota je 23 °C'); -$logger->log('Teplota je 10 °C'); -``` - -Dokázali byste bez znalosti implementace zodpovědět otázku, kam se zprávy zapisují? Napadlo by vás, že pro fungování je potřeba existence konstanty `LOG_DIR`? A dokázali byste vytvořit druhou instanci, která bude zapisovat jinam? Určitě ne. - -Pojďme třídu opravit: - -```php -class Logger -{ - public function __construct( - private string $file, - ) { - } - - public function log(string $message): void - { - file_put_contents($this->file, $message . "\n", FILE_APPEND); - } -} -``` - -Třída je teď mnohem srozumitelnější, konfigurovatelnější a tedy užitečnější. - -```php -$logger = new Logger('/cesta/k/logu.txt'); -$logger->log('Teplota je 15 °C'); -``` - - -Ale to mě nezajímá! -------------------- - -*"Když vytvořím objekt Article a zavolám save(), tak nechci řešit databázi, prostě chci, aby se uložil do té, kterou mám nastavenou v konfiguraci."* - -*"Když použiju Logger, tak prostě chci, aby se zpráva zapsala, a nechci řešit kam. Ať se použije globální nastavení."* - -To jsou správné připomínky. - -Jako příklad si ukážeme třídu rozesílající newslettery, která zaloguje, jak to dopadlo: - -```php -class NewsletterDistributor -{ - public function distribute(): void - { - $logger = new Logger(/* ... */); - try { - $this->sendEmails(); - $logger->log('Emaily byly rozeslány'); - - } catch (Exception $e) { - $logger->log('Došlo k chybě při rozesílání'); - throw $e; - } - } -} -``` - -Vylepšený `Logger`, který již nepoužívá konstantu `LOG_DIR`, vyžaduje v konstruktoru uvést cestu k souboru. Jak tohle vyřešit? Třídu `NewsletterDistributor` vůbec nezajímá, kam se zprávy zapisují, chce je jen zapsat. - -Řešením je opět [#pravidlo č. 1: nech si to předat]: všechna data, která třída potřebuje, jí předáme. - -Takže to znamená, že si skrze konstruktor předáme cestu k logu, kterou pak použijeme při vytváření objektu `Logger`? - -```php -class NewsletterDistributor -{ - public function __construct( - private string $file, // ⛔ TAKHLE NE! - ) { - } - - public function distribute(): void - { - $logger = new Logger($this->file); -``` - -Takhle ne! Cesta totiž **nepatří** mezi data, která třída `NewsletterDistributor` potřebuje; ta potřebuje `Logger`. Vnímáte ten rozdíl? Třída `NewsletterDistributor` potřebuje logger jako takový. Takže ten si předáme: - -```php -class NewsletterDistributor -{ - public function __construct( - private Logger $logger, // ✅ - ) { - } - - public function distribute(): void - { - try { - $this->sendEmails(); - $this->logger->log('Emaily byly rozeslány'); - - } catch (Exception $e) { - $this->logger->log('Došlo k chybě při rozesílání'); - throw $e; - } - } -} -``` - -Nyní je ze signatur třídy `NewsletterDistributor` jasné, že součástí její funkčnosti je i logování. A úkol vyměnit logger za jiný, třeba kvůli testování, je zcela triviální. Navíc pokud by se konstruktor třídy `Logger` změnil, nebude to mít na naši třídu žádný vliv. - - -Pravidlo č. 2: ber, co tvé jest -------------------------------- - -Nenechte se zmást a nenechte si předávat závislosti svých závislostí. Nechte si předávat jen své závislosti. - -Díky tomu bude kód využívající jiné objekty zcela nezávislý na změnách jejich konstruktorů. Jeho API bude pravdivější. A hlavně bude triviální tyto závislosti vyměnit za jiné. - - -Nový člen rodiny ----------------- - -Ve vývojářském týmu padlo rozhodnutí vytvořit druhý logger, který zapisuje do databáze. Vytvoříme tedy třídu `DatabaseLogger`. Takže máme dvě třídy, `Logger` a `DatabaseLogger`, jedna zapisuje do souboru, druhá do databáze … nezdá se vám na tom pojmenování něco divného? Nebylo by lepší přejmenovat `Logger` na `FileLogger`? Určitě ano. - -Ale uděláme to chytře. Pod původním názvem vytvoříme rozhraní: - -```php -interface Logger -{ - function log(string $message): void; -} -``` - -… které budou oba loggery implementovat: - -```php -class FileLogger implements Logger -// ... - -class DatabaseLogger implements Logger -// ... -``` - -A díky tomu nebude potřeba nic měnit ve zbytku kódu, kde se logger využívá. Například konstruktor třídy `NewsletterDistributor` bude stále spokojen s tím, že jako parametr vyžaduje `Logger`. A bude jen na nás, kterou instanci mu předáme. - -**Proto nikdy nedáváme názvům rozhraní příponu `Interface` nebo předponu `I`.** Jinak by nebylo možné kód takto hezky rozvíjet. - - -Houstone, máme problém ----------------------- - -Zatímco v celé aplikaci si můžeme vystačit s jedinou instancí loggeru, ať už souborového nebo databázového, a jednoduše jej předáváme všude tam, kde se něco loguje, docela jinak je tomu v případě třídy `Article`. Její instance totiž vytváříme dle potřeby, klidně vícekrát. Jak se vypořádat s vazbou na databázi v jejím konstruktoru? - -Jako příklad může sloužit kontroler, který po odeslání formuláře má uložit článek do databáze: - -```php -class EditController extends Controller -{ - public function formSubmitted($data) - { - $article = new Article(/* ... */); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Možné řešení se přímo nabízí: necháme si objekt databáze předat konstruktorem do `EditController` a použijeme `$article = new Article($this->db)`. - -Stejně jako v předchozím případě s `Logger` a cestou k souboru, tohle není správný postup. Databáze není závislost `EditController`, ale `Article`. Předávat si databázi tedy jde proti [pravidlu č. 2: ber, co tvé jest |#Pravidlo č. 2: ber co tvé jest]. Když se změní konstruktor třídy `Article` (přibude nový parametr), bude nutné upravit také kód na všech místech, kde se vytváří instance. Ufff. - -Houstone, co navrhuješ? - - -Pravidlo č. 3: nech to na továrně ---------------------------------- - -Tím, že jsme zrušili skryté vazby a všechny závislosti předáváme jako argumenty, získali jsme konfigurovatelnější a pružnější třídy. A tudíž potřebujeme ještě cosi dalšího, co nám ty pružnější třídy vytvoří a nakonfiguruje. Budeme tomu říkat továrny. - -Pravidlo zní: pokud má třída závislosti, nech vytváření jejích instancí na továrně. - -Továrny jsou chytřejší náhrada operátoru `new` ve světě dependency injection. - -.[note] -Nezaměňujte prosím s návrhovým vzorem *factory method*, který popisuje specifický způsob využití továren a s tímto tématem nesouvisí. - - -Továrna -------- - -Továrna je metoda nebo třída, která vyrábí a konfiguruje objekty. Třídu vyrábějící `Article` nazveme `ArticleFactory` a mohla by vypadat například takto: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Její použití v kontroleru bude následující: - -```php -class EditController extends Controller -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function formSubmitted($data) - { - // necháme továrnu vytvořit objekt - $article = $this->articleFactory->create(); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Když se v tuto chvíli změní signatura konstruktoru třídy `Article`, jediná část kódu, která na to musí reagovat, je samotná továrna `ArticleFactory`. Veškerého dalšího kódu, který s objekty `Article` pracuje, jako například `EditController`, se to nijak nedotkne. - -Možná si teď klepete na čelo, jestli jsme si vůbec pomohli. Množství kódu narostlo a celé to začíná vypadat podezřele komplikovaně. - -Nemějte obavy, za chvíli se dostaneme k Nette DI kontejneru. A ten má řadu es v rukávu, kterými budování aplikací používajících dependency injection nesmírně zjednoduší. Tak kupříkladu místo třídy `ArticleFactory` bude stačit [napsat pouhý interface |factory]: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Ale to předbíháme, ještě vydržte :-) - - -Shrnutí -------- - -Na začátku této kapitoly jsme slibovali, že si ukážeme postup, jak navrhovat čistý kód. Stačí třídám - -1) [předávat závislosti, které potřebují |#Pravidlo č. 1: nech si to předat] -2) [a naopak nepředávat, co přímo nepotřebují |#Pravidlo č. 2: ber co tvé jest] -3) [a že objekty se závislostmi se nejlépe vyrábí v továrnách |#Pravidlo č. 3: nech to na továrně] - -Nemusí se to tak na první pohled zdát, ale tato tři pravidla mají dalekosáhlé důsledky. Vedou k radikálně jinému pohledu na návrh kódu. Stojí to za to? Programátoři, kteří zahodili staré zvyky a začali důsledně používat dependency injection, považují tento krok za zásadní moment v profesním životě. Otevřel se jim svět přehledných a udržitelných aplikací. - -Co když ale kód důsledně dependency injection nepoužívá? Co když je postaven na statických metodách nebo singletonech? Přináší to nějaké problémy? [Přináší a velmi zásadní |global-state]. - -{{maintitle: Co je Dependency Injection? Návod pro PHP s příklady}} -{{description: Dependency injection vysvětlené od začátku na příkladech v PHP: tři jednoduchá pravidla, která vás dovedou k čistému, srozumitelnému a udržitelnému kódu.}} diff --git a/dependency-injection/cs/nette-container.texy b/dependency-injection/cs/nette-container.texy deleted file mode 100644 index dad1d3eaf5..0000000000 --- a/dependency-injection/cs/nette-container.texy +++ /dev/null @@ -1,145 +0,0 @@ -Nette DI Container -****************** - -.[perex] -Nette DI je jednou z nejzajímavějších knihoven Nette. Umí generovat a automaticky aktualizovat kompilované DI kontejnery, které jsou extrémně rychlé a úžasně snadno konfigurovatelné. - -Podobu služeb, které má vytvářet DI kontejner, definujeme obvykle pomocí konfiguračních souborů ve [formátu NEON|neon:format]. Kontejner, který jsme ručně vytvořili v [předchozí kapitole|container], by se zapsal takto: - -```neon -parameters: - db: - dsn: 'mysql:' - user: root - password: '***' - -services: - - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - - ArticleFactory - - EditController -``` - -Zápis je opravdu stručný. - -Všechny závislosti deklarované v konstruktorech tříd `ArticleFactory` a `EditController` si Nette DI samo zjistí a předá díky tzv. [autowiringu|autowiring], v konfiguračním souboru proto není potřeba nic uvádět. Takže i když dojde ke změně parametrů, nemusíte v konfiguraci nic měnit. Během vývoje Nette kontejner automaticky přegeneruje. Vy se můžete soustředit čistě na vývoj aplikace. - -Pokud chceme závislosti předávat pomocí setterů, použijeme k tomu sekci [setup |services#Setup]. - -Nette DI vygeneruje přímo PHP kód kontejneru. Výsledkem je tedy soubor `.php`, který si můžete otevřít a studovat. Díky tomu přesně vidíte, jak kontejner funguje. Můžete jej také debuggovat v IDE a krokovat. A hlavně: vygenerované PHP je extrémně rychlé. - -Nette DI umí také generovat kód [továren|factory] na základě dodaného rozhraní. Proto místo třídy `ArticleFactory` nám bude stačit vytvořit v aplikaci jen interface: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Celý příklad najdete v kurzu [Dependency Injection by Example |https://github.com/nette-examples/di-by-example], kde si kontejner nejprve napíšete ručně a teprve pak ho necháte vygenerovat. - - -Samostatné použití ------------------- - -Nasazení knihovny Nette DI do aplikace je velmi snadné. Nejprve ji nainstalujeme Composerem (protože stahování zipů je tááák zastaralé): - -```shell -composer require nette/di -``` - -Následující kód pomocí [Compileru |api:Nette\DI\Compiler] vytvoří instanci DI kontejneru podle konfigurace uložené v souboru `config.neon`: - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp'); -$class = $loader->load(function ($compiler) { - $compiler->loadConfig(__DIR__ . '/config.neon'); -}); -$container = new $class; -``` - -Kontejner se vygeneruje jen jednou, jeho kód se zapíše do cache (adresář `__DIR__ . '/temp'`) a při dalších požadavcích se už jen odsud načítá. - -`Compiler` sám o sobě zpřístupní v konfiguraci jen sekce `services` a `parameters`. Chcete-li používat další - třeba `search`, `decorator`, `di` nebo `inject` - nejdřív zaregistrujte jejich rozšíření. A abyste mohli registrovat rozšíření ze sekce `extensions` v konfiguraci, přidejte `ExtensionsExtension`: - -```php -$compiler->addExtension('search', new Nette\DI\Extensions\SearchExtension($tempDir)); -$compiler->addExtension('extensions', new Nette\DI\Extensions\ExtensionsExtension); -``` - -Všechna tato rozšíření za vás automaticky registruje [Configurator |application:bootstrapping] používaný v plných Nette aplikacích. - -Pokud v jednom cache adresáři udržujete několik různých kontejnerů, odlište je klíčem předaným jako druhý argument metody `load()`; ten se stane součástí názvu vygenerované třídy: - -```php -$class = $loader->load( - fn($compiler) => $compiler->loadConfig(__DIR__ . '/config.neon'), - 'my-key', -); -``` - -Pro vytvoření a získání služeb slouží metody `getService()` nebo `getByType()`. Takto vytvoříme objekt `EditController`: - -```php -$controller = $container->getByType(EditController::class); -$controller->someMethod(); -``` - -Během vývoje je užitečné aktivovat režim auto-refresh, kdy se kontejner automaticky přegeneruje, pokud dojde ke změně jakékoliv třídy nebo konfiguračního souboru. Stačí v konstruktoru [ContainerLoader |api:Nette\DI\ContainerLoader] uvést jako druhý argument `true`. - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp', true); -``` - -Auto-refresh funguje i v rámci jednoho procesu, což je důležité pro dlouho běžící workery, vývojové servery a CLI nástroje. Protože PHP nemůže znovu deklarovat už načtenou třídu, deklaruje každá přestavba kontejner pod novým názvem třídy a `load()` vrací název odpovídající aktuální konfiguraci. Volejte tedy `load()` znovu před každou jednotkou práce a kdykoli se vrácený název změní, vytvořte nový kontejner; pozná se i přestavba, kterou nad stejnou cache provedl jiný proces. Název třídy si nikdy neukládejte, mezi přestavbami není stabilní. .{data-version:3.2.7} - - -Práce s kontejnerem -------------------- - -Kromě `getService()` a `getByType()` nabízí objekt kontejneru několik dalších užitečných metod: - -- `getByType(string $type, bool $throw = true): ?object` vrátí službu daného typu. Pokud jako druhý argument předáte `false`, vrátí místo vyhození výjimky `null`, když žádná taková služba neexistuje. -- `hasService(string $name): bool` a `isCreated(string $name): bool` zjistí, zda je služba definovaná a zda už byla vytvořena. -- `getServiceDescriptors(): array<string, ServiceDescriptor>` popíše všechny registrované služby pod jejich názvy: typ, autowiring, tagy, aliasy a instanci, pokud už existuje. Nic přitom nevytvoří. .{data-version:3.2.7} -- `addService(string $name, object $service): static` vloží službu do kontejneru; closure s deklarovaným návratovým typem zaregistruje továrnu místo hotové instance. `removeService(string $name): void` ji odebere. -- `getParameters(): array` vrátí všechny parametry kontejneru, `getParameter($key)` vrátí jeden. -- `createInstance(string $class, array $args = []): object` vytvoří novou instanci dané třídy a předá jí závislosti konstruktoru pomocí autowiringu. -- `callMethod(callable $function, array $args = []): mixed` zavolá daný callable a předá mu argumenty pomocí autowiringu. -- `callInjects(object $service): void` zavolá na daném objektu všechny metody `inject*()` a předá jim závislosti. - -Konstruktor kontejneru také přijímá pole parametrů, které doplní ty definované v konfiguraci: - -```php -$container = new $class(['host' => 'localhost']); -``` - - -PSR-11 Container .{data-version:3.2.7} --------------------------------------- - -Třída `Nette\Bridges\DIPsr\PsrContainer` obalí kontejner a zpřístupní jej přes PSR-11 `ContainerInterface`, takže jej můžete předat jakékoliv knihovně nebo frameworku, který jej očekává: - -```php -$psr = new Nette\Bridges\DIPsr\PsrContainer($container); - -$psr->get(PDO::class); // typ, dohledaný autowiringem -$psr->get('database'); // název služby -``` - -Identifikátor se nejprve chápe jako typ a dohledá se autowiringem, teprve potom jako název služby. Pokud typu odpovídá více autowirovaných služeb, `get()` místo výběru jedné z nich vyhodí `NotFoundException`, zatímco `has()` vrátí `false`. Výjimky mostu implementují rozhraní z PSR-11, takže `catch (Psr\Container\NotFoundExceptionInterface)` funguje podle očekávání. - -Balíček `psr/container` není závislostí `nette/di`, takže si jej do aplikace přidejte sami: - -```shell -composer require psr/container -``` - - -Použití s frameworkem Nette ---------------------------- - -Jak jsme si ukázali, použití Nette DI není omezené na aplikace psané v Nette Frameworku, můžete jej pomocí pouhých 3 řádků kódu nasadit kdekoliv. Pokud však vyvíjíte aplikace v Nette Framework, konfiguraci a vytvoření kontejneru má na starosti [Bootstrap |application:bootstrapping#Konfigurace DI kontejneru]. - -{{maintitle: Nette DI Container: rychlý kompilovaný kontejner pro PHP}} -{{description: Nette DI generuje a automaticky aktualizuje kompilované kontejnery, které jsou extrémně rychlé a snadno se konfigurují v NEON. Podívejte se, jak ho použít.}} diff --git a/dependency-injection/cs/passing-dependencies.texy b/dependency-injection/cs/passing-dependencies.texy deleted file mode 100644 index 990d38a148..0000000000 --- a/dependency-injection/cs/passing-dependencies.texy +++ /dev/null @@ -1,218 +0,0 @@ -Předávání závislostí -******************** - -<div class=perex> - -Argumenty, nebo v terminologii DI "závislosti", lze do tříd předávat těmito hlavními způsoby: - -* předávání konstruktorem -* předávání metodou (tzv. setterem) -* nastavením proměnné -* metodou `inject*()` nebo atributem `#[Inject]` - -</div> - -Nyní si jednotlivé varianty ukážeme na konkrétních příkladech. - - -Předávání konstruktorem -======================= - -Závislosti jsou předávány v okamžiku vytváření objektu jako argumenty konstruktoru: - -```php -class MyClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -$obj = new MyClass($cache); -``` - -Tato forma je vhodná pro povinné závislosti, které třída nezbytně potřebuje ke své funkci, neboť bez nich nepůjde instanci vytvořit. - -Od PHP 8.0 můžeme použít kratší formu zápisu ([constructor property promotion |https://blog.nette.org/cs/php-8-0-kompletni-prehled-novinek#toc-constructor-property-promotion]), která je funkčně ekvivalentní: - -```php -// PHP 8.0 -class MyClass -{ - public function __construct( - private Cache $cache, - ) { - } -} -``` - -Od PHP 8.1 lze proměnnou označit příznakem `readonly`, který deklaruje, že obsah proměnné se už nezmění: - -```php -// PHP 8.1 -class MyClass -{ - public function __construct( - private readonly Cache $cache, - ) { - } -} -``` - -DI kontejner předá konstruktoru závislosti automaticky pomocí [autowiringu |autowiring]. Argumenty, které takto předat nelze (např. řetězce, čísla, booleany) [zapíšeme v konfiguraci |services#Argumenty]. - - -Constructor hell ----------------- - -Termín *constructor hell* označuje situaci, když potomek dědí od rodičovské třídy, jejíž konstruktor vyžaduje závislosti, a zároveň potomek vyžaduje závislosti. Přitom musí převzít a předat i ty rodičovské: - -```php -abstract class BaseClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass extends BaseClass -{ - private Database $db; - - // ⛔ CONSTRUCTOR HELL - public function __construct(Cache $cache, Database $db) - { - parent::__construct($cache); - $this->db = $db; - } -} -``` - -Problém nastane v okamžiku, kdy budeme chtít změnit konstruktor třídy `BaseClass`, třeba když přibude nová závislost. Pak je totiž nutné upravit také všechny konstruktory potomků. Což z takové úpravy dělá peklo. - -Jak tomu předcházet? Řešením je **dávat přednost [kompozici před dědičností |faq#Proč se upřednostňuje kompozice před dědičností]**. - -Tedy navrhneme kód jinak. Budeme se vyhýbat [abstraktním |nette:introduction-to-object-oriented-programming#Abstraktní třídy] `Base*` třídám. Místo toho, aby `MyClass` získávala určitou funkčnost tím, že dědí od `BaseClass`, si tuto funkčnost nechá předat jako závislost: - -```php -final class SomeFunctionality -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass -{ - private SomeFunctionality $sf; - private Database $db; - - public function __construct(SomeFunctionality $sf, Database $db) // ✅ - { - $this->sf = $sf; - $this->db = $db; - } -} -``` - - -Předávání setterem -================== - -Závislosti jsou předávány voláním metody, která je uloží do privátní proměnné. Obvyklou konvencí pojmenování těchto metod je tvar `set*()`, proto se jim říká settery, ale mohou se samozřejmě jmenovat jakkoliv jinak. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - $this->cache = $cache; - } -} - -$obj = new MyClass; -$obj->setCache($cache); -``` - -Tento způsob je vhodný pro nepovinné závislosti, které nejsou pro funkci třídy nezbytné, neboť není garantováno, že objekt závislost skutečně dostane (tj. že uživatel metodu zavolá). - -Zároveň tento způsob připouští volat setter opakovaně a závislost tak měnit. Pokud to není žádoucí, přidáme do metody kontrolu, nebo od PHP 8.1 označíme property `$cache` příznakem `readonly`. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - if (isset($this->cache)) { - throw new RuntimeException('The dependency has already been set'); - } - $this->cache = $cache; - } -} -``` - -Volání setteru definujeme v konfiguraci DI kontejneru v [klíči setup |services#Setup]. I tady se využívá automatického předávání závislostí pomocí autowiringu: - -```neon -services: - - create: MyClass - setup: - - setCache -``` - - -Nastavením proměnné -=================== - -Závislosti jsou předávány zapsáním přímo do členské proměnné: - -```php -class MyClass -{ - public Cache $cache; -} - -$obj = new MyClass; -$obj->cache = $cache; -``` - -Tento způsob se považuje za nevhodný, protože členská proměnná musí být deklarována jako `public`. A tudíž nemáme kontrolu nad tím, že předaná závislost bude skutečně daného typu (platilo před PHP 7.4) a přicházíme o možnost reagovat na nově přiřazenou závislost vlastním kódem, například zabránit následné změně. Zároveň se proměnná stává součástí veřejného rozhraní třídy, což nemusí být žádoucí. - -Nastavení proměnné definujeme v konfiguraci DI kontejneru v [sekci setup |services#Setup]: - -```neon -services: - - create: MyClass - setup: - - $cache = @\Cache -``` - - -Inject -====== - -Zatímco předchozí tři způsoby platí obecně ve všech objektově orientovaných jazycích, injektování metodami `inject*()` nebo atributem `#[Inject]` se typicky používá u presenterů v Nette, kde je zapnuté automaticky; u libovolné jiné služby ho lze zapnout pomocí [`inject: true` |services#Režim Inject]. Pojednává o nich [samostatná kapitola |best-practices:inject-method-attribute]. - - -Jaký způsob zvolit? -=================== - -- konstruktor je vhodný pro povinné závislosti, které třída nezbytně potřebuje ke své funkci -- setter je naopak vhodný pro nepovinné závislosti, nebo závislosti, u kterých chceme mít možnost je dále měnit -- veřejné proměnné vhodné nejsou - -{{maintitle: Předávání závislostí v PHP: konstruktor, setter a proměnná}} -{{description: Jak předávat závislosti do třídy: konstruktorem, setterem, nastavením proměnné nebo metodou inject. Čím se způsoby liší a který zvolit.}} diff --git a/dependency-injection/cs/services.texy b/dependency-injection/cs/services.texy deleted file mode 100644 index e7695babdb..0000000000 --- a/dependency-injection/cs/services.texy +++ /dev/null @@ -1,483 +0,0 @@ -Definování služeb -***************** - -.[perex] -Konfigurace je místem, kde učíme DI kontejner, jak má sestavovat jednotlivé služby a jak je propojovat s dalšími závislostmi. Nette poskytuje velice přehledný a elegantní způsob, jak toho dosáhnout. - -Sekce `services` v konfiguračním souboru formátu NEON je místem, kde definujeme vlastní služby a jejich konfigurace. Podívejme se na jednoduchý příklad definice služby pojmenované `database`, která reprezentuje instanci třídy `PDO`: - -```neon -services: - database: PDO('sqlite::memory:') -``` - -Uvedená konfigurace vyústí v následující tovární metodu v [DI kontejneru|container]: - -```php -public function createServiceDatabase(): PDO -{ - return new PDO('sqlite::memory:'); -} -``` - -Názvy služeb nám umožňují odkazovat se na ně v dalších částech konfiguračního souboru, a to ve formátu `@nazevSluzby`. Pokud není potřeba službu pojmenovávat, můžeme jednoduše použít pouze odrážku: - -```neon -services: - - PDO('sqlite::memory:') -``` - -Pro získání služby z DI kontejneru můžeme využít metodu `getService()` s názvem služby jako parametrem, nebo metodu `getByType()` s typem služby: - -```php -$database = $container->getService('database'); -$database = $container->getByType(PDO::class); -``` - - -Vytvoření služby -================ - -Většinou vytváříme službu jednoduše tím, že vytvoříme instanci určité třídy. Například: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Pokud potřebujeme konfiguraci rozšířit o další klíče, lze definici rozepsat do více řádků: - -```neon -services: - database: - create: PDO('sqlite::memory:') - setup: ... -``` - -Klíč `create` má alias `factory`, obě varianty jsou v praxi běžné. Nicméně doporučujeme používat `create`. - -Argumenty konstruktoru nebo vytvářecí metody mohou být alternativně zapsány v klíči `arguments`: - -```neon -services: - database: - create: PDO - arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] -``` - -Služby nevznikají jen prostým vytvořením instance třídy, mohou být také výsledkem volání statických metod nebo metod jiných služeb: - -```neon -services: - database: DatabaseFactory::create() - router: @routerFactory::create() -``` - -Všimněte si, že pro jednoduchost se místo `->` používá `::`, viz [#výrazové prostředky]. Vygenerují se tyto tovární metody: - -```php -public function createServiceDatabase(): PDO -{ - return DatabaseFactory::create(); -} - -public function createServiceRouter(): RouteList -{ - return $this->getService('routerFactory')->create(); -} -``` - -DI kontejner potřebuje znát typ vytvořené služby. Pokud vytváříme službu pomocí metody, která nemá uvedený návratový typ, musíme tento typ explicitně uvést v konfiguraci: - -```neon -services: - database: - create: DatabaseFactory::create() - type: PDO -``` - - -Argumenty -========= - -Do konstruktoru a metod předáváme argumenty způsobem velmi podobným jako v samotném PHP: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Pro lepší čitelnost můžeme argumenty rozepsat do samostatných řádků. V takovém případě je používání čárek volitelné: - -```neon -services: - database: PDO( - 'mysql:host=127.0.0.1;dbname=test' - root - secret - ) -``` - -Argumenty můžete také pojmenovat a nemusíte se pak starat o jejich pořadí: - -```neon -services: - database: PDO( - username: root - password: secret - dsn: 'mysql:host=127.0.0.1;dbname=test' - ) -``` - -Pokud chcete některé argumenty vynechat a použít jejich výchozí hodnotu nebo dosadit službu pomocí [autowiringu|autowiring], použijte podtržítko: - -```neon -services: - foo: Foo(_, %appDir%) -``` - -Jako argumenty lze předávat služby, používat parametry a mnohem více, viz [#výrazové prostředky]. - - -Setup -===== - -V sekci `setup` definujeme metody, které se mají volat při vytváření služby. - -```neon -services: - database: - create: PDO(%dsn%, %user%, %password%) - setup: - - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) -``` - -To by v PHP vypadalo takto: - -```php -public function createServiceDatabase(): PDO -{ - $service = new PDO('...', '...', '...'); - $service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - return $service; -} -``` - -Kromě volání metod lze také předávat hodnoty do properties. Podporováno je i přidání prvku do pole, které je potřeba zapsat v uvozovkách, aby nekolidovalo se syntaxí NEON: - -```neon -services: - foo: - create: Foo - setup: - - $value = 123 - - '$onClick[]' = [@bar, clickHandler] -``` - -Což by v PHP kódu vypadalo následovně: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - $service->value = 123; - $service->onClick[] = [$this->getService('bar'), 'clickHandler']; - return $service; -} -``` - -V setupu lze však volat i statické metody nebo metody jiných služeb. Pokud potřebujete předat jako argument aktuální službu, uveďte ji jako `@self`: - -```neon -services: - foo: - create: Foo - setup: - - My\Helpers::initializeFoo(@self) - - @anotherService::setFoo(@self) -``` - -Všimněte si, že pro jednoduchost se místo `->` používá `::`, viz [#výrazové prostředky]. Vygeneruje se taková tovární metoda: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - My\Helpers::initializeFoo($service); - $this->getService('anotherService')->setFoo($service); - return $service; -} -``` - - -Výrazové prostředky -=================== - -Nette DI nám dává mimořádně bohaté výrazové prostředky, pomocí kterých můžeme zapsat téměř cokoliv. V konfiguračních souborech tak můžeme využívat [parametry |configuration#Parametry]: - -```neon -# parametr -%wwwDir% - -# hodnota parametru pod klíčem -%mailer.user% - -# parametr uvnitř řetězce -'%wwwDir%/images' -``` - -Dále vytvářet objekty, volat metody a funkce: - -```neon -# vytvoření objektu -DateTime() - -# volání statické metody -Collator::create(%locale%) - -# volání PHP funkce -::getenv(DB_USER) -``` - -Odkazovat se na služby buď jejich jménem, nebo pomocí typu: - -```neon -# služba dle názvu -@database - -# služba dle typu -@Nette\Database\Connection -``` - -Používat first-class callable syntax: .{data-version:3.2.0} - -```neon -# vytvoření callbacku, obdoba [@user, logout] -@user::logout(...) -``` - -Používat konstanty: - -```neon -# konstanta třídy -FilesystemIterator::SKIP_DOTS - -# globální konstantu získáme PHP funkcí constant() -::constant(\PHP_VERSION) -``` - -Přistupovat k veřejným vlastnostem a konstantám služby přes `@service::member`. Zda se název vyhodnotí jako vlastnost, nebo jako konstanta, se rozlišuje podle prvního písmene - malé počáteční písmeno znamená veřejnou vlastnost, velké konstantu: - -```neon -# veřejná vlastnost služby (začíná malým písmenem) -@settings::apiUrl - -# konstanta třídy služby (začíná velkým písmenem) -@settings::Version -``` - -Volání metod lze řetězit stejně jako v PHP. Jen pro jednoduchost se místo `->` používá `::`: - -```neon -DateTime()::format('Y-m-d') -# PHP: (new DateTime())->format('Y-m-d') - -@http.request::getUrl()::getHost() -# PHP: $this->getService('http.request')->getUrl()->getHost() -``` - -Tyto výrazy můžete používat kdekoliv, při [vytváření služeb |#Vytvoření služby], v [argumentech |#Argumenty], v sekci [#setup] nebo [parametrech |configuration#Parametry]: - -```neon -parameters: - ipAddress: @http.request::getRemoteAddress() - -services: - database: - create: DatabaseFactory::create( @anotherService::getDsn() ) - setup: - - initialize( ::getenv('DB_USER') ) -``` - - -Speciální funkce ----------------- - -V konfiguračních souborech můžete používat tyto speciální funkce: - -- `not()` negace hodnoty -- `bool()`, `int()`, `float()`, `string()` bezeztrátové přetypování na daný typ .{data-version:3.0.5} -- `typed()` vytvoří pole všech služeb daného typu -- `tagged()` vytvoření pole všech služeb s daným tagem - -```neon -services: - - Foo( - id: int(::getenv('ProjectId')) - productionMode: not(%debugMode%) - ) -``` - -Oproti klasickému přetypování v PHP, jako je např. `(int)`, bezeztrátové přetypování vyhodí výjimku pro nečíselné hodnoty. - -Funkce `typed()` vytvoří pole všech služeb daného typu (třída nebo rozhraní). Vynechá služby, které mají vypnutý autowiring. Lze uvést i více typů oddělených čárkou. - -```neon -services: - - BarsDependent( typed(Bar) ) -``` - -Pole služeb určitého typu můžete předávat jako argument také automaticky pomocí [autowiringu |autowiring#Pole služeb]. - -Funkce `tagged()` pak vytváří pole všech služeb s určitým tagem. I zde můžete uvést více tagů oddělených čárkou. - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - - -Autowiring -========== - -Klíč `autowired` umožňuje ovlivnit chování autowiringu pro konkrétní službu. Pro detaily viz [kapitola o autowiringu|autowiring]. - -```neon -services: - foo: - create: Foo - autowired: false # služba foo je vyřazena z autowiringu -``` - - -Lazy služby .{data-version:3.2.4} -================================= - -Lazy loading je technika, která odkládá vytvoření služby až do chvíle, kdy je skutečně potřeba. V globální konfiguraci lze [povolit lazy vytváření |configuration#Lazy služby] pro všechny služby najednou. Pro jednotlivé služby pak můžete toto chování přepsat: - -```neon -services: - foo: - create: Foo - lazy: false -``` - -Když je služba definovaná jako lazy, při jejím vyžádání z DI kontejneru dostaneme speciální zástupný objekt. Ten vypadá a chová se stejně jako skutečná služba, ale skutečná inicializace (volání konstruktoru a setupu) proběhne až při prvním volání jakékoliv její metody nebo property. - -Počítejte s tím, že když služba vzniká později, projeví se později i chyby v její konfiguraci. Například chybné přihlašovací údaje k databázi se neohlásí při startu aplikace, ale teprve při prvním dotazu. - -Lazy vytváření také zmírňuje problém cyklických závislostí, tedy situace, kdy služba A vyžaduje službu B a B zároveň vyžaduje A. Bez něj kontejner ohlásí chybu `Circular reference detected`. S lazy proxy dostane služba A pouze zástupný objekt služby B, který se inicializuje až při skutečném použití, tedy ve chvíli, kdy A už existuje. Přesto cyklická závislost signalizuje chybný návrh a je lepší se jí zbavit. - -.[note] -Lazy loading vyžaduje PHP 8.4 nebo novější a funguje pouze pro služby vytvářené přímou instanciací třídy (např. `create: Foo`), nikoliv pro služby vytvářené tovární metodou. Nelze jej použít ani pro třídy, které v základu dědí z interní PHP třídy. Když lazy loading nelze uplatnit, příznak `lazy: true` se tiše ignoruje. - - -Tagy -==== - -Tagy slouží k přidání doplňujících informací ke službám. Službě můžete přidat jeden nebo více tagů: - -```neon -services: - foo: - create: Foo - tags: - - cached -``` - -Tagy mohou také nést hodnoty: - -```neon -services: - foo: - create: Foo - tags: - logger: monolog.logger.event -``` - -Abyste získali všechny služby s určitými tagy, můžete použít funkci `tagged()`: - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - -V DI kontejneru můžete získat názvy všech služeb s určitým tagem pomocí metody `findByTag()`: - -```php -$names = $container->findByTag('logger'); -// $names je pole s názvy služeb jako klíči a hodnotami tagů jako hodnotami -// např. ['foo' => 'monolog.logger.event', ...] -``` - - -Režim Inject -============ - -Pomocí příznaku `inject: true` se aktivuje předávání závislostí přes veřejné proměnné s atributem [Inject |best-practices:inject-method-attribute#Atributy Inject] a metody [inject*() |best-practices:inject-method-attribute#Metody inject]. - -```neon -services: - articles: - create: App\Model\Articles - inject: true -``` - -Ve výchozím nastavení je `inject` aktivováno pouze pro presentery. - - -Modifikace služeb -================= - -DI kontejner obsahuje mnoho služeb, které byly přidány prostřednictvím vestavěného nebo [uživatelského rozšíření|extensions]. Můžete upravit definice těchto služeb přímo v konfiguraci. Například můžete změnit třídu služby `application.application`, což je standardně `Nette\Application\Application`, na jinou: - -```neon -services: - application.application: - create: MyApplication - alteration: true -``` - -Příznak `alteration` říká, že jen modifikujeme existující službu. Zároveň slouží jako pojistka: pokud upravovaná služba neexistuje, kompilace skončí chybou. - -Můžeme také doplnit setup: - -```neon -services: - application.application: - create: MyApplication - alteration: true - setup: - - '$onStartup[]' = [@resource, init] -``` - -Službu nemusíte identifikovat interním názvem, můžete na ni odkázat i jejím typem. Předchozí příklad tak lze zapsat i takto: - -```neon -services: - @Nette\Application\Application: - create: MyApplication -``` - -Při přepisování služby můžeme chtít odstranit původní argumenty, položky setup nebo tagy, k čemuž slouží `reset`: - -```neon -services: - application.application: - create: MyApplication - alteration: true - reset: - arguments: true - setup: true - tags: true -``` - -Pokud chcete odstranit službu přidanou rozšířením, můžete to udělat takto: - -```neon -services: - cache.journal: false -``` - -{{maintitle: Definice služeb v Nette DI: konfigurace v NEON}} -{{description: Naučte DI kontejner, jak vytvářet služby a propojovat je se závislostmi: argumenty, setup, výrazové prostředky, lazy služby, tagy a režim inject.}} diff --git a/dependency-injection/cs/upgrading.texy b/dependency-injection/cs/upgrading.texy deleted file mode 100644 index 20996307d8..0000000000 --- a/dependency-injection/cs/upgrading.texy +++ /dev/null @@ -1,52 +0,0 @@ -Upgrade -******* - - -Upgrade na verzi 3.1 -==================== - -- autowiring již nepředává `null` do nullable parametru bez výchozí hodnoty; argument uveďte explicitně, nebo parametru doplňte výchozí hodnotu -- skončila podpora anotace `@return`; použijte návratový typ, nebo typ uveďte v definici služby pomocí `type:` -- klíč `dynamic` byl přejmenován na `imported` a `class` na `type` -- symbol pro vynechaný argument se změnil z `...` na `_`, např. `MyService(_, 123)` -- v souborech NEON už není potřeba na začátku řetězce escapovat znak `@` -- klíč `parameters` uvnitř definic generovaných továren je zastaralý -- metoda `Nette\DI\Config\Loader::save()` je zastaralá; konfiguraci exportujte pomocí `Nette\DI\Config\Adapters\NeonAdapter::dump()` - -Verze 3.1 je přechodová: nepřináší novinky, ale pomocí notices upozorňuje na vše, co bude později fungovat jinak. Viz článek [Nette DI 3.1: přechodová verze |https://blog.nette.org/cs/nette-di-3-1-prechodova-verze]. - - -Upgrade na verzi 3.0 -==================== - -- byla odstraněna podpora INI souborů -- byl odstraněn přímý zápis PHP kódu do konfigurace pomocí otazníků (např. `"$service->onError[] = ?"(...)`); použijte místo něj syntaxi s polem `'$onError[]' = [...]` -- v konfiguračních souborech místo `class: PDO(...)` použijte `factory: PDO(...)` -- tag `nette.presenter` se pro presentery už nepoužívá - - -Pro tvůrce Compiler Extensions ------------------------------- - -Zatímco v Nette 2.4 interně popisoval každou službu objekt `Nette\DI\ServiceDefinition`, dnes existuje definic vícero: `Nette\DI\Definitions\ImportedDefinition` pro importované (dynamické) služby, `Nette\DI\Definitions\FactoryDefinition` pro generované továrničky na základě rozhraní, `Nette\DI\Definitions\AccessorDefinition` pro generované accessory a `Nette\DI\Definitions\ServiceDefinition` pro běžné služby. - -Kromě `ContainerBuilder::addDefinition()` proto existuje pro vytvoření nové definice několik dalších metod: `addFactoryDefinition()`, `addAccessorDefinition()` a `addImportedDefinition()`. - - -Upgrade na verzi 2.4 -==================== - -- sekce (např. production, development) v jednom konfiguračním souboru jsou zastaralé; použijte dvojici souborů `config.neon` a `config.local.neon` -- dědičnost definic služeb je zastaralá -- `Statement::setEntity()` je zastaralá - - -Upgrade na verzi 2.3 -==================== - -- byla odstraněna podpora pro umisťování služeb do sekce extension v konfiguračním souboru -- byla odstraněna podpora dynamicky přidávaných rozšíření -- při dynamické výměně služby (přes `removeService()`, `addService()`) musí být nová služba instancí stejného rozhraní/třídy jako původní - -{{maintitle: Upgrade Nette DI: změny v jednotlivých verzích}} -{{description: Co se změnilo v Nette DI 3.1, 3.0, 2.4 a 2.3 a jak upravit kód a konfiguraci v NEON: autowiring parametrů s null, přejmenované klíče a další.}} diff --git a/dependency-injection/de/@home.texy b/dependency-injection/de/@home.texy deleted file mode 100644 index ed9eb1555d..0000000000 --- a/dependency-injection/de/@home.texy +++ /dev/null @@ -1,22 +0,0 @@ -Nette DI -******** - -.[perex] -Dependency Injection ist ein Entwurfsmuster, das Ihre Sichtweise auf Code und Entwicklung grundlegend verändern wird. Es öffnet Ihnen den Weg in die Welt sauber entworfener und wartbarer Anwendungen. - -- [Was ist Dependency Injection? |introduction] -- [Globaler Zustand & Singletons |global-state] -- [Abhängigkeiten übergeben |passing-dependencies] -- [Was ist ein DI-Container? |container] -- [Häufig gestellte Fragen |faq] - - -Das Paket `nette/di` bietet einen äußerst fortschrittlichen kompilierten DI-Container für PHP. - -- [Nette DI Container |nette-container] -- [Konfiguration |configuration] -- [Service-Definitionen |services] -- [Autowiring |autowiring] -- [Generierte Factories |factory] -- [Extensions für Nette DI erstellen |extensions] -- [Kompilierung im Detail |compilation-internals] diff --git a/dependency-injection/de/@left-menu.texy b/dependency-injection/de/@left-menu.texy deleted file mode 100644 index 0e123e618a..0000000000 --- a/dependency-injection/de/@left-menu.texy +++ /dev/null @@ -1,28 +0,0 @@ -Dependency Injection -******************** -- [Was ist DI? |introduction] -- [Globaler Zustand & Singletons |global-state] -- [Abhängigkeiten übergeben |passing-dependencies] -- [Was ist ein DI-Container? |container] -- [Häufig gestellte Fragen |faq] - - -Nette DI --------- -- [Nette DI Container |nette-container] -- [Konfiguration |configuration] -- [Service-Definitionen |services] -- [Autowiring |autowiring] -- [Generierte Factories |factory] -- [Extensions für Nette DI erstellen |extensions] -- [Kompilierung im Detail |compilation-internals] -- [Upgrade|upgrading] - - -Weiterführende Lektüre -********************** -- [Nette Dokumentation |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Best Practices |best-practices:] -- [Fehlerbehebung |nette:troubleshooting] diff --git a/dependency-injection/de/@meta.texy b/dependency-injection/de/@meta.texy deleted file mode 100644 index b3b806b2ca..0000000000 --- a/dependency-injection/de/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokumentation}} diff --git a/dependency-injection/de/autowiring.texy b/dependency-injection/de/autowiring.texy deleted file mode 100644 index 78bcf520d8..0000000000 --- a/dependency-injection/de/autowiring.texy +++ /dev/null @@ -1,279 +0,0 @@ -Autowiring -********** - -.[perex] -Autowiring ist eine großartige Fähigkeit, die dem Konstruktor und anderen Methoden automatisch die benötigten Services übergibt, sodass wir sie nicht ausdrücklich angeben müssen. Das spart Ihnen viel Zeit. - -Dadurch können wir beim Schreiben von Service-Definitionen die allermeisten Argumente weglassen. Statt: - -```neon -services: - articles: Model\ArticleRepository(@database, @cache.storage) -``` - -schreiben Sie einfach: - -```neon -services: - articles: Model\ArticleRepository -``` - -Das Autowiring richtet sich nach Typen, damit es funktioniert, muss die Klasse `ArticleRepository` also ungefähr so definiert sein: - -```php -namespace Model; - -class ArticleRepository -{ - public function __construct(\PDO $db, \Nette\Caching\Storage $storage) - {} -} -``` - -Autowiring verwendet niemals die Namen von Services. Es richtet sich einzig nach dem Typsystem von PHP, weiß also auch, dass eine Klasse die Interfaces erfüllt, die sie implementiert, und die Klassen, von denen sie erbt. Dadurch ist der Name eines Services bloß ein Hilfsbezeichner, und ihn umzubenennen zerstört in der Anwendung nichts. - -Damit sich Autowiring nutzen lässt, muss es im Container von jedem Typ **genau einen Service** geben. Gäbe es mehrere, wüsste das Autowiring nicht, welchen es übergeben soll, und würde eine Exception werfen: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - tempDb: PDO('sqlite::memory:') - articles: Model\ArticleRepository # WIRFT EINE EXCEPTION, sowohl mainDb als auch tempDb passen -``` - -Eine Lösung ist, das Autowiring zu umgehen und den Namen des Services ausdrücklich anzugeben (etwa `articles: Model\ArticleRepository(@mainDb)`). Bequemer ist es jedoch, das Autowiring für einen der Services [abzuschalten |#Autowiring abschalten] oder einen Service gegenüber den anderen zu [bevorzugen |#Bevorzugung beim Autowiring]. - - -Autowiring abschalten ---------------------- - -Über die Option `autowired: false` können wir das Autowiring für einen Service abschalten: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - - tempDb: - create: PDO('sqlite::memory:') - autowired: false # der Service tempDb ist vom Autowiring ausgenommen - - articles: Model\ArticleRepository # dem Konstruktor wird daher mainDb übergeben -``` - -Der Service `articles` wirft keine Exception darüber, dass für den Konstruktor zwei passende `PDO`-Services (`mainDb` und `tempDb`) zur Verfügung stehen, denn er zieht nur den Service `mainDb` in Betracht. - -Autowiring lässt sich über die Konfigurationsoption [`di › excluded` |configuration#DI] auch global für ganze Typen abschalten; dort werden die Typen (und ihre Nachfahren) aufgeführt, die nie autowiret werden sollen. - -.[note] -Die Konfiguration des Autowirings unterscheidet sich in Nette von der in Symfony. In Symfony bedeutet `autowire: false`, dass für die Konstruktorargumente des Services kein Autowiring verwendet werden soll. In Nette betrifft das Autowiring die Konstruktorargumente und alle weiteren Methoden, die über den Container aufgerufen werden (etwa Setter Injection). Die Option `autowired: false` verhindert, dass der Container diese Instanz des Services automatisch als Abhängigkeit an andere Services übergibt. - - -Bevorzugung beim Autowiring ---------------------------- - -Haben wir mehrere Services desselben Typs und geben für einen von ihnen die Option `autowired` an, wird dieser Service zum bevorzugten: - -```neon -services: - mainDb: - create: PDO(%dsn%, %user%, %password%) - autowired: PDO # wird zum bevorzugten - - tempDb: - create: PDO('sqlite::memory:') - - articles: Model\ArticleRepository -``` - -Der Service `articles` wirft keine Exception darüber, dass mehrere `PDO`-Services passen (`mainDb` und `tempDb`), sondern verwendet den bevorzugten, also `mainDb`. - - -Sammlung von Services ---------------------- - -Autowiring kann auch Arrays von Services eines bestimmten Typs übergeben. Weil PHP von Haus aus nicht erlaubt, den Typ der Array-Elemente in einer Typdeklaration anzugeben, müssen Sie die Typdeklaration `array` um einen phpDoc-Kommentar mit dem Typ der Elemente ergänzen, etwa `ClassName[]`: - -```php -namespace Model; - -class ShipManager -{ - /** - * @param Shipper[] $shippers - */ - public function __construct(array $shippers) - {} -} -``` - -Der DI-Container übergibt dann automatisch ein Array der Services, die dem angegebenen Typ entsprechen. Services mit [abgeschaltetem Autowiring |#Autowiring abschalten] lässt er aus, und den gerade erzeugten Service nimmt er nie in dessen eigene Sammlung auf. Anders als beim Übergeben eines einzelnen Services haben das [Einschränken |#Autowiring einschränken] des Autowirings auf einen bestimmten Typ und das Kennzeichnen eines Services als [bevorzugt |#Bevorzugung beim Autowiring] hier keine Wirkung - das Array enthält immer alle Services des angegebenen Typs. - -Der Typ im Kommentar kann auch die Form `array<int, Class>` oder `list<Class>` haben. Wenn Sie die Form des phpDoc-Kommentars nicht beeinflussen können, lässt sich ein Array von Services über [`typed()` |services#Spezielle Funktionen] direkt in der Konfiguration übergeben. - - -Skalare Argumente ------------------ - -Autowiring funktioniert nur für Objekte und Arrays von Objekten. Skalare Argumente (etwa Strings, Zahlen, boolesche Werte) müssen [in der Konfiguration angegeben |services#Argumente] werden. Eine Alternative ist, ein [Objekt mit Einstellungen|best-practices:passing-settings-to-presenters] zu erzeugen, das den skalaren Wert (oder mehrere Werte) kapselt. Dieses Objekt lässt sich dann über Autowiring übergeben. - -```php -class MySettings -{ - public function __construct( - // readonly lässt sich seit PHP 8.1 verwenden - public readonly bool $value, - ) - {} -} -``` - -Als Service registrieren Sie es, indem Sie es der Konfiguration hinzufügen: - -```neon -services: - - MySettings('any value') -``` - -Andere Klassen können es dann über Autowiring anfordern. - - -Optionale Abhängigkeiten ------------------------- - -Hat ein Parameter des Konstruktors oder einer Methode einen Standardwert und existiert im Container kein Service des verlangten Typs, wirft das Autowiring keine Exception - es überspringt das Argument einfach, sodass der Standardwert verwendet wird. So deklarieren Sie optionale Abhängigkeiten: - -```php -class Foo -{ - public function __construct( - private ?Logger $logger = null, - ) {} -} -``` - -Bei einem Parameter ohne Standardwert führt ein fehlender Service dagegen immer zu einer Exception. - - -Autowiring einschränken ------------------------ - -Für einzelne Services lässt sich das Autowiring auf bestimmte Klassen oder Interfaces einschränken. - -Normalerweise übergibt das Autowiring einen Service an jeden Methodenparameter, dessen Typ zum Service passt. Einschränken bedeutet, dass wir Bedingungen aufstellen, die die für die Methodenparameter angegebenen Typen erfüllen müssen, damit ihnen der Service übergeben wird. - -Nehmen wir ein Beispiel: - -```php -class ParentClass -{} - -class ChildClass extends ParentClass -{} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Würden wir sie alle als Services registrieren, scheiterte das Autowiring: - -```neon -services: - parent: ParentClass - child: ChildClass - parentDep: ParentDependent # WIRFT EINE EXCEPTION, sowohl parent als auch child passen - childDep: ChildDependent # das Autowiring übergibt dem Konstruktor den Service child -``` - -Der Service `parentDep` wirft die Exception `Multiple services of type ParentClass found: child, parent`, weil sowohl der Service `parent` als auch `child` in seinen Konstruktor passen und das Autowiring sich nicht entscheiden kann, welchen es wählen soll. - -Für den Service `child` können wir das Autowiring deshalb auf den Typ `ChildClass` einschränken: - -```neon -services: - parent: ParentClass - child: - create: ChildClass - autowired: ChildClass # lässt sich auch als 'autowired: self' schreiben - - parentDep: ParentDependent # das Autowiring übergibt dem Konstruktor den Service parent - childDep: ChildDependent # das Autowiring übergibt dem Konstruktor den Service child -``` - -Jetzt wird dem Konstruktor des Services `parentDep` der Service `parent` übergeben, denn er ist nun das einzige passende Objekt. Der Service `child` wird vom Autowiring nicht mehr dorthin übergeben. Ja, der Service `child` ist weiterhin vom Typ `ParentClass`, aber die Einschränkung `autowired: ChildClass` bewirkt, dass er nur an Parameter übergeben wird, die ausdrücklich als `ChildClass` (oder als deren Untertypen) deklariert sind. Weil `ParentDependent` ein `ParentClass` verlangt, kommt der Service `child` dort nicht mehr als Kandidat für das Autowiring in Betracht. - -Für den Service `child` ließe sich `autowired: ChildClass` auch als `autowired: self` schreiben, denn `self` ist ein Platzhalter für die Klasse des aktuellen Services. - -Im Schlüssel `autowired` lassen sich auch mehrere Klassen oder Interfaces als Array angeben: - -```neon -autowired: [ParentClass, FooInterface] -``` - -Versuchen wir, dem Beispiel Interfaces hinzuzufügen: - -```php -interface FooInterface -{} - -interface BarInterface -{} - -class ParentClass implements FooInterface -{} - -class ChildClass extends ParentClass implements BarInterface -{} - -class FooDependent -{ - function __construct(FooInterface $obj) - {} -} - -class BarDependent -{ - function __construct(BarInterface $obj) - {} -} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Schränken wir den Service `child` in keiner Weise ein, passt er in die Konstruktoren aller Klassen `FooDependent`, `BarDependent`, `ParentDependent` und `ChildDependent`, und das Autowiring übergibt ihn dorthin. - -Schränken wir sein Autowiring jedoch mit `autowired: ChildClass` (oder `self`) auf `ChildClass` ein, übergibt es ihn nur dem Konstruktor von `ChildDependent`, weil dieser ein Argument vom Typ `ChildClass` verlangt und gilt, dass `ChildClass` *vom Typ* `ChildClass` ist. Bei keinem der anderen Parameter ist der verlangte Typ `ChildClass` oder ein Untertyp davon, der Service wird ihnen also nicht übergeben. - -Schränken wir ihn mit `autowired: ParentClass` auf `ParentClass` ein, übergibt ihn das Autowiring wieder dem Konstruktor von `ChildDependent` (weil die verlangte `ChildClass` ein Untertyp von `ParentClass` ist) und jetzt auch dem Konstruktor von `ParentDependent`, denn der verlangte Typ `ParentClass` passt ebenfalls. - -Schränken wir ihn auf `FooInterface` ein, wird er weiterhin in `ParentDependent` (die verlangte `ParentClass` ist ein Untertyp von `FooInterface`) und in `ChildDependent` autowiret, zusätzlich auch in den Konstruktor von `FooDependent`, nicht aber in `BarDependent`, weil `BarInterface` kein Untertyp von `FooInterface` ist. - -```neon -services: - child: - create: ChildClass - autowired: FooInterface - - fooDep: FooDependent # das Autowiring übergibt dem Konstruktor den Service child - barDep: BarDependent # WIRFT EINE EXCEPTION, kein Service passt - parentDep: ParentDependent # das Autowiring übergibt dem Konstruktor den Service child - childDep: ChildDependent # das Autowiring übergibt dem Konstruktor den Service child -``` diff --git a/dependency-injection/de/compilation-internals.texy b/dependency-injection/de/compilation-internals.texy deleted file mode 100644 index e4f02521ed..0000000000 --- a/dependency-injection/de/compilation-internals.texy +++ /dev/null @@ -1,222 +0,0 @@ -Kompilierung im Detail -********************** - -.[perex] -Diese Seite öffnet die Kompilierung des Containers: die Phasen, die sie durchläuft, wann die Parameter der Konfiguration aufgelöst werden, wann aus `@service`-Strings echte Referenzen werden und - die Frage, die Autoren von Extensions am häufigsten stellen - in welcher Phase Sie gefahrlos nach Services nach Typ suchen können. Sie ist die tiefergehende Ergänzung zu [Extensions erstellen |extensions]. - -Für eine gewöhnliche Anwendung, ja selbst für eine gewöhnliche Extension brauchen Sie nichts davon. Sobald Ihre Extension aber anfängt, den Graphen der Services zu untersuchen oder umzuformen, wird das Timing entscheidend: Derselbe Aufruf von `getByType()` liefert in der einen Phase eine verlässliche Antwort und in der anderen eine irreführende. Diese Seite erklärt, warum, damit Sie immer wissen, wohin Ihr Code gehört. - - -Zwei Welten: Kompilierung vs. Laufzeit -====================================== - -Am wichtigsten ist zu verstehen, dass ein Nette-Container **nicht bei jedem Request zusammengebaut wird**. Er wird einmal zu einer optimierten PHP-Klasse gebaut, diese Klasse wird auf der Festplatte abgelegt, und jeder weitere Request bindet die fertige Datei bloß per `include` ein. Die gesamte unten beschriebene Maschinerie - Extensions, Resolver, der Code-Generator - läuft **nur während der (Neu-)Kompilierung**. - -Das teilt die Welt in zwei Darstellungen, die nie gleichzeitig existieren: - -| | während der Kompilierung | zur Laufzeit -|---|---|--- -| Was existiert | **Definitionen** (Rezepte) im `ContainerBuilder` | **Instanzen** der Services im `Container` -| Zentrale Klassen | `Compiler`, `ContainerBuilder`, `Resolver`, `PhpGenerator` | `Container` (Elternklasse der erzeugten Klasse) -| `%param%`, `@service` | textuelle Markierungen, die noch übersetzt werden | bereits übersetzt / in den Code eingebacken - -Die erzeugte Klasse erweitert `Nette\DI\Container` und hat für jeden Service eine Methode `createServiceXxx()`. Ihre Parameter und die Metadaten für das Autowiring sind vorberechnet, zur Laufzeit bleibt also nichts mehr aufzulösen - nur noch, Services bei Bedarf zu instanziieren. - -.[note] -Im Entwicklermodus wird der Container automatisch neu gebaut, sobald sich eine Konfigurationsdatei oder die Klasse einer Extension ändert; beides wird als Abhängigkeit verfolgt. In der Produktion wird er einmal kompiliert und nie wieder geprüft, daher kommt die Geschwindigkeit. - - -Die Phasen auf einen Blick -========================== - -Die Kompilierung steuert `Compiler::compile()`, und sie läuft auf drei Schritte hinaus: - -```php -public function compile(): string -{ - $this->processExtensions(); // PHASE A: Schemas + loadConfiguration() - $this->processBeforeCompile(); // PHASE B: resolve + beforeCompile() + complete - return $this->generateCode(); // PHASE C: Code-Erzeugung + afterCompile() -} -``` - -Das ganze Denkmodell passt in eine einzige Idee - **jede Phase weiß mehr als die vorherige:** - -- **Phase A** füllt den Graphen mit Definitionen. Die Typen der Services **sind noch nicht verlässlich bekannt**, denn ein Typ kann aus dem Rückgabewert einer Factory stammen, den sich noch niemand angesehen hat. -- **Phase B** löst zuerst alle Typen auf (`resolve`), lässt dann die Extensions den Graphen umformen (`beforeCompile`) und [autowiret |autowiring] zum Schluss die Argumente (`complete`). -- **Phase C** verwandelt den fertigen Graphen in PHP und lässt die Extensions den erzeugten Code anfassen. - -Genau dieses wachsende Wissen ist der Grund, warum dieselbe Operation in der einen Phase sicher und in der anderen unzuverlässig ist. Der Rest dieser Seite geht die Phasen mit dieser Idee im Hinterkopf durch. - - -Phase A: Registrieren der Definitionen -====================================== - -In dieser Phase ruft Nette auf jeder Extension drei Methoden auf - `getConfigSchema()`, dann `setConfig()`, dann `loadConfiguration()` -, und zwar in einer **sorgfältig gesteuerten Reihenfolge**, denn hier zählt die Reihenfolge wirklich. - - -Warum die Reihenfolge zählt ---------------------------- - -- **`ParametersExtension` und `ExtensionsExtension` kommen zuerst.** Die erste muss vor allem anderen laufen, damit sie `%param%` in der gesamten Konfiguration auflösen kann - jede andere Extension bekommt ihren Abschnitt dann bereits mit eingesetzten Werten. Die zweite registriert weitere Extensions, die im Abschnitt `extensions:` aufgeführt sind, muss also ebenfalls existieren, bevor die übrigen verarbeitet werden. -- **`ServicesExtension` kommt zuletzt.** Der Abschnitt `services:` des Nutzers hat deshalb immer das letzte Wort und kann alles überschreiben, was die Extensions eingerichtet haben. -- **`InjectExtension` wird ganz ans Ende verschoben**, damit ihre Arbeit die Setups sieht, die alle anderen Extensions ergänzt haben. - -Was das für Sie bedeutet: Wenn `loadConfiguration()` Ihrer Extension läuft, sind die Parameter bereits aufgelöst, die Services des Nutzers aber noch nicht da. Diese einzige Tatsache erklärt die meisten der folgenden Timing-Regeln. - - -Von services: zu Definitionen ------------------------------ - -Der Abschnitt `services:` des Nutzers wird hier, im letzten Schritt von Phase A, in [Objekte von Definitionen |extensions#Typen von Definitionen] verwandelt. Jeder NEON-Eintrag wird normalisiert (Kurzschreibweisen werden vereinheitlicht), seine Art wird erkannt (gewöhnlicher Service, Factory, Accessor, ...) und im Builder eine passende Definition angelegt. Das ist auch der erste Moment, in dem einfache Argumente `@name` / `@Type` zu Referenzen werden - siehe [unten |#Referenzen: wann aus @service eine Referenz wird]. - -Am Ende von Phase A liegen alle Definitionen vor - jede Extension und der Nutzer haben registriert, was sie wollten -, aber das Bild ist noch nicht scharf: - -- **Typen sind nicht aufgelöst** bei Definitionen, deren Typ aus dem Rückgabewert einer Factory stammt, -- **Argumente sind nicht autowiret**, -- manche `@service`-Referenzen sind noch schlichte Strings. - -Genau deshalb ist die Suche nach Typ hier unzuverlässig - mehr dazu [unten |#Den ContainerBuilder untersuchen: wann es sicher ist]. - - -Parameter: wann %param% aufgelöst wird -====================================== - -Eine der beiden Hauptfragen. Die Antwort ist kurz: **einmal, ganz am Anfang von Phase A, über den gesamten Baum der Konfiguration.** - -`ParametersExtension` läuft als Erste, und zu ihren ersten Handlungen gehört das Auflösen der Platzhalter `%param%` - zuerst innerhalb der Parameter selbst (ein Parameter kann auf einen anderen verweisen), dann im gesamten Rest der Konfiguration. Wenn also irgendeine andere Extension, einschließlich `ServicesExtension`, ihren Abschnitt bekommt, sind die Platzhalter bereits verschwunden. Extensions arbeiten mit konkreten Werten, nie mit `%...%`. - -Ist ein Platzhalter der gesamte String, wird sein Wert *unverändert* zurückgegeben - auch Arrays und Objekte -, `%mailer%` kann sich also zu einem ganzen Array auflösen. Überall sonst wird er in einen String eingefügt, und die Punktschreibweise `%foo.bar%` greift in verschachtelte Arrays hinein. - - -Statische vs. dynamische Parameter ----------------------------------- - -Nicht jeder Wert lässt sich in den Code einbacken. Ein Parameter, dessen Wert sich je nach Umgebung unterscheidet - eine Umgebungsvariable, die aus dem Request abgeleitete `baseUrl` -, muss **dynamisch** bleiben. Solche Parameter deklarieren Sie über `setDynamicParameterNames()` oder `Expect::...->dynamic()` in einem Schema; mehr dazu unter [dynamische Parameter |application:bootstrapping#Dynamische Parameter]. - -Ein dynamischer Parameter wird nicht durch einen Wert ersetzt, sondern durch einen Ausdruck, der ihn *zur Laufzeit* liest. `%env.DB_HOST%` friert also nicht zu einem String ein, sondern wird zu einer Abfrage zur Laufzeit im erzeugten Container. Alles andere ist statisch und wird zur Kompilierzeit eingefroren - daher rührt die übliche Überraschung "mein Wert aus `getenv()` ist in jeder Umgebung derselbe": Der Parameter war schlicht statisch. - -Die umgekehrte Operation ist das **Escapen**: Damit ein wörtliches `%` oder `@` nicht interpretiert wird, verdoppelt man es (`%%`, `@@`). Nette macht das bei den Parametern, die es Ihnen bereitstellt, automatisch, sodass ihre Werte nie mit Platzhaltern oder Referenzen verwechselt werden. - - -Referenzen: wann aus @service eine Referenz wird -================================================ - -Die zweite Hauptfrage. Die Übersetzung von `@service` geschieht **in mehreren Schritten über verschiedene Phasen hinweg**, je nachdem, wie komplex der String ist. Sie müssen das selten von Hand nachvollziehen, aber die Schritte zu kennen erklärt, warum manche Referenzen früher aufgelöst werden als andere. - -- **Parsen (Laden der Konfiguration).** Ein `@service`, das *als Entity* verwendet wird - also als das, was einen Service erzeugt, wie in `Foo(@bar)` -, wird sofort zu einer Referenz. Ein `@service`, das *als Argument* verwendet wird, bleibt vorerst ein schlichter String. Ein in Anführungszeichen stehendes `@` wird zu `@@` escapt und gilt damit als wörtlicher Text, nicht als Referenz. -- **Phase A (`loadConfiguration`).** Beim Verarbeiten der Definitionen wird ein sauberes Argument `@name` oder `@Type` in ein `Reference`-Objekt verwandelt. Das erfasst nur die einfachen Formen; `@service::CONST` oder ein `@` innerhalb eines größeren Ausdrucks bleibt für später. -- **Phase B (`complete`).** Hier geschieht die eigentliche "kluge" Übersetzung: `@service` → Referenz, `@service::CONSTANT` → eine wörtliche Klassenkonstante, `@service::property` → das Lesen dieser Property, `@@x` → der wörtliche Text `@x`. - -Im Wort *Referenz* selbst steckt noch eine zweite Übersetzung. Eine `Reference` kann entweder über den **Namen** oder über den **Typ** (`@Namespace\Type`) zeigen. Eine Referenz über den Typ ist **noch kein Name eines Services** - sie wird durch das Autowiring zu einem konkreten Namen aufgelöst, und das geschieht erst im Schritt **complete**, sobald der Index des Autowirings gebaut ist. Das ist die Brücke zum nächsten Abschnitt: Abfragen des Autowirings werden absichtlich aufgeschoben, bis der Index bereitsteht. - -| Form | Wird zur Referenz/zum Ausdruck in | Wird zu einem konkreten Service aufgelöst in -|---|---|--- -| Entity (`@foo` als Factory) | Parsen | complete -| Argument `@foo`, `@Type` | Phase A | complete -| `@foo::CONST`, `@foo::prop` | Phase B | complete -| Referenz über den Typ `@Type` | Phase A/B | complete (Autowiring) - - -Den ContainerBuilder untersuchen: wann es sicher ist -==================================================== - -Nun die Frage, die Autoren von Extensions am häufigsten stellen: **In welcher Methode kann ich nach Services nach Typ suchen?** Die Antwort ergibt sich aus einer einfachen Regel darüber, wie der Builder seinen eigenen Zustand verfolgt. - -Die Suche **nach Typ** (`getByType()`, `getDefinitionByType()`, `findByType()`) verlangt, dass der Graph der Services *aufgelöst* ist - jeder Typ bekannt, der Index des Autowirings gebaut. Sobald Sie also eine dieser Methoden aufrufen und sich der Graph seit dem letzten Auflösen geändert hat, **löst der Builder den gesamten bekannten Graphen an Ort und Stelle auf**. Während des Auflösens selbst ist jede Suche nach Typ verboten und wirft eine `NotAllowedDuringResolvingException`. - -Die Suche **nach Tag** (`findByTag()`) hat diese Voraussetzung nicht - Tags hängen nicht von Typen ab, sie funktioniert also in **jeder Phase**. - -Phase für Phase: - -- **`loadConfiguration()` (Phase A) - die Suche nach Typ ist unzuverlässig.** Der Graph ist unvollständig: Extensions, die später laufen, haben ihre Services noch nicht registriert, und vor allem fehlt der Abschnitt `services:` des Nutzers (der zuletzt läuft). Ein Aufruf von `getByType()` funktioniert zwar - er löst einen vorzeitigen Resolve eines Teilgraphen aus -, aber die Antwort stammt aus einem unvollständigen Bild, und der vorzeitige Resolve ist verschwendete Arbeit. Faustregel: **In `loadConfiguration()` nur Definitionen registrieren, nicht nach Typ suchen.** `findByTag()` ist in Ordnung. -- **`beforeCompile()` (Phase B) - der richtige Ort zum Untersuchen.** Inzwischen existieren **alle** Definitionen (auch die des Nutzers), die **Typen sind aufgelöst** und der **Index des Autowirings ist gebaut**, `getByType()`, `findByType()` und `findByTag()` liefern also **verlässliche** Antworten. Die Argumente sind *noch nicht* autowiret - das ist der unmittelbar nächste Schritt (`complete`), nach allen Aufrufen von `beforeCompile()`. Wenn Sie hier eine Definition ändern, löst das nächste `getByType()` den Graphen transparent neu auf, Sie können also frei zwischen Änderungen und Abfragen wechseln. -- **`afterCompile()` (Phase C) - nur Code.** Sie arbeitet über der erzeugten Klasse, nicht über dem Builder. Der Graph ist fertig; hier formen Sie das entstehende PHP. - -| Ich möchte ... | Phase -|---|--- -| einen Service registrieren | `loadConfiguration()` -| nach **Tag** suchen und Definitionen ändern | `loadConfiguration()` oder `beforeCompile()` -| nach **Typ** suchen (`getByType`/`findByType`) | **`beforeCompile()`** -| davon abhängen, welche Services das Autowiring für Argumente gewählt hat | nicht zur Kompilierzeit - prüfen Sie es zur Laufzeit -| den erzeugten Code anfassen | `afterCompile()` -| Code ausführen, nachdem der Container gestartet ist | [Initialisierungscode |extensions#Initialisierungscode] - - -Innerhalb von Phase B: resolve und complete -=========================================== - -Phase B besteht aus zwei Durchläufen, zwischen die die Aufrufe von `beforeCompile()` eingeschoben sind: - -```php -$this->builder->resolve(); // Typen aufgelöst, Index des Autowirings gebaut -foreach ($this->extensions as $extension) { - $extension->beforeCompile(); -} -$this->builder->complete(); // ERST JETZT werden die Argumente autowiret -``` - -**`resolve()`** bestimmt den Typ jedes Services - entweder aus seinem deklarierten `type` oder abgeleitet aus seiner Factory: dem Rückgabetyp einer Factory-Methode, der Klasse, die sie instanziiert, oder dem Service, auf den eine Referenz zeigt - und baut anschließend den Index des Autowirings, der jeden Typ (die Klasse samt ihren Eltern und Interfaces) auf einen Namen eines Services abbildet. Ein Service mit `autowired: false` bleibt aus dem Index heraus; `autowired: [A, B]` schränkt die Typen ein, unter denen er sichtbar ist. Entscheidend ist: resolve klärt *Typen*, nicht *Argumente* - das Autowiring der Argumente bräuchte den fertigen Index, den es erst nach diesem Durchlauf gibt. - -**`complete()`** ist der Ort, an dem das Autowiring der Argumente tatsächlich geschieht. Für jede Definition füllt es die fehlenden Argumente des Konstruktors und des Setups, indem es ihre Typen im nun vollständigen Index nachschlägt. Deshalb blieben die Referenzen über den Typ während resolve unaufgelöst: Das Nachschlagen gehört hierher, sobald es einen verlässlichen Index gibt, in dem sich nachschlagen lässt. - - -Phase C: Erzeugen des Codes -=========================== - -`generateCode()` übergibt den fertigen Graphen an den `PhpGenerator`, der eine Klasse erzeugt, die `Container` erweitert und pro Service eine Methode `createServiceXxx()` hat, dazu die vorberechneten Metadaten `aliases`, `tags` und `wiring`. Jedes `Statement` wird zu PHP-Text (`new Foo(...)`, Methodenaufrufe, Zugriff auf Properties), und jede `Reference` wird zu einem Aufruf `$this->getService(...)`. - -Die Extensions bekommen dann einen abschließenden Durchlauf `afterCompile()` über der erzeugten Klasse - hier werden zum Beispiel die Getter für statische und dynamische Parameter ausgegeben - und dazu die Gelegenheit, [Initialisierungscode |extensions#Initialisierungscode] zu ergänzen, der bei jedem Request läuft. - - -Der Ablauf in einem Bild -======================== - -``` -KOMPILIERUNG (einmal, in den Cache) -│ -├─ Konfigurationsdateien laden NEON -> Statement/Array; Dateien zusammenführen -│ @ in Anführungszeichen -> @@ ; Entities -> Statement -│ -▼ Compiler::compile() -│ -├─ PHASE A processExtensions() -│ ├─ ParametersExtension (ERSTE) ── %param% AUFGELÖST in der gesamten Konfiguration -│ │ dynamische -> Ausdruck zur Laufzeit -│ ├─ ExtensionsExtension (ERSTE) ── registriert weitere Extensions -│ ├─ ...weitere Extensions... ── loadConfiguration(): nur Definitionen registrieren -│ └─ ServicesExtension (LETZTE) ── services: -> Objekte vom Typ Definition -│ @name/@Type -> Reference -│ [Graph der Anzahl nach vollständig; TYPEN und ARGUMENTE noch nicht; Suche nach Typ unzuverlässig] -│ -├─ PHASE B processBeforeCompile() -│ ├─ builder.resolve() ── alle Typen auflösen; Index des Autowirings bauen -│ │ [Typen bereit; Index bereit] -│ ├─ beforeCompile() Extensions ── hier ist getByType/findByType/findByTag SICHER -│ │ (Argumente noch nicht autowiret) -│ └─ builder.complete() ── ARGUMENTE autowiren; Übersetzung der Referenzen abschließen -│ Referenzen über den Typ -> Namen von Services -│ -└─ PHASE C generateCode() - ├─ PhpGenerator.generate() ── Statement -> PHP; Methoden createServiceXxx() - ├─ afterCompile() Extensions ── Code anpassen; Getter für Parameter ausgeben - └─ toString() ── endgültiger PHP-Code -> Cache - -──────────────────────────────────────────────────────────── - -LAUFZEIT (jeder Request) -│ -├─ new Container($dynamicParams) -├─ initialize() ── Boot-Code der Extensions (Session, Header, Validierung) -└─ getService()/getByType() ── lazy Instanzen aus vorberechneten Metadaten -``` - - -Verbreitete Irrtümer -==================== - -- "In `loadConfiguration()` suche ich Services nach Typ." Nein - der Graph ist unvollständig (der Abschnitt `services:` des Nutzers läuft nach Ihnen) und `getByType()` löst einen vorzeitigen Resolve eines Teilgraphen aus. Verschieben Sie es nach `beforeCompile()`. `findByTag()` ist auch hier in Ordnung. -- "Ein Wert aus `getenv()` in einem Parameter unterscheidet sich je nach Umgebung." Nur wenn der Parameter dynamisch ist. Sonst wird er zur Kompilierzeit eingebacken und bleibt überall gleich. -- "Eine Referenz `@Type` ist bereits der Name eines Services." Ist sie nicht - sie ist eine Referenz über den Typ, die das Autowiring erst im Schritt complete zu einem konkreten Namen auflöst. -- "Meine Extension liest eine Hilfsdatei, aber Änderungen zeigen sich nicht." Melden Sie sie mit `$builder->addDependency($file)` an, sonst weiß der Cache nichts davon und baut nicht neu. -- "Während `resolve()` kann ich `getByType()` aufrufen." Nein - das wirft eine `NotAllowedDuringResolvingException`. Die Suche nach Typ gehört in `beforeCompile()` oder später, nie mitten ins Auflösen. diff --git a/dependency-injection/de/configuration.texy b/dependency-injection/de/configuration.texy deleted file mode 100644 index c1a7df1460..0000000000 --- a/dependency-injection/de/configuration.texy +++ /dev/null @@ -1,335 +0,0 @@ -Konfiguration des DI-Containers -******************************* - -.[perex] -Übersicht der Konfigurationsmöglichkeiten für den Nette DI Container. - - -Konfigurationsdatei -=================== - -Der Nette DI Container lässt sich bequem über Konfigurationsdateien steuern. Diese werden üblicherweise im [Format NEON|neon:format] geschrieben. Wir empfehlen, [Editoren mit Unterstützung |tools:ide] für dieses Format zu verwenden. - -<pre> -"decorator .[prism-token prism-atrule]":[#Decorator]: "Decorator .[prism-token prism-comment]"<br> -"di .[prism-token prism-atrule]":[#DI]: "DI-Container .[prism-token prism-comment]"<br> -"extensions .[prism-token prism-atrule]":[#Extensions]: "Weitere DI-Extensions installieren .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#Dateien einbinden]: "Dateien einbinden .[prism-token prism-comment]"<br> -"parameters .[prism-token prism-atrule]":[#Parameter]: "Parameter .[prism-token prism-comment]"<br> -"search .[prism-token prism-atrule]":[#Search]: "Automatische Registrierung von Services .[prism-token prism-comment]"<br> -"services .[prism-token prism-atrule]":[services]: "Services .[prism-token prism-comment]" -</pre> - -.[note] -Um einen String zu schreiben, der das Zeichen `%` enthält, müssen Sie es durch Verdoppeln zu `%%` escapen. - - -Parameter -========= - -In der Konfiguration können Sie Parameter definieren, die sich anschließend als Teil der Service-Definitionen verwenden lassen. So machen Sie die Konfiguration übersichtlicher oder führen Werte an einer Stelle zusammen, die sich ändern können. - -```neon -parameters: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: secret -``` - -Auf den Parameter `dsn` verweisen wir überall in der Konfiguration mit der Schreibweise `%dsn%`. Parameter lassen sich auch innerhalb von Strings verwenden, etwa `'%wwwDir%/images'`. - -Parameter müssen nicht nur Strings oder Zahlen sein, sie können auch Arrays enthalten: - -```neon -parameters: - mailer: - host: smtp.example.com - secure: ssl - user: franta@gmail.com - languages: [cs, en, de] -``` - -Auf einen bestimmten Schlüssel verweisen wir mit `%mailer.user%`. - -Wenn Ihr Code (etwa eine Klasse) den Wert eines Parameters braucht, übergeben Sie ihn der Klasse. Zum Beispiel im Konstruktor. Es gibt kein globales Konfigurationsobjekt, das Klassen nach Werten von Parametern fragen könnten. Das wäre ein Verstoß gegen das Prinzip der Dependency Injection. - - -Services -======== - -Siehe [eigenes Kapitel|services]. - - -Decorator -========= - -Wie ändern Sie mehrere Services eines bestimmten Typs auf einmal? Wie rufen Sie zum Beispiel auf allen Presentern, die von einer bestimmten Basisklasse erben, eine bestimmte Methode auf? Dafür ist der Decorator da. - -```neon -decorator: - # für alle Services, die Instanzen dieser Klasse oder dieses Interfaces sind - App\Presentation\BasePresenter: - setup: - - setProjectId(10) # diese Methode aufrufen - - $absoluteUrls = true # und die Variable setzen -``` - -Der Decorator lässt sich auch verwenden, um [Tags |services#Tags] zu setzen oder den [Inject-Modus |services#Inject-Modus] einzuschalten. - -```neon -decorator: - InjectableInterface: - tags: [mytag: 1] - inject: true -``` - - -DI -=== - -Technische Einstellungen des DI-Containers. - -```neon -di: - # den DIC in der Tracy Bar anzeigen? - debugger: ... # (bool) standardmäßig automatische Erkennung (eingeschaltet, wenn Tracy vorhanden ist) - - # Typen von Parametern, die Sie nie autowiren - excluded: ... # (string[]) - - # das lazy Erzeugen von Services einschalten? - lazy: ... # (bool) Standard ist false - - # die Klasse, von der der DI-Container erbt - parentClass: ... # (string) standardmäßig Nette\DI\Container -``` - - -Lazy Services .{data-version:3.2.4} ------------------------------------ - -Die Einstellung `lazy: true` aktiviert das lazy (aufgeschobene) Erzeugen von Services. Das bedeutet, dass Services nicht in dem Moment tatsächlich erzeugt werden, in dem sie beim DI-Container angefordert werden, sondern erst bei ihrer ersten Verwendung. Das kann den Start der Anwendung beschleunigen und den Speicherverbrauch senken, weil nur die Services erzeugt werden, die für den jeweiligen Request wirklich nötig sind. - -Für einen bestimmten Service lässt sich das lazy Erzeugen [anpassen |services#Lazy Services]. - -.[note] -Lazy Objekte lassen sich nur für eigene Klassen verwenden, nicht für interne PHP-Klassen. Erfordert PHP 8.4 oder neuer. - - -Export der Metadaten --------------------- - -Die Klasse des DI-Containers enthält außerdem eine Menge Metadaten. Sie können ihre Größe verringern, indem Sie den Export der Metadaten einschränken. - -```neon -di: - export: - # Parameter exportieren? - parameters: false # (bool) standardmäßig true - - # Tags exportieren, und welche? - tags: # (string[]|bool) standardmäßig alle - - event.subscriber - - # Daten für das Autowiring exportieren, und welche? - types: # (string[]|bool) standardmäßig alle - - Nette\Database\Connection - - Symfony\Component\Console\Application -``` - -Wenn Sie `$container->getParameters()` nicht verwenden, können Sie den Export der Parameter abschalten. Außerdem können Sie nur die Tags exportieren, die Sie tatsächlich verwenden, um Services über `$container->findByTag(...)` zu holen. Wenn Sie diese Methode überhaupt nicht aufrufen, können Sie den Export der Tags mit `false` vollständig abschalten. - -Die Metadaten für das [Autowiring|autowiring] lassen sich erheblich verkleinern, indem Sie nur die Klassen aufführen, die Sie tatsächlich über `$container->getByType()` anfordern. Auch hier gilt: Wenn Sie diese Methode nicht aufrufen (oder sie nur in der Datei [bootstrap|application:bootstrapping] aufrufen, etwa um `Nette\Application\Application` zu holen), können Sie den Export der Typen mit `false` vollständig abschalten. - - -Extensions -========== - -Registrierung weiterer DI-Extensions. So fügen Sie zum Beispiel die DI-Extension `Dibi\Bridges\Nette\DibiExtension3` unter dem Namen `dibi` hinzu: - -```neon -extensions: - dibi: Dibi\Bridges\Nette\DibiExtension3 -``` - -Konfiguriert wird sie anschließend im Abschnitt `dibi`: - -```neon -dibi: - host: localhost -``` - -Als Extension lässt sich auch eine Klasse mit Parametern hinzufügen: - -```neon -extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, [%appDir%], %tempDir%/cache) -``` - - -Dateien einbinden -================= - -Weitere Konfigurationsdateien lassen sich im Abschnitt `includes` einbinden: - -```neon -includes: - - parameters.php - - services.neon - - presenters.neon -``` - -Der Name `parameters.php` ist kein Tippfehler; die Konfiguration lässt sich auch in eine PHP-Datei schreiben, die sie als Array zurückgibt: - -```php -<?php -return [ - 'database' => [ - 'main' => [ - 'dsn' => 'sqlite::memory:', - ], - ], -]; -``` - -Erscheinen Elemente mit denselben Schlüsseln in mehreren Konfigurationsdateien, werden sie überschrieben oder bei Arrays [zusammengeführt |#Zusammenführen]. Eine später eingebundene Datei hat höhere Priorität als die vorherige. Die Datei, in der der Abschnitt `includes` steht, hat höhere Priorität als die darin eingebundenen Dateien. - - -Search -====== - -Die automatische Registrierung von Services im DI-Container vereinfacht die Entwicklung erheblich. Nette fügt dem Container die Presenter automatisch hinzu, Sie können aber ebenso leicht beliebige weitere Klassen ergänzen. - -Geben Sie einfach an, in welchen Verzeichnissen (und Unterverzeichnissen) nach den Klassen gesucht werden soll: - -```neon -search: - - in: %appDir%/Forms - - in: %appDir%/Model -``` - -Wenn Sie nur eine einzige Suchregel brauchen, können Sie die Liste weglassen und ihre Schlüssel direkt unter `search` schreiben: - -```neon -search: - in: %appDir% -``` - -Üblicherweise wollen wir aber nicht absolut alle Klassen und Interfaces hinzufügen, also können wir sie filtern: - -```neon -search: - - in: %appDir%/Forms - - # Filterung nach Dateiname (string|string[]) - files: - - *Factory.php - - # Filterung nach Klassenname (string|string[]) - classes: - - *Factory -``` - -Oder wir wählen die Klassen aus, die von mindestens einer der aufgeführten Klassen erben oder eines der Interfaces implementieren: - - -```neon -search: - - in: %appDir% - extends: - - App\*Form - implements: - - App\*FormInterface -``` - -Sie können auch Ausschlussregeln über Masken von Klassennamen oder über Vorfahren definieren. Passt eine Klasse auf eine Ausschlussregel, wird sie dem DI-Container nicht hinzugefügt: - -```neon -search: - - in: %appDir% - exclude: - files: ... - classes: ... - extends: ... - implements: ... -``` - -Allen automatisch registrierten Services lassen sich Tags zuweisen: - -```neon -search: - - in: %appDir% - tags: ... -``` - -Neben Klassen registriert die Suche auch Interfaces, die eine einzige Methode `create()` oder `get()` haben - als [generierte Factories oder Accessors |factory]. Klassen, für die im Container bereits ein Service desselben Typs registriert ist, werden übersprungen, es entstehen also keine Duplikate. - - -Zusammenführen -============== - -Erscheinen Elemente mit denselben Schlüsseln in mehreren Konfigurationsdateien, werden sie überschrieben oder bei Arrays zusammengeführt. Die später eingebundene Datei hat höhere Priorität als die vorherige. - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>Ergebnis</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> - <td> -```neon -items: - - 1 - - 2 - - 3 -``` - </td> -</tr> -</table> - -Bei Arrays lässt sich das Zusammenführen verhindern, indem hinter den Namen des Schlüssels ein Ausrufezeichen gesetzt wird: - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>Ergebnis</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items!: - - 3 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> -</tr> -</table> - -{{maintitle: Konfiguration der Dependency Injection}} diff --git a/dependency-injection/de/container.texy b/dependency-injection/de/container.texy deleted file mode 100644 index 686eeb2215..0000000000 --- a/dependency-injection/de/container.texy +++ /dev/null @@ -1,142 +0,0 @@ -Was ist ein DI-Container? -************************* - -.[perex] -Ein Dependency-Injection-Container (DIC oder DI-Container) ist ein Objekt, das dafür zuständig ist, andere Objekte (Services genannt) zu erzeugen und zu konfigurieren. - -Es mag Sie überraschen, aber in vielen Fällen brauchen Sie keinen Dependency-Injection-Container, um die Vorteile der Dependency Injection (kurz DI) zu nutzen. Schließlich haben wir schon im [einführenden Kapitel|introduction] konkrete Beispiele für DI gezeigt, und kein Container war nötig. - -Sobald Sie aber eine große Zahl von Objekten mit komplexen Abhängigkeiten verwalten, wird ein DI-Container sehr nützlich. Das ist bei Webanwendungen auf Basis eines Frameworks oft der Fall. - -Im vorigen Kapitel haben wir die Klassen `Article` und `EditController` vorgestellt. Beide haben Abhängigkeiten, nämlich die Datenbank und die Factory `ArticleFactory`. Und für diese Klassen bauen wir jetzt einen Container. Für ein so einfaches Beispiel ist ein Container natürlich mit Kanonen auf Spatzen geschossen. Wir bauen ihn aber, um zu zeigen, wie er aussieht und funktioniert. - -Hier ist ein einfacher, fest verdrahteter Container für das obige Beispiel: - -```php -class Container -{ - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection('mysql:', 'root', '***'); - } - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->createDatabase()); - } - - public function createEditController(): EditController - { - return new EditController($this->createArticleFactory()); - } -} -``` - -Die Verwendung sähe so aus: - -```php -$container = new Container; -$controller = $container->createEditController(); -``` - -Wir fordern das Objekt einfach beim Container an, ohne wissen zu müssen, wie es zu erzeugen ist oder welche Abhängigkeiten es hat; das alles erledigt der Container. Die Abhängigkeiten übergibt er automatisch. Das ist seine Stärke. - -Derzeit hat der Container alle Informationen fest verdrahtet. Gehen wir also einen Schritt weiter und ergänzen Parameter, damit der Container wirklich nützlich wird: - -```php -class Container -{ - public function __construct( - private array $parameters, - ) { - } - - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection( - $this->parameters['db.dsn'], - $this->parameters['db.user'], - $this->parameters['db.password'], - ); - } - - // ... -} - -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); -``` - -Aufmerksamen Lesern fällt vielleicht ein Problem auf. Jedes Mal, wenn wir ein `EditController`-Objekt holen, entstehen auch neue Instanzen von `ArticleFactory` und der Datenbankverbindung. Das wollen wir ganz sicher nicht. - -Wir ergänzen deshalb eine Methode `getService()`, die immer dieselben Instanzen zurückgibt: - -```php -class Container -{ - private array $services = []; - - public function __construct( - private array $parameters, - ) { - } - - public function getService(string $name): object - { - if (!isset($this->services[$name])) { - // getService('Database') ruft createDatabase() auf - $method = 'create' . $name; - $this->services[$name] = $this->$method(); - } - return $this->services[$name]; - } - - // ... -} -``` - -Beim ersten Aufruf, etwa `$container->getService('Database')`, ruft sie `createDatabase()` auf, um das Objekt der Datenbank zu erzeugen, legt es im Array `$services` ab und gibt es zurück. Bei weiteren Aufrufen gibt sie die bereits abgelegte Instanz direkt zurück. - -Auch den Rest des Containers passen wir so an, dass er `getService()` verwendet: - -```php -class Container -{ - // ... - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->getService('Database')); - } - - public function createEditController(): EditController - { - return new EditController($this->getService('ArticleFactory')); - } -} -``` - -Übrigens bezeichnet der Begriff Service jedes Objekt, das der Container verwaltet. Daher der Name der Methode `getService()`. - -Fertig. Wir haben einen voll funktionsfähigen DI-Container! Und wir können ihn verwenden: - -```php -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); - -$controller = $container->getService('EditController'); -$database = $container->getService('Database'); -``` - -Wie Sie sehen, ist es nicht schwer, einen DIC zu schreiben. Bemerkenswert ist, dass die Objekte selbst nichts davon wissen, dass ein Container sie erzeugt. Folglich lässt sich auf diese Weise jedes PHP-Objekt erzeugen, ohne seinen Quellcode zu ändern. - -Die Klasse des Containers von Hand zu erzeugen und zu pflegen kann schnell zum Albtraum werden. Deshalb sprechen wir im nächsten Kapitel über den [Nette DI Container|nette-container], der sich fast vollständig selbst erzeugen und aktualisieren kann. - - -{{maintitle: Was ist ein Dependency-Injection-Container?}} diff --git a/dependency-injection/de/extensions.texy b/dependency-injection/de/extensions.texy deleted file mode 100644 index 5a8fd3a9c7..0000000000 --- a/dependency-injection/de/extensions.texy +++ /dev/null @@ -1,443 +0,0 @@ -Extensions für Nette DI erstellen -********************************* - -.[perex] -Eine Extension ist eine Klasse, die sich in die Kompilierung des DI-Containers einklinkt. Sie kann Services programmatisch registrieren, ihren eigenen Konfigurationsabschnitt validieren, Services anderer verändern und sogar den erzeugten Code des Containers anpassen. Diese Seite zeigt Ihnen, wie Sie eine schreiben, was wann passiert und worauf Sie achten müssen. - -Über Extensions binden sich Pakete auf die native Art in Nette ein: Alle `nette/*`-Pakete nutzen sie, und Ihre kann das auch. Eine typische Extension erledigt eines oder mehrere dieser Dinge: - -- **bindet eine Bibliothek ein** - registriert deren Services im Container und stellt einen freundlichen, validierten Konfigurationsabschnitt bereit (daher kommen die Abschnitte `mail:` oder `database:`) -- **automatisiert die Registrierung** - registriert viele ähnliche Services in einer Schleife oder nach einer Regel, wo es mühsam wäre, sie in `services:` aufzuzählen -- **nimmt übergreifende Änderungen vor** - findet Services, die andere registriert haben, und ergänzt sie, hängt etwa an jeden Service mit einem bestimmten Tag einen Logger - -Für die tägliche Arbeit an einer Anwendung brauchen Sie selten eine - der Abschnitt [services |services] der Konfiguration deckt das Registrieren und Verdrahten Ihrer Klassen ab. Greifen Sie zu einer Extension, wenn die Konfiguration allein nicht mehr reicht. - -Aktiviert wird eine Extension im Abschnitt `extensions`. So fügen Sie eine Extension, die die Klasse `BlogExtension` darstellt, unter dem Namen `blog` hinzu: - -```neon -extensions: - blog: BlogExtension -``` - -Wenn ihr Konstruktor Argumente entgegennimmt, übergeben Sie sie gleich dort: - -```neon -extensions: - blog: BlogExtension(%debugMode%) -``` - - -Wie die Kompilierung abläuft -============================ - -Um Extensions sicher schreiben zu können, müssen Sie eines wissen: **wann Ihr Code läuft.** Nette verdrahtet die Services nicht beim Bearbeiten von Requests. Stattdessen *kompiliert* es den Container im Voraus: Es liest alle Konfigurationsdateien, lässt die Extensions ihre Arbeit tun und erzeugt eine optimierte PHP-Klasse, die es auf der Festplatte ablegt. Jeder weitere Request lädt nur noch diese fertige Klasse. Der Code Ihrer Extension läuft also nur dann, wenn der Container (neu) gebaut wird - nicht bei jedem Request. - -Das hat eine wichtige Folge: Während der Kompilierung existiert noch kein einziger Service. Was existiert, sind **Definitionen** - Rezepte, die beschreiben, welche Klasse jeder Service haben wird, wie er zu erzeugen ist und was danach auf ihm aufgerufen werden soll. Die Definitionen leben im Objekt [ContainerBuilder |#ContainerBuilder]. Eine Extension ist im Grunde *skriptbare Konfiguration*: Alles, was Sie im Abschnitt `services:` deklarieren können, können Sie auch in PHP bauen - bedingt, in Schleifen oder als Reaktion darauf, was andere registriert haben. - -Die Kompilierung läuft in Phasen ab, und eine Extension kann in jede von ihnen eingreifen: - -1) die Konfigurationsabschnitte aller Extensions werden validiert (`getConfigSchema()`) -2) jede Extension registriert ihre Services (`loadConfiguration()`); der Abschnitt `services:` des Nutzers wird zuletzt verarbeitet, die Anwendung hat also immer das letzte Wort -3) sobald alle Definitionen stehen und die Typen der Services aufgelöst sind, dürfen die Extensions sie verändern (`beforeCompile()`) -4) die Klasse des Containers wird erzeugt; die Extensions können ihren Code noch anpassen (`afterCompile()`) und Code ausgeben, der beim Start der Anwendung läuft ([Initialisierung |#Initialisierungscode]) - -.[note] -Im Entwicklermodus wird der Container automatisch neu kompiliert, sobald Sie eine Konfigurationsdatei oder die Klasse der Extension selbst ändern - beides wird als Abhängigkeit verfolgt. Sie können Extensions also entwickeln, ohne je einen Cache zu leeren. - -.[tip] -Einen tieferen Blick darauf, was in jeder Phase geschieht - wann Parameter aufgelöst werden, wann aus `@service` eine Referenz wird und ab wann es sicher ist, Services nach Typ zu suchen -, bietet [Kompilierung im Detail |compilation-internals]. - - -Die erste Extension -=================== - -Hier ist eine kleine, aber vollständige Extension. Aktiviert und konfiguriert wird sie in derselben Datei: - -```neon -extensions: - blog: BlogExtension - -blog: - postsPerPage: 5 -``` - -Und das ist die ganze Klasse: - -```php -use Nette\Schema\Expect; - -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function getConfigSchema(): Nette\Schema\Schema - { - return Expect::structure([ - 'postsPerPage' => Expect::int(10), - 'allowComments' => Expect::bool(true), - ]); - } - - - public function loadConfiguration(): void - { - $builder = $this->getContainerBuilder(); - - $builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class, ['postsPerPage' => $this->config->postsPerPage]); - - if ($this->config->allowComments) { - $builder->addDefinition($this->prefix('comments')) - ->setFactory(Blog\Comments::class); - } - } -} -``` - -`getConfigSchema()` beschreibt, was der Abschnitt `blog:` (benannt nach dem Schlüssel, unter dem wir die Extension registriert haben) enthalten darf, samt Typen und Standardwerten - die validierten Werte stehen danach in `$this->config` zur Verfügung. In `loadConfiguration()` registrieren wir die Services. Achten Sie auf die Namen: `$this->prefix('articles')` erzeugt `blog.articles`, sodass sich Services verschiedener Extensions nicht in die Quere kommen können. - -Und die letzten Zeilen zeigen, wozu es Extensions überhaupt gibt: Der Service `comments` wird nur registriert, wenn Kommentare eingeschaltet sind. Eine reine Konfigurationsdatei kann solche Entscheidungen nicht treffen. - -So registrierte Services verhalten sich genau so, als stünden sie in `services:` - sie werden bei Bedarf lazy erzeugt, und das Autowiring übergibt sie überall dorthin, wo `Blog\Articles` als Typ angegeben ist. - -Die folgenden Kapitel beschreiben ausführlich den Lebenszyklus einer Extension, dann die API des [ContainerBuilder |#ContainerBuilder], die Sie in der Extension verwenden, und schließlich die [Fallstricke |#Tipps und Fallstricke], die man kennen sollte. - - -Lebenszyklus einer Extension -============================ - -Eine Extension erbt von [api:Nette\DI\CompilerExtension] und überschreibt einige der vier Methoden `getConfigSchema()`, `loadConfiguration()`, `beforeCompile()` und `afterCompile()`, die der Compiler während der Kompilierung in dieser Reihenfolge aufruft. - - -getConfigSchema(): Nette\Schema\Schema .[method] ------------------------------------------------- - -Definiert das Schema des Konfigurationsabschnitts der Extension. Dadurch bekommen die Nutzer Validierung und klare Fehlermeldungen geschenkt: Ein Tippfehler oder ein falscher Typ im Abschnitt `blog:` wird mit einer verständlichen Meldung gemeldet, ohne dass Sie eine einzige Prüfung schreiben. - -Das Schema wird mit der Bibliothek [Schema |schema:] beschrieben und kann Typen, Standardwerte, erlaubte Werte und vieles mehr ausdrücken: - -```php -public function getConfigSchema(): Nette\Schema\Schema -{ - return Expect::structure([ - 'postsPerPage' => Expect::int(10), - 'storage' => Expect::anyOf('files', 'database')->firstIsDefault(), - ]); -} -``` - -Die validierte Konfiguration steht in `$this->config` als Objekt vom Typ `stdClass` zur Verfügung (oder als Array, wenn Sie dem Schema `castTo('array')` anhängen). - -Wenn der Wert einer Option zur Kompilierzeit nicht bekannt sein kann - etwa weil er aus einer Umgebungsvariablen stammt -, kennzeichnen Sie ihn mit `dynamic()`, also zum Beispiel `Expect::int()->dynamic()`. Mehr dazu unter [dynamische Parameter |application:bootstrapping#Dynamische Parameter]. - - -loadConfiguration() .[method] ------------------------------ - -Der Ort, an dem die Extension über den [ContainerBuilder |#ContainerBuilder] ihre Services registriert: - -```php -public function loadConfiguration(): void -{ - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class); -} -``` - -Soll ein Service auch unter einem kurzen Namen verfügbar sein, ergänzen Sie einen Alias. Üblicherweise geschieht das nur, wenn die Extension unter ihrem gewohnten Namen registriert ist, damit sich mehrere Instanzen der Extension nicht darum streiten können: - -```php -if ($this->name === 'blog') { - $builder->addAlias('articles', $this->prefix('articles')); -} -``` - -Wenn es viele Services sind, ist es womöglich bequemer, sie in einer eigenen NEON-Datei mit der vertrauten [services |services]-Syntax zu definieren. Das Präfix `@extension` verweist auf die aktuelle Extension: - -```neon -services: - articles: - create: MyBlog\ArticlesModel(@connection) - - comments: - create: MyBlog\CommentsModel(@connection, @extension.articles) -``` - -Diese Definitionen laden wir mit `loadDefinitionsFromConfig()`; die Namen bekommen automatisch das Präfix, und die Datei wird als Abhängigkeit verfolgt, sodass eine Änderung eine Neukompilierung auslöst: - -```php -public function loadConfiguration(): void -{ - $this->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/services.neon')['services'], - ); -} -``` - - -beforeCompile() .[method] -------------------------- - -Wenn diese Methode aufgerufen wird, hält der Builder bereits **alle** Definitionen: Ihre, die anderer Extensions und die aus den Konfigurationsdateien des Nutzers. Auch die Typen der Services sind aufgelöst, die Suche nach Typ ist also verlässlich. Damit ist diese Phase ideal, um den endgültigen Graphen der Services zu untersuchen und zu ergänzen. - -Typischerweise suchen Sie Services nach Tag oder nach Typ und ergänzen die gefundenen Definitionen: - -```php -public function beforeCompile(): void -{ - $builder = $this->getContainerBuilder(); - - foreach ($builder->findByTag('logaware') as $name => $attrs) { - $builder->getDefinition($name)->addSetup('setLogger'); - } -} -``` - -Der Aufruf `setLogger()` hat keine ausdrücklichen Argumente - die liefert das Autowiring, genau wie in Factories. - -Sie können auch mit anderen registrierten Extensions zusammenarbeiten, die Sie über `$this->compiler->getExtensions()` bekommen, wahlweise nach Klasse oder Interface gefiltert: - -```php -foreach ($this->compiler->getExtensions(FooExtension::class) as $extension) { - // ... -} -``` - - -afterCompile(Nette\PhpGenerator\ClassType $class) .[method] ------------------------------------------------------------ - -In der letzten Phase wird die Klasse des Containers als Objekt [ClassType |php-generator:#Klassen] der Bibliothek [PHP Generator |php-generator:] erzeugt. Sie enthält für jeden Service eine Factory-Methode und steht kurz davor, in den Cache geschrieben zu werden. Ihren Code können Sie noch verändern: - -```php -public function afterCompile(Nette\PhpGenerator\ClassType $class): void -{ - $method = $class->getMethod('__construct'); - // ... -} -``` - -Diese Phase werden Sie nur selten brauchen. Um Code zu ergänzen, der beim Start der Anwendung läuft, verwenden Sie stattdessen die Initialisierung: - - -Initialisierungscode --------------------- - -Alle bisherigen Phasen beeinflussen, wie der Container *gebaut* wird. Darüber hinaus kann eine Extension Code ausgeben, der zur *Laufzeit* läuft, unmittelbar nachdem der Container erzeugt wurde - etwa um eine Session zu starten oder Services anzustoßen. Der Code wird über die Methode [addBody() |php-generator:#Rümpfe von Methoden und Funktionen] in das Objekt `$this->initialization` geschrieben: - -```php -public function loadConfiguration(): void -{ - // Services mit dem Tag 'run' müssen gleich nach dem Start des Containers erzeugt werden - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $attrs) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } -} -``` - -Nette selbst nutzt die Initialisierung zum Beispiel, um die Session automatisch zu starten oder Sicherheits-HTTP-Header zu senden. Und denken Sie daran: Anders als alles andere in einer Extension läuft dieser Code bei **jedem Request**, halten Sie ihn also klein. - - -ContainerBuilder -================ - -[api:Nette\DI\ContainerBuilder] ist das Objekt, über das eine Extension mit dem Compiler spricht. Es hält die [Definitionen |#Wie die Kompilierung abläuft] aller Services und bietet Methoden, um sie hinzuzufügen, zu finden und zu verändern. Sie bekommen es in `loadConfiguration()` und `beforeCompile()`: - -```php -$builder = $this->getContainerBuilder(); -``` - - -Services hinzufügen -------------------- - -Einen Service zu registrieren ist dasselbe, was Sie im Abschnitt `services:` einer NEON-Datei tun - nur in PHP geschrieben. Zu jedem Konfigurationsschlüssel gehört eine Methode auf der Definition, diese beiden Schreibweisen sind also gleichwertig: - -```neon -services: - articles: - create: Blog\Articles(@connection) - setup: - - setLogger(@logger) - tags: [logaware] -``` - -```php -$builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class, ['@connection']) - ->addSetup('setLogger', ['@logger']) - ->addTag('logaware'); -``` - -Die von `addDefinition()` zurückgegebene Definition ist eine [ServiceDefinition |#Typen von Definitionen] mit den Gegenstücken zu den Konfigurationsschlüsseln: `setType()` (die Klasse des Services), `setFactory()` (wie er zu erzeugen ist), `setArguments()`, `addSetup()`, `addTag()` und `setAutowired()`. - -`addSetup()` spiegelt die Liste `setup:` wider und akzeptiert dieselben Formen: einen Methodenaufruf `addSetup('setLogger', ['@logger'])`, eine Zuweisung an eine Property `addSetup('$cache', ['@cache'])` oder einen Aufruf auf einem anderen Service `addSetup('@Tracy\Bar::addPanel', [$panel])`. - -Neben gewöhnlichen Services kann der Builder auch [generierte |factory] Factories, Accessors und Locators registrieren - jeweils mit einer eigenen Methode, die den passenden [Typ der Definition |#Typen von Definitionen] zurückgibt: - -| Methode | Registriert -|--------|---------- -| `addDefinition()` | einen gewöhnlichen Service (gibt `ServiceDefinition` zurück) -| `addFactoryDefinition()` | eine generierte [Factory |factory] (Interface mit einer Methode `create()`) -| `addAccessorDefinition()` | einen generierten [Accessor |factory#Accessor] (Interface mit einer Methode `get()`) -| `addLocatorDefinition()` | eine [Multifactory bzw. einen Locator |factory#Multifactory/Accessor], die mehrere Factories vereint -| `addImportedDefinition()` | einen Service, der dem Container zur Laufzeit von außen übergeben wird -| `addAlias()` | einen zweiten Namen für einen bestehenden Service - -Bei einer Factory konfigurieren Sie das Objekt, das sie erzeugt, über `getResultDefinition()`; ein Accessor verweist stattdessen über `setReference()` auf einen bestehenden Service: - -```php -$builder->addFactoryDefinition($this->prefix('latteFactory')) - ->setImplement(LatteFactory::class) - ->getResultDefinition() - ->setFactory(Latte\Engine::class) - ->addSetup('setStrictTypes', [true]); -``` - -`addLocatorDefinition()` und `addImportedDefinition()` braucht man selten - solche Services kommen üblicherweise aus dem Schlüssel `implement:` und aus importierten Services in NEON, statt von Hand geschrieben zu werden. - - -Services finden und ändern --------------------------- - -Zum Suchen und Durchlaufen der bestehenden Definitionen bietet der Builder: - -| Methode | Beschreibung -|--------|------------ -| `getDefinition(string $name)` | die Definition mit dem angegebenen Namen (wirft, wenn sie fehlt) -| `hasDefinition(string $name)` | ob eine Definition oder ein Alias mit dem Namen existiert -| `getDefinitions()` | alle Definitionen -| `removeDefinition(string $name)` | entfernt eine Definition -| `getByType(string $type)` | den Namen des autowireten Services des Typs, oder `null` -| `getDefinitionByType(string $type)` | die autowirete Definition des Typs -| `findByType(string $type)` | alle Definitionen des Typs als Paare `Name => Definition` -| `findByTag(string $tag)` | Services mit dem Tag als Paare `Name => Wert des Tags` -| `addExcludedClasses(array $types)` | nimmt Klassen und Interfaces vom Autowiring aus - -Ein praktisches Idiom ist, mit `getByType()` herauszufinden, ob ein Service überhaupt existiert - um sich zum Beispiel nur dann an einen Logger zu hängen, wenn die Anwendung einen hat: - -```php -if ($builder->getByType(Psr\Log\LoggerInterface::class)) { - $builder->getDefinition($this->prefix('articles')) - ->addSetup('setLogger'); -} -``` - - -Typen von Definitionen ----------------------- - -Jede Methode `add*Definition()` gibt eine andere Art von Definition zurück. Sie alle erben vom gemeinsamen Vorfahren `Nette\DI\Definitions\Definition`: - -- **`ServiceDefinition`** - ein gewöhnlicher Service; konfiguriert mit `setType()`, `setFactory()`, `addSetup()`, `addTag()` und `setAutowired()` -- **`FactoryDefinition`** - eine [generierte Factory |factory]: ein Interface, dessen Methode `create()` bei jedem Aufruf ein neues Objekt zurückgibt -- **`AccessorDefinition`** - ein [generierter Accessor |factory#Accessor]: ein Interface, dessen Methode `get()` einen bestehenden Service zurückgibt -- **`LocatorDefinition`** - eine [Multifactory bzw. ein Locator |factory#Multifactory/Accessor], die mehrere Factories oder Accessors in einem Interface vereint -- **`ImportedDefinition`** - ein Service, den der Container nicht selbst erzeugt, sondern zur Laufzeit von außen bekommt - -Denken Sie daran, dass `getDefinition()` die Art von Definition zurückgibt, die unter dem angegebenen Namen liegt. Wenn Ihr Code auf eine generierte Factory stoßen kann, prüfen Sie zuerst den Typ und konfigurieren Sie das erzeugte Objekt über `getResultDefinition()`: - -```php -$def = $builder->getDefinition($name); -if ($def instanceof Nette\DI\Definitions\FactoryDefinition) { - $def = $def->getResultDefinition(); -} -$def->addSetup('setLogger'); -``` - - -Tipps und Fallstricke -===================== - - -Kompilierzeit vs. Laufzeit --------------------------- - -Die häufigste Quelle für Verwirrung: Der Code einer Extension läuft, während der Container **kompiliert** wird, nicht während die Anwendung Requests bearbeitet. In der Praxis heißt das: - -- Eine Extension arbeitet nie mit Instanzen von Services - die existieren noch nicht. Erzeugen Sie Services nicht mit `new`; registrieren Sie eine Definition und lassen Sie sie den Container erzeugen. -- Alle Konfigurationswerte werden in den erzeugten Code eingebacken. Ein Wert, der sich zwischen Umgebungen unterscheiden kann (ein Pfad, ein Passwort aus `getenv()`), muss als [dynamisch |application:bootstrapping#Dynamische Parameter] gekennzeichnet werden, sonst wird er zur Kompilierzeit eingefroren. -- Strings, die Sie an `$this->initialization->addBody()` übergeben, werden jetzt nicht ausgeführt - sie sind PHP-Code, der in den Container ausgegeben und bei jedem Request ausgeführt wird. - - -Abhängigkeiten von Dateien --------------------------- - -Der Container wird neu kompiliert, wenn sich Konfigurationsdateien oder Klassen von Extensions ändern. Wenn Ihre Extension aber irgendeine andere Datei liest - eine Liste von Entities, eine XML-Konfiguration einer Bibliothek -, kann der Container davon nichts wissen. Melden Sie solche Dateien an mit: - -```php -$builder->addDependency($file); -``` - -Sonst erwartet Sie ein klassisches Rätsel: Sie ändern die Datei, aber die Anwendung verhält sich weiter wie zuvor - die Änderung zeigt sich erst, wenn der Container aus einem anderen Grund neu gebaut wird. (Über `loadFromFile()` gelesene Dateien werden automatisch verfolgt.) - - -Bedingte Registrierung ----------------------- - -Eine Extension kann sich an ihre Umgebung anpassen. Optionale Integrationen sichert man üblicherweise mit `class_exists()` ab: - -```php -if (class_exists(Symfony\Component\Console\Command\Command::class)) { - $builder->addDefinition($this->prefix('command')) - ->setFactory(Blog\Console\SitemapCommand::class); -} -``` - -Und Werte wie `%debugMode%` übergibt man am besten über den Konstruktor der Extension: - -```neon -extensions: - blog: BlogExtension(%debugMode%) -``` - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function __construct( - private bool $debugMode = false, - ) {} -} -``` - -Ein typischer Anwendungsfall ist, ein Tracy-Panel nur im Entwicklermodus zu registrieren. - - -Komplexe Argumente ------------------- - -Manchmal ist ein Argument für eine Factory oder einen Setup-Aufruf kein einfacher Wert, kein Klassenname und keine `@service`-Referenz. Für diese Fälle gibt es: - -- `new Nette\DI\Definitions\Statement(Blog\Panel::class, [$args])` - ein an Ort und Stelle erzeugtes Objekt, ein "anonymer Service", der als Argument dient -- `new Nette\DI\Definitions\Reference('blog.articles')` - eine Referenz auf einen Service, das Objekt-Gegenstück zum String `@name` -- `$builder::literal('PHP_SAPI')` - ein Stück rohen PHP-Codes, das unverändert in den erzeugten Container eingefügt wird - -Ein Beispiel - die Registrierung eines Tracy-Panels: - -```php -$builder->getDefinition($this->prefix('articles')) - ->addSetup('@Tracy\Bar::addPanel', [ - new Nette\DI\Definitions\Statement(Blog\ArticlesPanel::class), - ]); -``` - - -Exportierte Tags und Typen --------------------------- - -Der [Export der Metadaten |configuration#Export der Metadaten] lässt sich in der Konfiguration so einschränken, dass der kompilierte Container nur die Tags und Autowiring-Typen behält, die die Anwendung tatsächlich nutzt. Wenn Ihre Extension zur Laufzeit Services über `$container->findByTag()` oder `$container->getByType()` holt, könnte eine solche Einschränkung genau die Metadaten entfernen, auf die Sie sich verlassen. - -Um dem vorzubeugen, sagen Sie dem Compiler, welche Tags und Typen immer exportiert werden müssen: - -```php -public function loadConfiguration(): void -{ - // dieser Tag wird immer exportiert, auch wenn der Export eingeschränkt ist - $this->compiler->addExportedTag('event.subscriber'); - - // dieser Typ steht für getByType() immer zur Verfügung - $this->compiler->addExportedType(Nette\Database\Connection::class); -} -``` - -Beide Methoden ergänzen die exportierten Metadaten nur; sie überschreiben nie die Konfiguration `di › export` der Anwendung. Schränkt die Anwendung den Export also auf eine Liste ein, bleiben die Tags und Typen, die Ihre Extension braucht, enthalten; nur wenn der Export der Tags ganz abgeschaltet wird (`tags: false`), verschwinden sie zusammen mit allem anderen. diff --git a/dependency-injection/de/factory.texy b/dependency-injection/de/factory.texy deleted file mode 100644 index f82a10c1b8..0000000000 --- a/dependency-injection/de/factory.texy +++ /dev/null @@ -1,233 +0,0 @@ -Generierte Factories -******************** - -.[perex] -Nette DI kann den Code von Factories automatisch anhand von Interfaces erzeugen und erspart Ihnen damit das Schreiben von Code. - -Eine Factory ist eine Klasse, die dafür zuständig ist, Objekte zu erzeugen und ihnen ihre Abhängigkeiten zu übergeben. Bitte verwechseln Sie das nicht mit dem Entwurfsmuster *Factory Method*, das eine bestimmte Art beschreibt, Factories zu nutzen, und mit diesem Thema nichts zu tun hat. - -Wie eine solche Factory aussieht, haben wir im [einführenden Kapitel |introduction#Factory] gezeigt: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Nette DI kann den Code einer Factory automatisch erzeugen. Sie müssen nur ein Interface anlegen, und Nette DI erzeugt die Implementierung. Das Interface muss genau eine Methode namens `create` haben und einen Rückgabetyp deklarieren: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Die Factory `ArticleFactory` hat also eine Methode `create`, die `Article`-Objekte erzeugt. Die Klasse `Article` könnte zum Beispiel so aussehen: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } -} -``` - -Fügen Sie die Factory der Konfigurationsdatei hinzu: - -```neon -services: - - ArticleFactory -``` - -Nette DI erzeugt die passende Implementierung der Factory. - -In dem Code, der die Factory verwendet, fordern Sie das Objekt über sein Interface an, und Nette DI liefert die erzeugte Implementierung: - -```php -class UserController -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function foo() - { - // die Factory das Objekt erzeugen lassen - $article = $this->articleFactory->create(); - } -} -``` - - -Factory mit Parametern -====================== - -Die Methode `create` der Factory kann Parameter entgegennehmen, die sie dann an den Konstruktor weiterreicht. Ergänzen wir die Klasse `Article` zum Beispiel um die ID des Autors des Artikels: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - private int $authorId, - ) { - } -} -``` - -Den Parameter ergänzen wir auch in der Factory: - -```php -interface ArticleFactory -{ - function create(int $authorId): Article; -} -``` - -Weil der Name des Parameters im Konstruktor (`$authorId`) mit dem Namen des Parameters in der Methode der Factory übereinstimmt, übergibt Nette DI ihn automatisch. - - -Erweiterte Definition -===================== - -Die Definition lässt sich über den Schlüssel `implement` auch mehrzeilig schreiben: - -```neon -services: - articleFactory: - implement: ArticleFactory -``` - -Diese längere Schreibweise erlaubt es, über den Schlüssel `arguments` weitere Argumente für den Konstruktor anzugeben und über `setup` weiter zu konfigurieren, ganz wie bei gewöhnlichen Service-Definitionen. - -Ein Beispiel: Nähme die Methode `create()` den Parameter `$authorId` nicht entgegen, könnten wir in der Konfiguration einen festen Wert angeben, der dem Konstruktor von `Article` übergeben wird: - -```neon -services: - articleFactory: - implement: ArticleFactory - arguments: - authorId: 123 -``` - -Nähme `create()` umgekehrt `$authorId` entgegen, wäre der Wert aber nicht Teil des Konstruktors, sondern würde über eine Methode wie `Article::setAuthorId()` übergeben, verweisen wir im Abschnitt `setup` auf den Parameter: - -```neon -services: - articleFactory: - implement: ArticleFactory - setup: - - setAuthorId($authorId) -``` - - -Accessor -======== - -Neben Factories kann Nette auch sogenannte Accessors erzeugen. Das sind Objekte mit einer Methode `get()`, die einen bestimmten Service aus dem DI-Container zurückgibt. Wiederholte Aufrufe von `get()` liefern immer dieselbe Instanz. - -Accessors bieten Lazy Loading für Abhängigkeiten. Stellen Sie sich eine Klasse vor, die Fehler in eine eigene Datenbank protokolliert. Bekäme diese Klasse die Datenbankverbindung über Dependency Injection im Konstruktor, würde die Verbindung immer aufgebaut, auch wenn Fehler selten auftreten und die Verbindung die meiste Zeit ungenutzt bleibt. Stattdessen kann die Klasse einen Accessor bekommen. Das Objekt der Datenbank (die Verbindung) entsteht erst dann, wenn die Methode `get()` des Accessors zum ersten Mal aufgerufen wird. - -Wie erzeugt man einen Accessor? Schreiben Sie einfach ein Interface, und Nette DI erzeugt die Implementierung. Das Interface muss genau eine Methode namens `get` haben, die keine Parameter entgegennimmt und den Rückgabetyp deklariert: - -```php -interface PDOAccessor -{ - function get(): PDO; -} -``` - -Fügen Sie den Accessor der Konfigurationsdatei hinzu, zusammen mit der Definition des Services, den er zurückgeben soll: - -```neon -services: - - PDOAccessor - - PDO(%dsn%, %user%, %password%) -``` - -Weil der Accessor einen `PDO`-Service zurückgibt und in der Konfiguration nur ein einziger solcher Service definiert ist, gibt der Accessor genau diesen Service zurück. Gibt es mehrere Services dieses Typs, geben Sie über den Namen an, welchen der Accessor zurückgeben soll, etwa `- PDOAccessor(@db1)`. - - -Multifactory/Accessor -===================== - -Bisher konnten unsere Factories und Accessors nur einen einzigen Typ von Objekt erzeugen bzw. zurückgeben. Sie können aber leicht Multifactories bauen, die die Fähigkeiten von Factories und Accessors vereinen. Das Interface einer solchen Komponente kann mehrere Methoden namens `create<Name>()` und `get<Name>()` enthalten, zum Beispiel: - -```php -interface MultiFactory -{ - function createArticle(): Article; - function getDb(): PDO; -} -``` - -Statt mehrere einzelne Factories und Accessors zu injizieren, können Sie also eine einzige, umfassendere Komponente injizieren. - -Alternativ lässt sich statt mehrerer Methoden ein `get()` mit einem Parameter verwenden: - -```php -interface MultiFactoryAlt -{ - function get($name): PDO; -} -``` - -Dann tut `MultiFactory::getDb()` dasselbe wie `MultiFactoryAlt::get('db')`. Diese alternative Schreibweise hat allerdings den Nachteil, dass aus der Signatur des Interfaces nicht ausdrücklich hervorgeht, welche Werte für `$name` unterstützt werden. Außerdem lassen sich im Interface für verschiedene Werte von `$name` keine unterschiedlichen Rückgabetypen festlegen. - -Statt `get($name)` kann das Interface `create($name)` deklarieren, das bei jedem Aufruf eine neue Instanz zurückgibt (während `get()` eine gemeinsame liefert). Das Interface darf nur eine einzige solche Methode mit Parameter enthalten. Ist der Rückgabetyp der Methode nullable (etwa `?PDO`), gibt sie bei einem unbekannten `$name` statt einer Exception den Wert `null` zurück. - - -Definition über eine Liste --------------------------- -Eine Multifactory lässt sich in der Konfiguration über eine Liste definieren, wobei die Services inline geschrieben werden: .{data-version:3.2.0} - -```neon -services: - - MultiFactory( - article: Article() # definiert createArticle() - db: PDO(%dsn%, %user%, %password%) # definiert getDb() - ) -``` - -Alternativ können Sie in der Definition der Multifactory über Referenzen auf bestehende Services verweisen: - -```neon -services: - article: Article - - PDO(%dsn%, %user%, %password%) - - MultiFactory( - article: @article # definiert createArticle() - db: @\PDO # definiert getDb() - ) -``` - - -Definition über Tags --------------------- - -Eine weitere Möglichkeit, eine Multifactory zu definieren, sind [Tags |services#Tags]. Der Wert des Tags bestimmt den Namen der zugehörigen Methode: - -```neon -services: - article: - create: Article - tags: {multi: article} # definiert createArticle() - db: - create: PDO(%dsn%, %user%, %password%) - tags: {multi: db} # definiert getDb() - - - MultiFactory(tagged: multi) -``` diff --git a/dependency-injection/de/faq.texy b/dependency-injection/de/faq.texy deleted file mode 100644 index f71d386bbc..0000000000 --- a/dependency-injection/de/faq.texy +++ /dev/null @@ -1,123 +0,0 @@ -Häufig gestellte Fragen zu DI (FAQ) -*********************************** - - -Ist DI ein anderer Name für IoC? --------------------------------- - -*Inversion of Control* (IoC) ist ein Prinzip, das den Kontrollfluss in einem Programm beschreibt: Ruft Ihr Code fremden Code auf, oder ruft fremder Code (etwa ein Framework) Ihren Code auf? IoC ist ein weiter Begriff, der [Events |nette:glossary#Events], das sogenannte [Hollywood-Prinzip |application:components#Hollywood Style] und weitere Aspekte umfasst. Zu diesem Begriff gehören auch die Factories, die wir in [Regel Nr. 3: Überlassen Sie es der Factory |introduction#Regel Nr. 3: Überlassen Sie es der Factory] besprochen haben und die eine Umkehrung des Operators `new` darstellen. - -*Dependency Injection* (DI) dreht sich darum, wie Objekte ihre Abhängigkeiten bekommen (also die anderen Objekte, mit denen sie arbeiten müssen). Es ist ein Entwurfsmuster, das dafür wirbt, Objekten die Abhängigkeiten ausdrücklich zu übergeben, statt sie von den Objekten erzeugen oder suchen zu lassen. - -DI lässt sich deshalb als eine besondere Form von IoC verstehen. Nicht jede Form von IoC führt allerdings zu sauberem Code. Antipatterns sind zum Beispiel Techniken, die sich auf [globalen Zustand|global-state] oder auf das Muster [Service Locator |#Was ist ein Service Locator?] stützen. - - -Was ist ein Service Locator? ----------------------------- - -Es ist ein alternativer Ansatz zur Dependency Injection. Dabei gibt es ein zentrales Objekt (den Locator), bei dem alle verfügbaren Services (Abhängigkeiten) registriert sind. Braucht ein Objekt eine Abhängigkeit, fordert es sie beim Service Locator an. - -Gegenüber DI fehlt ihm allerdings die Transparenz. Die Abhängigkeiten stecken versteckt im Code des Objekts (in den Aufrufen des Locators), statt in seiner API (im Konstruktor oder in Methoden) ausdrücklich zu stehen, sodass man den Code lesen muss, um die Zusammenhänge zu verstehen. Auch das Testen ist aufwendiger, denn Sie können beim Erzeugen eines Objekts nicht einfach Mock-Abhängigkeiten übergeben, sondern müssen häufig den Service Locator selbst manipulieren. Außerdem führt der Service Locator eine unnötige Abhängigkeit ein: Die Objekte werden an den Locator gekoppelt, anders als bei DI, wo die Objekte im Idealfall nichts vom Container wissen. - - -Wann verwendet man DI besser nicht? ------------------------------------ - -Es sind keine nennenswerten Nachteile bekannt, wenn man das Entwurfsmuster Dependency Injection richtig einsetzt. Im Gegenteil führt es zu [zahlreichen Komplikationen|global-state], Abhängigkeiten aus global zugänglichen Orten zu holen (etwa aus statischen Properties oder Singletons), und ebenso, einen Service Locator zu verwenden. DI zu verwenden ist deshalb im Allgemeinen immer ratsam. Das ist kein Dogma; es hat sich schlicht keine bessere Alternative durchgesetzt, um Abhängigkeiten auf saubere Weise zu verwalten. - -Es gibt jedoch bestimmte, eng begrenzte Situationen, in denen der globale Zugriff auf Objekte vertretbar sein kann. Zum Beispiel beim Debuggen, wenn Sie den Wert einer Variablen ausgeben, die Laufzeit messen oder an einer bestimmten Stelle eine Meldung protokollieren müssen. In diesen Fällen, in denen es um vorübergehende Eingriffe geht, die später wieder aus dem Code verschwinden, kann ein global zugänglicher Dumper, Timer oder Logger legitim sein. Diese Werkzeuge sind nicht Teil des eigentlichen Entwurfs der Anwendung. - - -Hat DI Nachteile? ------------------ - -Bringt der Einsatz von Dependency Injection Nachteile mit sich, etwa mehr Schreibarbeit oder geringere Leistung? Was verlieren wir, wenn wir anfangen, Code nach den Prinzipien der DI zu schreiben? - -DI selbst hat vernachlässigbaren Einfluss auf die Laufzeit oder den Speicherverbrauch. Die Leistung des DI-Containers kann eine Rolle spielen, aber [Nette DI |nette-container] kompiliert den Container zu schlichtem PHP-Code, sodass beim Ausführen der Anwendung praktisch kein Mehraufwand entsteht. - -Wenn Sie Code nach den Prinzipien der DI schreiben, müssen Sie oft Konstruktoren anlegen, die Abhängigkeiten entgegennehmen. Was früher mühsam wirkte, geht mit modernen IDEs und Fähigkeiten wie der [Constructor Property Promotion |https://blog.nette.org/en/php-8-0-complete-overview-of-news#toc-constructor-property-promotion] von PHP 8 sehr schnell. Factories kann Nette DI oft automatisch erzeugen, was den Boilerplate-Code weiter verringert. Dafür entfällt das Schreiben von Singletons und statischen Accessors. - -Insgesamt ist eine gut entworfene Anwendung mit DI üblicherweise weder deutlich kürzer noch deutlich länger als eine, die sich auf Singletons oder globalen Zugriff stützt. Der Code für das Erzeugen und Verdrahten von Abhängigkeiten wandert einfach aus den einzelnen Klassen an dafür vorgesehene Orte: in die Konfiguration des DI-Containers und in die Factories. - - -Wie schreibt man eine Altanwendung auf DI um? ---------------------------------------------- - -Der Umstieg einer Altanwendung auf Dependency Injection kann ein anspruchsvoller Prozess sein, besonders bei großen und komplexen Anwendungen. Es ist wichtig, systematisch vorzugehen. - -- Beim Umstieg auf Dependency Injection ist es wichtig, dass alle Teammitglieder die verwendeten Prinzipien und Praktiken verstehen. -- Analysieren Sie zuerst die bestehende Anwendung, um die zentralen Komponenten und ihre Abhängigkeiten zu erkennen. Erstellen Sie einen Plan, welche Teile in welcher Reihenfolge umgebaut werden. -- Setzen Sie einen DI-Container um oder verwenden Sie besser eine bestehende Bibliothek wie Nette DI. -- Bauen Sie Teile der Anwendung nach und nach so um, dass sie Dependency Injection verwenden. Das kann bedeuten, Konstruktoren oder Methoden so zu ändern, dass sie Abhängigkeiten als Parameter entgegennehmen. -- Passen Sie den Code an den Stellen an, an denen Objekte erzeugt werden, sodass sie aus dem Container geholt oder über Factories des Containers erzeugt werden. - -Denken Sie daran, dass der Umstieg auf Dependency Injection eine Investition in die Qualität des Codes und die langfristige Wartbarkeit der Anwendung ist. Diese Änderungen vorzunehmen mag anspruchsvoll sein, das Ergebnis sollte aber saubererer, modularerer und leicht testbarer Code sein, der für künftige Erweiterungen und Wartung bereit ist. - - -Warum ist Komposition der Vererbung vorzuziehen? ------------------------------------------------- -Für die Wiederverwendung von Code ist [Komposition |nette:introduction-to-object-oriented-programming#Komposition] der [Vererbung |nette:introduction-to-object-oriented-programming#Vererbung] im Allgemeinen vorzuziehen, weil sie zu loserer Kopplung führt. Bei Komposition geraten Sie seltener in die Lage, dass eine Änderung an einer Basisklasse abhängige Unterklassen zerstört. Ein typisches Beispiel ist die Situation, die als [Constructor Hell |passing-dependencies#Constructor Hell] bekannt ist. - - -Lässt sich der Nette DI Container außerhalb von Nette verwenden? ----------------------------------------------------------------- - -Auf jeden Fall. Der Nette DI Container ist Teil von Nette, aber als eigenständige Bibliothek entworfen, die sich unabhängig von den übrigen Teilen des Frameworks verwenden lässt. Installieren Sie ihn einfach über Composer, legen Sie eine Konfigurationsdatei mit Ihren Services an und erzeugen Sie den DI-Container mit ein paar Zeilen PHP-Code. Und schon können Sie in Ihren Projekten von Dependency Injection profitieren. - -Das Kapitel über den [Nette DI Container |nette-container] beschreibt einen konkreten Anwendungsfall samt Codebeispielen. - - -Warum steht die Konfiguration in NEON-Dateien? ----------------------------------------------- - -NEON ist eine einfache und gut lesbare Konfigurationssprache, die innerhalb von Nette entstanden ist, um Anwendungen, Services und ihre Abhängigkeiten einzurichten. Gegenüber JSON oder YAML bietet sie dafür deutlich intuitivere und flexiblere Möglichkeiten. In NEON lassen sich Definitionen von Services und Beziehungen natürlich beschreiben, die sich in JSON oder YAML nur schwer oder gar nicht so klar ausdrücken ließen. - - -Bremst das Parsen von NEON-Dateien die Anwendung aus? ------------------------------------------------------ - -NEON-Dateien werden zwar sehr schnell geparst, ihre Parsing-Geschwindigkeit ist in der Produktion aber weitgehend belanglos. Die Konfigurationsdateien werden nämlich nur einmal geparst, wenn die Anwendung zum ersten Mal läuft (oder wenn sie sich ändern). Nach dem Parsen wird der Code des DI-Containers erzeugt, gecacht (auf der Festplatte abgelegt), und bei jedem weiteren Request läuft dieser kompilierte PHP-Code, sodass kein weiteres Parsen nötig ist. - -So funktioniert es in einer Produktionsumgebung. Während der Entwicklung werden die NEON-Dateien jedes Mal geparst, wenn sich ihr Inhalt ändert, sodass der Entwickler immer einen aktuellen DI-Container hat. Wie gesagt ist das Parsen selbst sehr schnell. - - -Wie greife ich in meiner Klasse auf die Parameter aus der Konfigurationsdatei zu? ---------------------------------------------------------------------------------- - -Denken Sie an [Regel Nr. 1: Lassen Sie es sich übergeben |introduction#Regel Nr. 1: Lassen Sie es sich übergeben]. Wenn eine Klasse eine Information aus der Konfigurationsdatei braucht, überlegen Sie nicht, wie sich die Klasse diese *holen* kann. Fordern Sie sie stattdessen einfach an, zum Beispiel über den Konstruktor der Klasse. Und geben Sie diesen Wert dann in der Konfigurationsdatei an. - -In diesem Beispiel ist `%myParameter%` ein Platzhalter für den Wert des Parameters `myParameter`, der dem Konstruktor von `MyClass` übergeben wird: - -```neon -# config.neon -parameters: - myParameter: Irgendein Wert - -services: - - MyClass(%myParameter%) -``` - -Wenn Sie mehrere Parameter übergeben oder Autowiring nutzen wollen, ist es sinnvoll, die [Parameter in ein Objekt zu packen |best-practices:passing-settings-to-presenters]. - - -Unterstützt Nette das Container-Interface PSR-11? -------------------------------------------------- - -Der [Nette DI Container |api:Nette\DI\Container] unterstützt PSR-11 nicht direkt. Wenn Sie aber Interoperabilität zwischen dem Nette DI Container und Bibliotheken oder Frameworks brauchen, die das PSR-11 Container Interface erwarten, können Sie einen [einfachen Adapter |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f] schreiben, der als Brücke zwischen dem Nette DI Container und PSR-11 dient. - - -Was bedeuten die Begriffe Container, Compiler, Definition und so weiter? ------------------------------------------------------------------------- - -Ein kurzes Wörterbuch der Begriffe, die rund um Nette DI immer wieder auftauchen, die meisten davon beim [Schreiben von Extensions |extensions]: - -- **Container** - das kompilierte Objekt (`Nette\DI\Container`), das Services bei Bedarf erzeugt und sie zur Laufzeit hält. Es wird einmal als optimierter PHP-Code erzeugt. -- **Compiler** - die Maschinerie, die Konfigurationsdateien und Extensions in diese Klasse des Containers verwandelt. -- **ContainerBuilder** - das veränderliche Modell des Containers, das während der Kompilierung verwendet wird; es hält die Definitionen der Services, bevor ein einziger echter Service existiert. Siehe [Extensions erstellen |extensions#ContainerBuilder]. -- **Service** - ein Objekt, das der Container verwaltet, üblicherweise einmal erzeugt und geteilt (ein Singleton) - eine Datenbankverbindung, ein Mailer, ein Logger. -- **Definition** - das Rezept für einen Service: sein Typ, wie er zu erzeugen ist und was danach zu tun ist. Nette verwandelt Definitionen in die Factory-Methoden des Containers; es gibt mehrere Arten (siehe [Typen von Definitionen |extensions#Typen von Definitionen]). -- **Typ** - die Klasse oder das Interface eines Services, anhand derer das Autowiring Services den Stellen zuordnet, die sie verlangen. -- **Autowiring** - das automatische Übergeben von Services an Konstruktoren und Methoden anhand ihres Typs, sodass Sie Abhängigkeiten nicht von Hand verdrahten. -- **Tag** - eine Markierung an einer Definition (wahlweise mit einem Wert); eine Extension kann dann alle Services mit dieser Markierung über `findByTag()` finden. -- **Setup** - zusätzliche Aufrufe, die unmittelbar nach dem Erzeugen auf einem Service ausgeführt werden - Methodenaufrufe oder Zuweisungen an Properties, ergänzt über `addSetup()`. -- **Alias** - ein alternativer Name für einen bestehenden Service. diff --git a/dependency-injection/de/global-state.texy b/dependency-injection/de/global-state.texy deleted file mode 100644 index e25543a757..0000000000 --- a/dependency-injection/de/global-state.texy +++ /dev/null @@ -1,294 +0,0 @@ -Globaler Zustand und Singletons -******************************* - -.[perex] -Achtung: Die folgenden Konstrukte sind Symptome schlecht entworfenen Codes: - -- `Foo::getInstance()` -- `DB::insert(...)` -- `Article::setDb($db)` -- `ClassName::$var` oder `static::$var` - -Kommt eines dieser Konstrukte in Ihrem Code vor? Dann haben Sie die Gelegenheit, ihn zu verbessern. Vielleicht denken Sie, das seien gängige Konstrukte, die man auch in Beispiellösungen verschiedener Bibliotheken und Frameworks sieht. Wenn ja, ist deren Code-Entwurf mangelhaft. - -Wir reden hier nicht von akademischer Reinheit. Alle diese Konstrukte haben eines gemeinsam: Sie nutzen globalen Zustand. Und globaler Zustand wirkt sich verheerend auf die Qualität des Codes aus. Klassen täuschen über ihre Abhängigkeiten hinweg. Der Code wird unvorhersehbar. Er verwirrt Entwickler und mindert ihre Effizienz. - -In diesem Kapitel erklären wir, warum das so ist und wie sich globaler Zustand vermeiden lässt. - - -Globale Verflechtung --------------------- - -In einer idealen Welt sollte ein Objekt nur mit Objekten kommunizieren, die ihm [direkt übergeben wurden |passing-dependencies]. Erzeuge ich zwei Objekte `A` und `B` und übergebe zwischen ihnen nie eine Referenz, kann weder `A` noch `B` auf den Zustand des anderen zugreifen oder ihn verändern. Das ist eine höchst wünschenswerte Eigenschaft von Code. Es ist wie mit einer Batterie und einer Glühbirne; die Birne leuchtet nicht, bis Sie sie mit einem Draht an die Batterie anschließen. - -Für globale (statische) Variablen oder Singletons gilt das jedoch nicht. Objekt `A` könnte *drahtlos* auf Objekt `C` zugreifen und es verändern, ohne dass eine Referenz übergeben wurde, indem es `C::changeSomething()` aufruft. Greift auch Objekt `B` auf das globale `C` zu, können sich `A` und `B` über `C` gegenseitig beeinflussen. - -Der Einsatz globaler Variablen führt eine neue Form der *drahtlosen* Kopplung ein, die von außen unsichtbar ist. Er erzeugt eine Nebelwand, die den Code schwerer verständlich und schwerer benutzbar macht. Um die Abhängigkeiten wirklich zu erfassen, müssen Entwickler jede Zeile des Quellcodes lesen, statt sich einfach auf die Interfaces der Klassen zu verlassen. Und diese Kopplung ist obendrein völlig unnötig. Globaler Zustand wird verwendet, weil er von überall leicht zugänglich ist und es zum Beispiel erlaubt, über eine globale (statische) Methode `DB::insert()` in die Datenbank zu schreiben. Wie wir aber zeigen werden, ist die vermeintliche Bequemlichkeit verschwindend klein gegenüber den schweren Komplikationen, die er mit sich bringt. - -.[note] -Was das Verhalten angeht, gibt es zwischen einer globalen und einer statischen Variablen keinen Unterschied. Sie sind gleichermaßen schädlich. - - -Die spukhafte Fernwirkung -------------------------- - -"Spukhafte Fernwirkung" - so nannte Albert Einstein bekanntlich ein Phänomen der Quantenphysik, das ihm nicht geheuer war. -Gemeint ist die Quantenverschränkung, bei der das Messen einer Eigenschaft eines Teilchens augenblicklich ein anderes, verschränktes Teilchen beeinflusst, ganz gleich, wie weit sie voneinander entfernt sind, und seien es Millionen Lichtjahre, was scheinbar das grundlegende Gesetz des Universums verletzt, dass sich nichts schneller als das Licht bewegen kann. - -In der Welt der Software beschreibt "spukhafte Fernwirkung" eine Situation, in der wir einen Vorgang ausführen, den wir für isoliert halten (weil keine Abhängigkeiten ausdrücklich übergeben wurden), und dennoch treten in entfernten Teilen des Systems unerwartete Wechselwirkungen und Zustandsänderungen auf, ohne dass wir davon wissen. Das kann nur über globalen Zustand geschehen. - -Stellen Sie sich vor, Sie stoßen zu einem Entwicklungsteam mit einer großen, gereiften Codebasis. Ihr neuer Vorgesetzter bittet Sie, eine neue Funktion umzusetzen, und wie ein guter Entwickler beginnen Sie damit, einen Test zu schreiben. Weil Sie im Projekt aber neu sind, machen Sie eine Menge erkundende Tests der Art "was passiert, wenn ich diese Methode aufrufe". Und Sie versuchen, den folgenden Test zu schreiben: - -```php -function testCreditCardCharge() -{ - $cc = new CreditCard('1234567890123456', 5, 2028); // Ihre Kartennummer - $cc->charge(100); -} -``` - -Sie führen den Code aus, vielleicht mehrmals, und nach einer Weile bemerken Sie Benachrichtigungen der Bank auf Ihrem Telefon: Bei jedem Durchlauf wurden Ihrer Kreditkarte 100 $ belastet! 🤦‍♂️ - -Wie um alles in der Welt konnte der Test eine echte Belastung auslösen? Mit einer Kreditkarte zu arbeiten ist nicht einfach. Man muss mit einem Webservice eines Dritten kommunizieren, dessen URL kennen, sich authentifizieren und so weiter. Nichts davon steht im Test. Schlimmer noch: Sie wissen nicht, wo diese Informationen liegen, und können deshalb die externen Abhängigkeiten nicht mocken, um die Belastung von 100 $ bei jedem Testlauf zu verhindern. Und woher hätten Sie als neuer Entwickler wissen sollen, dass das, was Sie vorhatten, Sie um 100 $ ärmer macht? - -Das ist eine spukhafte Fernwirkung! - -Sie sind gezwungen, umfangreichen Quellcode zu durchforsten und erfahrene Kollegen zu befragen, um die Verflechtungen des Projekts zu verstehen. Diese Schwierigkeit entsteht, weil das Interface der Klasse `CreditCard` die nötige Initialisierung des globalen Zustands nicht offenlegt. Selbst ein Blick in den Quellcode der Klasse verrät womöglich nicht, welche Initialisierungsmethode aufzurufen ist. Bestenfalls finden Sie die globale Variable, auf die zugegriffen wird, und versuchen daraus abzuleiten, wie sie zu initialisieren ist. - -Die Klassen in einem solchen Projekt sind pathologische Lügner. Die Klasse `CreditCard` tut so, als ließe sie sich einfach instanziieren und ihre Methode `charge()` aufrufen. In Wirklichkeit arbeitet sie heimlich mit einer anderen Klasse zusammen, `PaymentGateway`, die das Zahlungs-Gateway darstellt. Auch das Interface von `PaymentGateway` legt womöglich eine eigenständige Initialisierung nahe, in Wirklichkeit zieht es sich die Zugangsdaten aber vielleicht aus einer Konfigurationsdatei und so weiter. Die ursprünglichen Entwickler wissen, dass `CreditCard` das `PaymentGateway` braucht. Sie haben den Code so geschrieben. Für Neuankömmlinge ist es aber ein völliges Rätsel, das sie daran hindert, sich einzuarbeiten und wirksam beizutragen. - -Wie lässt sich die Lage in Ordnung bringen? Ganz leicht. **Lassen Sie die API die Abhängigkeiten deklarieren.** - -```php -function testCreditCardCharge() -{ - $gateway = new PaymentGateway(/* ... */); - $cc = new CreditCard('1234567890123456', 5, 2028); - $cc->charge($gateway, 100); -} -``` - -Beachten Sie, wie die gegenseitigen Abhängigkeiten im Code sofort sichtbar werden. Weil die Methode `charge()` deklariert, dass sie ein `PaymentGateway` braucht, müssen Sie diese Abhängigkeit nicht mehr erraten oder erfragen. Sie wissen, dass Sie eine Instanz erzeugen müssen, und dabei entdecken Sie die nötigen Zugangsparameter. Ohne sie liefe der Code nicht einmal. - -Und vor allem können Sie das Zahlungs-Gateway jetzt mocken, sodass Ihnen nicht bei jedem Testlauf 100 $ belastet werden. - -Globaler Zustand erlaubt es Objekten, heimlich auf Abhängigkeiten zuzugreifen, die in ihren APIs nicht deklariert sind, und macht Ihre APIs damit zu pathologischen Lügnern. - -Vielleicht haben Sie das bisher nicht so gesehen, aber immer wenn Sie globalen Zustand verwenden, schaffen Sie geheime drahtlose Kommunikationskanäle. Diese spukhafte Fernwirkung zwingt Entwickler dazu, jede Codezeile zu lesen, um mögliche Wechselwirkungen zu verstehen, senkt die Produktivität und verwirrt neue Teammitglieder. Wenn Sie derjenige sind, der den Code geschrieben hat, kennen Sie die wahren Abhängigkeiten, aber jeder, der nach Ihnen kommt, tappt im Dunkeln. - -Schreiben Sie keinen Code, der sich auf globalen Zustand stützt; übergeben Sie Abhängigkeiten lieber ausdrücklich. Machen Sie sich Dependency Injection zu eigen. - - -Zerbrechlichkeit des globalen Zustands --------------------------------------- - -In Code, der globalen Zustand und Singletons nutzt, können Sie nie sicher sein, wann und von wem der Zustand verändert wurde. Dieses Risiko zeigt sich schon bei der Initialisierung. Der folgende Code will eine Datenbankverbindung erzeugen und ein Zahlungs-Gateway initialisieren, wirft aber immer wieder Exceptions, und die Ursache zu finden ist äußerst mühsam: - -```php -PaymentGateway::init(); -DB::init('mysql:', 'user', 'password'); -``` - -Sie müssen den Code sorgfältig verfolgen, um herauszufinden, dass das Objekt `PaymentGateway` drahtlos auf andere Objekte zugreift, von denen einige eine Datenbankverbindung brauchen. Die Datenbank muss also vor `PaymentGateway` initialisiert werden. Die Nebelwand des globalen Zustands verbirgt das aber vor Ihnen. Wie viel Zeit ließe sich sparen, wenn die APIs dieser Klassen ehrlich wären und ihre Abhängigkeiten deklarierten? - -```php -$db = new DB('mysql:', 'user', 'password'); -$gateway = new PaymentGateway($db, /* ... */); -``` - -Ein ähnliches Problem entsteht beim globalen Zugriff auf eine Datenbankverbindung: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public function save(): void - { - DB::insert(/* ... */); - } -} -``` - -Beim Aufruf der Methode `save()` ist unklar, ob eine Datenbankverbindung aufgebaut wurde und wer dafür zuständig ist. Müssen wir die Datenbankverbindung dynamisch ändern (etwa für Tests), greifen wir womöglich zu Methoden wie `DB::reconnect(...)` oder `DB::reconnectForTest()`. - -Betrachten wir ein Beispiel: - -```php -$article = new Article; -// ... -DB::reconnectForTest(); -Foo::doSomething(); -$article->save(); -``` - -Wie können wir sicher sein, dass beim Aufruf von `$article->save()` tatsächlich die Testdatenbank verwendet wird? Was, wenn die Methode `Foo::doSomething()` die globale Datenbankverbindung geändert hat? Um das festzustellen, müssten wir den Quellcode von `Foo` und womöglich vieler weiterer Klassen prüfen. Diese Untersuchung gäbe uns nur eine vorübergehende Antwort, denn die Lage könnte sich später ändern. - -Was, wenn wir die Datenbankverbindung in eine statische Variable innerhalb der Klasse `Article` verschieben? - -```php -class Article -{ - private static DB $db; - - public static function setDb(DB $db): void - { - self::$db = $db; - } - - public function save(): void - { - self::$db->insert(/* ... */); - } -} -``` - -Das ändert überhaupt nichts. Das Problem ist der globale Zustand selbst, ganz gleich, in welcher Klasse er versteckt ist. In diesem Szenario haben wir wie im vorigen beim Aufruf von `$article->save()` keine Gewissheit, in welche Datenbank die Daten geschrieben werden. Jeder könnte irgendwo in der Anwendung jederzeit über `Article::setDb()` die Datenbank geändert haben. Ohne dass wir davon wissen. - -Der globale Zustand macht unsere Anwendung **äußerst zerbrechlich**. - -Es gibt jedoch einen einfachen Weg, mit diesem Problem umzugehen. Lassen Sie die API einfach die Abhängigkeiten deklarieren, die für die einwandfreie Funktion nötig sind. - -```php -class Article -{ - public function __construct( - private DB $db, - ) { - } - - public function save(): void - { - $this->db->insert(/* ... */); - } -} - -$article = new Article($db); -// ... -Foo::doSomething(); -$article->save(); -``` - -Dieser Ansatz nimmt die Sorge vor versteckten oder unerwarteten Änderungen der Datenbankverbindung. Wir wissen jetzt sicher, wohin der Artikel gespeichert wird, und Änderungen in unbeteiligten Klassen können daran nichts mehr ändern. Der Code ist nicht mehr zerbrechlich, sondern stabil. - -Schreiben Sie keinen Code, der sich auf globalen Zustand stützt; übergeben Sie Abhängigkeiten lieber ausdrücklich. Machen Sie sich Dependency Injection zu eigen. - - -Singleton ---------- - -Singleton ist ein Entwurfsmuster, das nach der [Definition |https://en.wikipedia.org/wiki/Singleton_pattern] aus der berühmten Veröffentlichung der Gang of Four eine Klasse auf eine einzige Instanz beschränkt und globalen Zugriff darauf bietet. Die Umsetzung dieses Musters sieht üblicherweise so aus: - -```php -class Singleton -{ - private static self $instance; - - public static function getInstance(): self - { - self::$instance ??= new self; - return self::$instance; - } - - // und weitere Methoden, die die Aufgaben der Klasse erfüllen -} -``` - -Leider führt das Singleton globalen Zustand in die Anwendung ein. Und wie wir oben gezeigt haben, ist globaler Zustand unerwünscht. Deshalb gilt das Singleton als Antipattern. - -Verwenden Sie in Ihrem Code keine Singletons und ersetzen Sie sie durch andere Mechanismen. Sie brauchen Singletons wirklich nicht. Wenn Sie aber sicherstellen müssen, dass es in der gesamten Anwendung nur eine einzige Instanz einer Klasse gibt, überlassen Sie diese Verantwortung dem [DI-Container |container]. So entsteht ein Singleton im Gültigkeitsbereich der Anwendung, das üblicherweise Service genannt wird. Die Klasse selbst ist damit davon befreit, ihre Einzigartigkeit zu verwalten (sie hat also weder eine Methode `getInstance()` noch eine statische Property für die Instanz) und kann sich ganz auf ihre Aufgaben konzentrieren. Damit verstößt sie nicht länger gegen das Prinzip der einzigen Verantwortung. - - -Globaler Zustand und Tests --------------------------- - -Beim Schreiben von Tests gehen wir im Idealfall davon aus, dass jeder Test eine isolierte Einheit ist, in die kein Zustand von außen hinein- und aus der keiner hinausgelangt. Nachdem ein Test durchgelaufen ist, sollte jeder mit ihm verbundene Zustand automatisch vom Garbage Collector aufgeräumt werden. Das macht die Tests isoliert. Deshalb können wir sie in beliebiger Reihenfolge ausführen. - -Sobald aber globaler Zustand oder Singletons im Spiel sind, zerfallen diese nützlichen Annahmen. Zustand kann in Tests hinein- und aus ihnen hinausdringen. Plötzlich kann die Reihenfolge der Tests eine Rolle spielen. - -Um Code mit Singletons überhaupt testen zu können, müssen Entwickler oft Abstriche an deren Unversehrtheit machen und zum Beispiel erlauben, die Instanz des Singletons zu ersetzen. Solche Lösungen sind bestenfalls Hacks, die zu Code führen, der schwer zu warten und zu verstehen ist. Jeder Test (oder seine Methode `tearDown()`), der globalen Zustand verändert, muss diese Änderungen sorgfältig zurücknehmen. - -Globaler Zustand ist das größte Kopfzerbrechen beim Unit-Testing! - -Wie bringt man das in Ordnung? Ganz einfach. Schreiben Sie keinen Code, der Singletons verwendet; übergeben Sie Abhängigkeiten lieber ausdrücklich. Machen Sie sich Dependency Injection zu eigen. - - -Globale Konstanten ------------------- - -Globaler Zustand beschränkt sich nicht auf Singletons und statische Variablen, er kann auch globale Konstanten betreffen. - -Konstanten, deren Werte universelle Wahrheiten darstellen (`M_PI`) oder in sich abgeschlossene Informationen liefern (`PREG_BACKTRACK_LIMIT_ERROR`), sind in der Regel unbedenklich. Umgekehrt sind Konstanten, die als Weg dienen, Informationen *drahtlos* in den Code zu schleusen, faktisch versteckte Abhängigkeiten. So wie `LOG_FILE` im folgenden Beispiel. Die Verwendung der Konstanten `FILE_APPEND` ist dagegen völlig richtig. - -```php -const LOG_FILE = '...'; - -class Foo -{ - public function doSomething() - { - // ... - file_put_contents(LOG_FILE, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Stattdessen sollten wir den Pfad zur Logdatei als Parameter im Konstruktor der Klasse `Foo` deklarieren und ihn damit zu einem ausdrücklichen Teil ihrer API machen: - -```php -class Foo -{ - public function __construct( - private string $logFile, - ) { - } - - public function doSomething() - { - // ... - file_put_contents($this->logFile, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Jetzt übergeben wir den Pfad zur Logdatei ausdrücklich. Wir können ihn nach Bedarf leicht ändern, was das Testen und die Wartung des Codes vereinfacht. - - -Globale Funktionen und statische Methoden ------------------------------------------ - -Wir möchten betonen, dass die Verwendung statischer Methoden und globaler Funktionen an sich kein Problem ist. Wir haben die Schwierigkeiten mit Methoden wie `DB::insert()` erklärt, aber das eigentliche Problem war immer der dahinterliegende globale Zustand, üblicherweise in einer statischen Variablen. Die Methode `DB::insert()` verlässt sich auf eine statische Variable, die die Datenbankverbindung hält. Ohne diese Variable ließe sich die Methode gar nicht umsetzen. - -Deterministische statische Methoden und Funktionen wie `Closure::fromCallable()`, `strlen()` und viele andere zu verwenden, ist mit Dependency Injection vollkommen vereinbar. Diese Funktionen sind vorhersehbar, weil sie für dieselben Eingabeparameter immer dasselbe Ergebnis liefern. Sie verwenden keinerlei globalen Zustand. - -In PHP gibt es allerdings Funktionen, die nicht deterministisch sind. Dazu zählt zum Beispiel die Funktion `htmlspecialchars()`. Ihr dritter Parameter `$encoding` nimmt, wenn er weggelassen wird, standardmäßig den Wert der Konfigurationsoption `default_charset` an (`ini_get('default_charset')`). Es empfiehlt sich deshalb, diesen Parameter immer anzugeben, um mögliches unvorhersehbares Verhalten zu vermeiden. Nette macht das durchgängig. - -Manche Funktionen wie `strtolower()` und `strtoupper()` verhielten sich in der jüngeren Vergangenheit nicht deterministisch, sondern abhängig von der Einstellung des Locale (`setlocale()`). Das führte zu vielen Komplikationen, am häufigsten bei der Arbeit mit der türkischen Sprache. Das Türkische unterscheidet nämlich sowohl in Klein- als auch in Großbuchstaben zwischen dem 'I' mit und ohne Punkt. Folglich gab `strtolower('I')` den Wert `ı` zurück (kleines i ohne Punkt) und `strtoupper('i')` den Wert `İ` (großes I mit Punkt), was zu zahlreichen rätselhaften Fehlern in Anwendungen führte. Dieses Problem wurde in PHP 8.2 behoben, die Funktionen hängen nicht mehr vom Locale ab. - -Das ist ein gutes Beispiel dafür, wie globaler Zustand (die Einstellung des Locale) Tausende Entwickler weltweit geplagt hat. Die endgültige Lösung bestand darin, die Funktionen unabhängig vom Locale zu machen und damit die versteckte Abhängigkeit zu entfernen. - - -Wann lässt sich globaler Zustand verwenden? -------------------------------------------- - -Es gibt bestimmte, eng begrenzte Situationen, in denen die Verwendung von globalem Zustand vertretbar ist. Zum Beispiel beim Debuggen, wenn Sie den Wert einer Variablen ausgeben oder die Laufzeit eines bestimmten Codeabschnitts messen müssen. In diesen Fällen, in denen es um vorübergehende Eingriffe geht, die später wieder aus dem Code verschwinden, kann ein global zugänglicher Dumper oder Timer legitim sein. Diese Werkzeuge sind nicht Teil des eigentlichen Entwurfs der Anwendung. - -Ein weiteres Beispiel sind die Funktionen von PHP für reguläre Ausdrücke (`preg_*`), die kompilierte reguläre Ausdrücke intern in einem statischen Speicher zwischenspeichern. Wenn Sie die Funktionen an mehreren Stellen Ihres Codes mit demselben regulären Ausdruck aufrufen, wird der Ausdruck nur einmal kompiliert. Dieses Caching verbessert die Leistung und ist für den Nutzer vollständig unsichtbar, weshalb diese Verwendung von internem statischem Zustand allgemein akzeptabel ist. - - -Zusammenfassung ---------------- - -Wir haben besprochen, warum es sinnvoll ist: - -1) alle veränderlichen statischen Properties (globalen Zustand) aus Ihrem Code zu entfernen -2) Abhängigkeiten ausdrücklich zu deklarieren -3) und Dependency Injection zu nutzen - -Denken Sie beim Entwurf Ihres Codes daran, dass jedes veränderliche `static $foo` eine mögliche Quelle von Problemen ist. Um eine DI-freundliche Umgebung zu schaffen, ist es entscheidend, globalen Zustand vollständig zu beseitigen und durch Dependency Injection zu ersetzen. - -Dabei stellen Sie vielleicht fest, dass Sie Klassen mit mehreren Verantwortlichkeiten aufteilen müssen. Zögern Sie nicht; streben Sie das Single Responsibility Principle an. - -*Ich möchte Miško Hevery danken, dessen Artikel wie [Flaw: Brittle Global State & Singletons |https://web.archive.org/web/20230321084133/http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/] die Grundlage dieses Kapitels bilden.* diff --git a/dependency-injection/de/introduction.texy b/dependency-injection/de/introduction.texy deleted file mode 100644 index 464080acaa..0000000000 --- a/dependency-injection/de/introduction.texy +++ /dev/null @@ -1,525 +0,0 @@ -Was ist Dependency Injection? -***************************** - -.[perex] -Dieses Kapitel stellt die grundlegenden Programmierpraktiken vor, an die Sie sich beim Schreiben jeder Anwendung halten sollten. Es sind die Grundlagen, die für sauberen, verständlichen und wartbaren Code nötig sind. - -Wenn Sie diese Regeln übernehmen und befolgen, wird Nette Sie auf Schritt und Tritt unterstützen. Es erledigt die Routinearbeit für Sie und bietet größtmöglichen Komfort, sodass Sie sich auf die eigentliche Logik konzentrieren können. - -Die Prinzipien, die wir hier zeigen, sind ziemlich einfach. Es gibt nichts zu befürchten. - - -Erinnern Sie sich an Ihr erstes Programm? ------------------------------------------ - -Wir wissen nicht, in welcher Sprache Sie es geschrieben haben, aber wenn es PHP war, sah es wahrscheinlich ungefähr so aus: - -```php -function addition(float $a, float $b): float -{ - return $a + $b; -} - -echo addition(23, 1); // gibt 24 aus -``` - -Ein paar triviale Zeilen Code, und doch stecken darin so viele zentrale Konzepte. Dass es Variablen gibt. Dass Code in kleinere Einheiten wie Funktionen unterteilt wird. Dass wir ihnen Eingabeargumente übergeben und sie Ergebnisse zurückgeben. Es fehlen nur Bedingungen und Schleifen. - -Dass wir einer Funktion Eingabedaten übergeben und sie ein Ergebnis zurückgibt, ist ein völlig verständliches Konzept, das auch in anderen Bereichen verwendet wird, etwa in der Mathematik. - -Eine Funktion hat ihre Signatur, die aus ihrem Namen, der Liste der Parameter samt Typen und schließlich dem Typ des Rückgabewerts besteht. Als Nutzer interessiert uns die Signatur; über die innere Implementierung müssen wir üblicherweise nichts wissen. - -Stellen Sie sich nun vor, die Signatur der Funktion sähe so aus: - -```php -function addition(float $x): float -``` - -Eine Addition mit einem Parameter? Das ist seltsam… Und was ist damit? - -```php -function addition(): float -``` - -Das ist jetzt wirklich seltsam, oder? Wie wird die Funktion überhaupt verwendet? - -```php -echo addition(); // was gibt sie aus? -``` - -Bei solchem Code wären wir ratlos. Nicht nur ein Anfänger würde ihn nicht verstehen, sondern auch ein erfahrener Programmierer nicht. - -Sie fragen sich, wie so eine Funktion innen wohl aussieht? Woher bekäme sie die Zahlen, die sie addieren soll? Wahrscheinlich würde sie sie sich *irgendwie* selbst beschaffen, etwa so: - -```php -function addition(): float -{ - $a = Input::get('a'); - $b = Input::get('b'); - return $a + $b; -} -``` - -Im Körper der Funktion haben wir versteckte Abhängigkeiten von anderen globalen Funktionen oder statischen Methoden entdeckt. Um herauszufinden, woher die Zahlen tatsächlich kommen, müssen wir weiter nachforschen. - - -So nicht! ---------- - -Der eben gezeigte Entwurf ist der Inbegriff vieler negativer Eigenschaften: - -- Die Signatur der Funktion tat so, als bräuchte sie die zu addierenden Zahlen nicht, was uns verwirrt hat. -- Wir haben keine Ahnung, wie wir die Funktion dazu bringen, zwei andere Zahlen zu addieren. -- Wir mussten in den Code schauen, um herauszufinden, woher sie die Zahlen nimmt. -- Wir haben versteckte Abhängigkeiten entdeckt. -- Um alles zu verstehen, müssen wir auch diese Abhängigkeiten untersuchen. - -Und ist es überhaupt die Aufgabe einer Additionsfunktion, sich die Eingaben zu beschaffen? Natürlich nicht. Ihre Verantwortung ist nur die Addition selbst. - - -Solchem Code wollen wir nicht begegnen, und schon gar nicht wollen wir ihn schreiben. Die Abhilfe ist einfach: zurück zu den Grundlagen und einfach Parameter verwenden: - - -```php -function addition(float $a, float $b): float -{ - return $a + $b; -} -``` - - -Regel Nr. 1: Lassen Sie es sich übergeben ------------------------------------------ - -Die wichtigste Regel lautet: **Alle Daten, die Funktionen oder Klassen brauchen, müssen ihnen übergeben werden**. - -Statt versteckte Wege zu erfinden, auf denen sie sich die Daten selbst beschaffen, geben Sie ihnen einfach die Parameter. Sie sparen sich die Zeit, versteckte Pfade zu ersinnen, die Ihren Code ganz sicher nicht besser machen. - -Wenn Sie diese Regel immer und überall befolgen, sind Sie auf dem Weg zu Code ohne versteckte Abhängigkeiten. Zu Code, der nicht nur für den Autor verständlich ist, sondern auch für jeden, der ihn später liest. Wo alles aus den Signaturen der Funktionen und Klassen hervorgeht und man keine versteckten Details in der Implementierung suchen muss. - -Diese Technik heißt fachlich **Dependency Injection**. Und die Daten heißen **Abhängigkeiten**. Es ist bloß gewöhnliches Übergeben von Parametern, mehr nicht. - -.[note] -Bitte verwechseln Sie Dependency Injection, ein Entwurfsmuster, nicht mit einem "Dependency-Injection-Container", der ein Werkzeug ist, also etwas grundlegend anderes. Über Container sprechen wir später. - - -Von Funktionen zu Klassen -------------------------- - -Und wie überträgt sich das auf Klassen? Eine Klasse ist ein komplexeres Gebilde als eine einfache Funktion, aber Regel Nr. 1 gilt auch hier voll und ganz. Es gibt nur [mehr Möglichkeiten, Argumente zu übergeben |passing-dependencies]. Zum Beispiel ganz ähnlich wie bei der Funktion: - -```php -class Math -{ - public function sum(float $a, float $b): float - { - return $a + $b; - } -} - -$math = new Math; -echo $math->sum(23, 1); // 24 -``` - -Oder über andere Methoden, oder direkt über den Konstruktor: - -```php -class Sum -{ - public function __construct( - private float $a, - private float $b, - ) { - } - - public function calculate(): float - { - return $this->a + $this->b; - } -} - -$sum = new Sum(23, 1); -echo $sum->calculate(); // 24 -``` - -Beide Beispiele entsprechen vollständig der Dependency Injection. - - -Beispiele aus dem echten Leben ------------------------------- - -In der realen Welt werden Sie keine Klassen zum Addieren von Zahlen schreiben. Gehen wir also zu praktischen Beispielen über. - -Nehmen wir eine Klasse `Article`, die einen Blogartikel repräsentiert: - -```php -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - // speichert den Artikel in der Datenbank - } -} -``` - -und die Verwendung sieht so aus: - -```php -$article = new Article; -$article->title = '10 Dinge, die Sie über das Abnehmen wissen müssen'; -$article->content = 'Jedes Jahr entscheiden sich Millionen Menschen ...'; -$article->save(); -``` - -Die Methode `save()` speichert den Artikel in einer Datenbanktabelle. Sie mit [Nette Database |database:] umzusetzen wäre unkompliziert, gäbe es da nicht einen Haken: Woher bekommt `Article` die Verbindung zur Datenbank, also ein Objekt der Klasse `Nette\Database\Connection`? - -Es scheint, als hätten wir viele Möglichkeiten. Sie könnte sie aus einer statischen Variablen nehmen. Oder durch Erben von einer Klasse, die die Datenbankverbindung bereitstellt. Oder ein [Singleton |global-state#Singleton] verwenden. Oder sogenannte Facades, wie sie Laravel nutzt: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - DB::insert( - 'INSERT INTO articles (title, content) VALUES (?, ?)', - [$this->title, $this->content], - ); - } -} -``` - -Prima, das Problem ist gelöst. - -Oder etwa nicht? - -Erinnern wir uns an [#Regel Nr. 1: Lassen Sie es sich übergeben]: Alle Abhängigkeiten, die eine Klasse braucht, müssen ihr übergeben werden. Denn wenn wir die Regel brechen, haben wir den Weg zu unübersichtlichem Code voller versteckter Abhängigkeiten eingeschlagen, und das Ergebnis wird eine Anwendung sein, deren Wartung und Weiterentwicklung eine Herausforderung ist. - -Der Nutzer der Klasse `Article` hat keine Ahnung, wo die Methode `save()` den Artikel ablegt. In einer Datenbanktabelle? In welcher, der Produktions- oder der Testdatenbank? Und wie lässt sich das ändern? - -Der Nutzer muss nachsehen, wie die Methode `save()` implementiert ist, und findet dort die Verwendung von `DB::insert()`. Er muss also weiter nachforschen, woher diese Methode die Datenbankverbindung bekommt. Und versteckte Abhängigkeiten können eine ziemlich lange Kette bilden. - -In sauberem und gut entworfenem Code gibt es niemals versteckte Abhängigkeiten, Laravel-Facades oder statische Variablen. In sauberem und gut entworfenem Code werden Argumente übergeben: - -```php -class Article -{ - public function save(Nette\Database\Connection $db): void - { - $db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -Noch praktischer ist, wie wir später sehen werden, die Verwendung des Konstruktors: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function save(): void - { - $this->db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -.[note] -Wenn Sie ein erfahrener Programmierer sind, denken Sie vielleicht, dass `Article` überhaupt keine Methode `save()` haben sollte; sie sollte eine reine Datenstruktur sein, und das Speichern sollte ein eigenes Repository übernehmen. Das ergibt Sinn. Es würde uns aber weit über das Thema hinausführen, nämlich Dependency Injection, und über das Ziel, einfache Beispiele zu zeigen. - -Wenn Sie eine Klasse schreiben, die für ihren Betrieb zum Beispiel eine Datenbank braucht, erfinden Sie nicht, woher Sie sie bekommen, sondern lassen Sie sie sich übergeben. Etwa als Parameter des Konstruktors oder einer anderen Methode. Bekennen Sie sich zu den Abhängigkeiten. Bekennen Sie sich in der API Ihrer Klasse dazu. Sie bekommen verständlichen und vorhersagbaren Code. - -Und was ist mit dieser Klasse, die Fehlermeldungen protokolliert? - -```php -class Logger -{ - public function log(string $message): void - { - $file = LOG_DIR . '/log.txt'; - file_put_contents($file, $message . "\n", FILE_APPEND); - } -} -``` - -Was meinen Sie, haben wir uns an [#Regel Nr. 1: Lassen Sie es sich übergeben] gehalten? - -Haben wir nicht. - -Die entscheidende Information, das Verzeichnis mit der Logdatei, *beschafft sich die Klasse selbst* aus einer Konstanten. - -Sehen Sie sich das Beispiel der Verwendung an: - -```php -$logger = new Logger; -$logger->log('Die Temperatur beträgt 23 °C'); -$logger->log('Die Temperatur beträgt 10 °C'); -``` - -Könnten Sie, ohne die Implementierung zu kennen, sagen, wohin die Meldungen geschrieben werden? Wäre Ihnen eingefallen, dass für den Betrieb die Konstante `LOG_DIR` existieren muss? Und könnten Sie eine zweite Instanz erzeugen, die woandershin schreibt? Sicher nicht. - -Bringen wir die Klasse in Ordnung: - -```php -class Logger -{ - public function __construct( - private string $file, - ) { - } - - public function log(string $message): void - { - file_put_contents($this->file, $message . "\n", FILE_APPEND); - } -} -``` - -Die Klasse ist jetzt viel verständlicher, konfigurierbarer und damit nützlicher. - -```php -$logger = new Logger('/path/to/log.txt'); -$logger->log('Die Temperatur beträgt 15 °C'); -``` - - -Aber das interessiert mich nicht! ---------------------------------- - -*"Wenn ich ein Article-Objekt erzeuge und save() aufrufe, will ich mich nicht mit der Datenbank befassen; ich will nur, dass es in der gespeichert wird, die ich konfiguriert habe."* - -*"Wenn ich den Logger verwende, will ich nur, dass die Meldung geschrieben wird, und mich nicht damit befassen, wohin. Es soll die globale Einstellung gelten."* - -Das sind berechtigte Einwände. - -Zeigen wir als Beispiel eine Klasse, die Newsletter verteilt und das Ergebnis protokolliert: - -```php -class NewsletterDistributor -{ - public function distribute(): void - { - $logger = new Logger(/* ... */); - try { - $this->sendEmails(); - $logger->log('Die E-Mails wurden versendet'); - - } catch (Exception $e) { - $logger->log('Beim Versenden ist ein Fehler aufgetreten'); - throw $e; - } - } -} -``` - -Der verbesserte `Logger`, der die Konstante `LOG_DIR` nicht mehr verwendet, verlangt im Konstruktor den Pfad zur Datei. Wie lässt sich das lösen? Die Klasse `NewsletterDistributor` kümmert sich nicht darum, wohin die Meldungen geschrieben werden; sie will sie bloß protokollieren. - -Die Lösung ist wieder [#Regel Nr. 1: Lassen Sie es sich übergeben]: Wir übergeben alle Daten, die die Klasse braucht. - -Heißt das also, dass wir den Pfad zum Log durch den Konstruktor übergeben und ihn dann beim Erzeugen des `Logger`-Objekts verwenden? - -```php -class NewsletterDistributor -{ - public function __construct( - private string $file, // ⛔ NICHT SO! - ) { - } - - public function distribute(): void - { - $logger = new Logger($this->file); -``` - -Nicht so! Denn der Pfad ist **nicht** ein Datum, das die Klasse `NewsletterDistributor` braucht; ihn braucht der `Logger`. Sehen Sie den Unterschied? Die Klasse `NewsletterDistributor` braucht den Logger selbst. Also übergeben wir den Logger selbst: - -```php -class NewsletterDistributor -{ - public function __construct( - private Logger $logger, // ✅ - ) { - } - - public function distribute(): void - { - try { - $this->sendEmails(); - $this->logger->log('Die E-Mails wurden versendet'); - - } catch (Exception $e) { - $this->logger->log('Beim Versenden ist ein Fehler aufgetreten'); - throw $e; - } - } -} -``` - -Jetzt geht aus der Signatur der Klasse `NewsletterDistributor` hervor, dass das Protokollieren Teil ihrer Aufgabe ist. Und den Logger durch einen anderen zu ersetzen, etwa für Tests, ist völlig unkompliziert. Sollte sich außerdem der Konstruktor der Klasse `Logger` ändern, hat das auf unsere Klasse keinerlei Auswirkung. - - -Regel Nr. 2: Nehmen Sie, was Ihnen gehört ------------------------------------------ - -Lassen Sie sich nicht verwirren und übernehmen Sie nicht die Abhängigkeiten Ihrer Abhängigkeiten. Nehmen Sie nur Ihre eigenen. - -Dadurch ist Code, der andere Objekte verwendet, völlig unabhängig von Änderungen an deren Konstruktoren. Seine API wird genauer. Und vor allem lassen sich diese Abhängigkeiten mühelos durch andere ersetzen. - - -Neues Familienmitglied ----------------------- - -Das Entwicklungsteam hat beschlossen, einen zweiten Logger zu bauen, der in die Datenbank schreibt. Wir erzeugen also eine Klasse `DatabaseLogger`. Jetzt haben wir zwei Klassen, `Logger` und `DatabaseLogger`; die eine schreibt in eine Datei, die andere in die Datenbank ... wirkt die Benennung nicht etwas seltsam? Wäre es nicht besser, `Logger` in `FileLogger` umzubenennen? Sicherlich. - -Aber machen wir es geschickt. Wir legen ein Interface mit dem ursprünglichen Namen an: - -```php -interface Logger -{ - function log(string $message): void; -} -``` - -… das beide Logger implementieren werden: - -```php -class FileLogger implements Logger -// ... - -class DatabaseLogger implements Logger -// ... -``` - -Und dadurch muss im übrigen Code, der den Logger verwendet, nichts geändert werden. Der Konstruktor der Klasse `NewsletterDistributor` etwa wird sich weiterhin damit begnügen, `Logger` als Parameter zu verlangen. Und es liegt an uns, welche Instanz wir ihm geben. - -**Deshalb hängen wir an Namen von Interfaces nie das Suffix `Interface` und stellen ihnen nie das Präfix `I` voran.** Sonst ließe sich der Code nicht so elegant erweitern. - - -Houston, wir haben ein Problem ------------------------------- - -Während wir in der gesamten Anwendung mit einer einzigen Instanz des Loggers auskommen, ob dateibasiert oder datenbankbasiert, und sie einfach überall dorthin übergeben, wo protokolliert wird, sieht es bei der Klasse `Article` ganz anders aus. Ihre Instanzen erzeugen wir nach Bedarf, auch mehrfach. Wie gehen wir mit der Datenbank-Abhängigkeit in ihrem Konstruktor um? - -Ein Beispiel könnte ein Controller sein, der nach dem Absenden eines Formulars einen Artikel in der Datenbank speichern soll: - -```php -class EditController extends Controller -{ - public function formSubmitted($data) - { - $article = new Article(/* ... */); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Eine mögliche Lösung scheint auf der Hand zu liegen: Lassen wir das Datenbankobjekt über den Konstruktor an `EditController` übergeben und verwenden `$article = new Article($this->db)`. - -Genau wie im vorigen Fall mit `Logger` und dem Pfad zur Datei ist das nicht der richtige Weg. Die Datenbank ist keine Abhängigkeit von `EditController`, sondern von `Article`. Sie zu übergeben verstößt also gegen [Regel Nr. 2: Nehmen Sie, was Ihnen gehört |#Regel Nr. 2: Nehmen Sie, was Ihnen gehört]. Ändert sich der Konstruktor der Klasse `Article` (kommt ein neuer Parameter hinzu), müssen Sie den Code an allen Stellen anpassen, an denen Instanzen erzeugt werden. Uff. - -Houston, was schlagen Sie vor? - - -Regel Nr. 3: Überlassen Sie es der Factory ------------------------------------------- - -Indem wir versteckte Abhängigkeiten beseitigt und alle Abhängigkeiten als Argumente übergeben haben, haben wir besser konfigurierbare und flexiblere Klassen gewonnen. Deshalb brauchen wir noch etwas, das uns diese flexibleren Klassen erzeugt und konfiguriert. Wir nennen es Factories. - -Die Regel lautet: Hat eine Klasse Abhängigkeiten, überlassen Sie das Erzeugen ihrer Instanzen einer Factory. - -Factories sind in der Welt der Dependency Injection die klügere Alternative zum Operator `new`. - -.[note] -Bitte verwechseln Sie das nicht mit dem Entwurfsmuster *Factory Method*, das eine bestimmte Art beschreibt, Factories zu nutzen, und mit diesem Thema nichts zu tun hat. - - -Factory -------- - -Eine Factory ist eine Methode oder Klasse, die Objekte erzeugt und konfiguriert. Die Klasse, die `Article` herstellt, nennen wir `ArticleFactory`, und sie könnte so aussehen: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Ihre Verwendung im Controller sieht so aus: - -```php -class EditController extends Controller -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function formSubmitted($data) - { - // die Factory das Objekt erzeugen lassen - $article = $this->articleFactory->create(); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Ändert sich nun die Signatur des Konstruktors der Klasse `Article`, ist die `ArticleFactory` der einzige Teil des Codes, der darauf reagieren muss. Aller übrige Code, der mit `Article`-Objekten arbeitet, etwa `EditController`, bleibt davon unberührt. - -Vielleicht kratzen Sie sich jetzt am Kopf und fragen sich, ob wir die Lage überhaupt verbessert haben. Die Menge an Code ist gewachsen, und das Ganze fängt an, verdächtig kompliziert auszusehen. - -Keine Sorge, wir kommen bald zum Nette DI Container. Und der hat einige Tricks auf Lager, die das Bauen von Anwendungen mit Dependency Injection erheblich vereinfachen. Statt der Klasse `ArticleFactory` genügt dann zum Beispiel, [bloß ein Interface zu schreiben |factory]: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Aber wir greifen vor, bleiben Sie dran :-) - - -Zusammenfassung ---------------- - -Am Anfang dieses Kapitels haben wir versprochen, ein Vorgehen für den Entwurf sauberen Codes zu zeigen. Sorgen Sie einfach dafür, dass Klassen: - -1) [die Abhängigkeiten übergeben bekommen, die sie brauchen |#Regel Nr. 1: Lassen Sie es sich übergeben] -2) [und umgekehrt nicht das übergeben bekommen, was sie nicht unmittelbar brauchen |#Regel Nr. 2: Nehmen Sie, was Ihnen gehört] -3) [und dass Objekte mit Abhängigkeiten am besten in Factories erzeugt werden |#Regel Nr. 3: Überlassen Sie es der Factory] - -Auf den ersten Blick mag es nicht so wirken, aber diese drei Regeln haben weitreichende Folgen. Sie führen zu einer radikal anderen Sicht auf den Entwurf von Code. Lohnt sich das? Programmierer, die alte Gewohnheiten abgelegt und angefangen haben, Dependency Injection konsequent zu nutzen, halten diesen Schritt für einen Wendepunkt in ihrer beruflichen Laufbahn. Er hat ihnen eine Welt übersichtlicher und wartbarer Anwendungen eröffnet. - -Und was, wenn der Code Dependency Injection nicht konsequent nutzt? Was, wenn er auf statischen Methoden oder Singletons aufbaut? Führt das zu Problemen? [Ja, und zwar zu sehr erheblichen |global-state]. diff --git a/dependency-injection/de/nette-container.texy b/dependency-injection/de/nette-container.texy deleted file mode 100644 index 5b0d095ae8..0000000000 --- a/dependency-injection/de/nette-container.texy +++ /dev/null @@ -1,117 +0,0 @@ -Nette DI Container -****************** - -.[perex] -Nette DI ist eine der interessantesten Bibliotheken von Nette. Sie kann kompilierte DI-Container erzeugen und automatisch aktualisieren, die außerordentlich schnell und bemerkenswert einfach zu konfigurieren sind. - -Wie die Services aussehen sollen, die der DI-Container erzeugt, legt man üblicherweise über Konfigurationsdateien im [Format NEON|neon:format] fest. Der Container, den wir im [vorigen Kapitel|container] von Hand gebaut haben, würde so geschrieben: - -```neon -parameters: - db: - dsn: 'mysql:' - user: root - password: '***' - -services: - - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - - ArticleFactory - - EditController -``` - -Die Syntax ist sehr knapp. - -Alle Abhängigkeiten, die in den Konstruktoren der Klassen `ArticleFactory` und `EditController` deklariert sind, findet und übergibt Nette DI dank des sogenannten [Autowirings|autowiring] automatisch, in der Konfigurationsdatei muss also nichts angegeben werden. Selbst wenn sich die Parameter ändern, müssen Sie an der Konfiguration nichts ändern. Während der Entwicklung erzeugt Nette den Container automatisch neu. Sie können sich ganz auf die Entwicklung der Anwendung konzentrieren. - -Wollen wir Abhängigkeiten über Setter übergeben, verwenden wir dafür den Abschnitt [setup |services#Setup]. - -Nette DI erzeugt den PHP-Code des Containers direkt. Das Ergebnis ist also eine `.php`-Datei, die Sie öffnen und untersuchen können. So sehen Sie genau, wie der Container funktioniert. Sie können ihn auch in Ihrer IDE debuggen und Schritt für Schritt durchgehen. Und vor allem: Der erzeugte PHP-Code ist außerordentlich schnell. - -Nette DI kann auch den Code einer [Factory|factory] anhand eines vorgegebenen Interfaces erzeugen. Statt der Klasse `ArticleFactory` müssen wir in der Anwendung also nur ein Interface anlegen: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Das vollständige Beispiel finden Sie [auf GitHub|https://github.com/nette-examples/di-example-doc]. - - -Eigenständige Verwendung ------------------------- - -Die Bibliothek Nette DI in eine Anwendung einzubinden ist sehr einfach. Zuerst installieren wir sie über Composer (denn das Herunterladen von ZIP-Dateien ist so von gestern): - -```shell -composer require nette/di -``` - -Der folgende Code verwendet den [Compiler |api:Nette\DI\Compiler], um anhand der Konfiguration in der Datei `config.neon` eine Instanz des DI-Containers zu erzeugen: - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp'); -$class = $loader->load(function ($compiler) { - $compiler->loadConfig(__DIR__ . '/config.neon'); -}); -$container = new $class; -``` - -Der Container wird nur einmal erzeugt, sein Code wird in den Cache geschrieben (in das Verzeichnis `__DIR__ . '/temp'`), und bei weiteren Requests wird er nur noch von dort geladen. - -Für sich allein schaltet der `Compiler` in der Konfiguration nur die Abschnitte `services` und `parameters` frei. Um weitere zu nutzen - etwa `search`, `decorator`, `di` oder `inject` -, registrieren Sie zuerst deren Extensions. Und damit sich Extensions aus dem Abschnitt `extensions` der Konfiguration registrieren lassen, ergänzen Sie die `ExtensionsExtension`: - -```php -$compiler->addExtension('search', new Nette\DI\Extensions\SearchExtension($tempDir)); -$compiler->addExtension('extensions', new Nette\DI\Extensions\ExtensionsExtension); -``` - -Der [Configurator |application:bootstrapping], der in vollständigen Nette-Anwendungen verwendet wird, registriert sie alle automatisch. - -Wenn Sie mehrere verschiedene Container im selben Cache-Verzeichnis halten, unterscheiden Sie sie über einen Schlüssel, den Sie `load()` als zweites Argument übergeben; er wird Teil des Namens der erzeugten Klasse: - -```php -$class = $loader->load( - fn($compiler) => $compiler->loadConfig(__DIR__ . '/config.neon'), - 'my-key', -); -``` - -Zum Erzeugen und Holen von Services dienen die Methoden `getService()` oder `getByType()`. So erzeugen wir das Objekt `EditController`: - -```php -$controller = $container->getByType(EditController::class); -$controller->someMethod(); -``` - -Während der Entwicklung ist es nützlich, den Modus der automatischen Aktualisierung einzuschalten, in dem sich der Container automatisch neu erzeugt, sobald sich eine Klasse oder eine Konfigurationsdatei ändert. Geben Sie dazu im Konstruktor des [ContainerLoader |api:Nette\DI\ContainerLoader] als zweites Argument `true` an. - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp', true); -``` - - -Mit dem Container arbeiten --------------------------- - -Neben `getService()` und `getByType()` bietet das Objekt des Containers mehrere weitere nützliche Methoden: - -- `getByType(string $type, bool $throw = true): ?object` gibt den Service des angegebenen Typs zurück. Übergeben Sie als zweites Argument `false`, gibt sie `null` zurück, statt eine Exception zu werfen, wenn es keinen solchen Service gibt. -- `hasService(string $name): bool` und `isCreated(string $name): bool` sagen Ihnen, ob ein Service definiert ist und ob er bereits instanziiert wurde. -- `getParameters(): array` gibt alle Parameter des Containers zurück, `getParameter($key)` einen einzelnen. -- `createInstance(string $class, array $args = []): object` erzeugt eine neue Instanz der angegebenen Klasse und übergibt ihrem Konstruktor die Abhängigkeiten über Autowiring. -- `callMethod(callable $function, array $args = []): mixed` ruft das angegebene Callable auf und übergibt ihm die Argumente über Autowiring. -- `callInjects(object $service): void` ruft auf dem angegebenen Objekt alle Methoden `inject*()` auf und übergibt ihnen die Abhängigkeiten. - -Der Konstruktor des Containers nimmt außerdem ein Array von Parametern entgegen, die die in der Konfiguration definierten ergänzen: - -```php -$container = new $class(['host' => 'localhost']); -``` - - -Verwendung mit dem Nette Framework ----------------------------------- - -Wie wir gezeigt haben, ist die Verwendung von Nette DI nicht auf Anwendungen beschränkt, die auf dem Nette Framework aufbauen; Sie können es mit nur drei Zeilen Code überall einbinden. Entwickeln Sie Ihre Anwendungen jedoch mit dem Nette Framework, übernimmt die Konfiguration und das Erzeugen des Containers der [Bootstrap |application:bootstrapping#Konfiguration des DI-Containers]. diff --git a/dependency-injection/de/passing-dependencies.texy b/dependency-injection/de/passing-dependencies.texy deleted file mode 100644 index e504b71079..0000000000 --- a/dependency-injection/de/passing-dependencies.texy +++ /dev/null @@ -1,215 +0,0 @@ -Abhängigkeiten übergeben -************************ - -<div class=perex> - -Argumente, in der Terminologie der DI "Abhängigkeiten", lassen sich Klassen auf diese Hauptarten übergeben: - -* Übergabe im Konstruktor (Constructor Injection) -* Übergabe über eine Methode (sogenannte Setter Injection) -* Setzen einer Property (Property Injection) -* Über die Methode `inject*()` oder das Attribut `#[Inject]` - -</div> - -Zeigen wir jede Variante an konkreten Beispielen. - - -Übergabe im Konstruktor -======================= - -Die Abhängigkeiten werden beim Erzeugen des Objekts als Argumente des Konstruktors übergeben: - -```php -class MyClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -$obj = new MyClass($cache); -``` - -Dieser Weg eignet sich für zwingende Abhängigkeiten, die die Klasse für ihren Betrieb unbedingt braucht, denn ohne sie lässt sich die Instanz nicht erzeugen. - -Seit PHP 8.0 können wir eine kürzere Schreibweise verwenden ([Constructor Property Promotion |https://blog.nette.org/en/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), die funktional gleichwertig ist: - -```php -// PHP 8.0 -class MyClass -{ - public function __construct( - private Cache $cache, - ) { - } -} -``` - -Seit PHP 8.1 lässt sich eine Property mit dem Flag `readonly` kennzeichnen, das erklärt, dass sich der Wert der Property nach der Initialisierung nicht mehr ändert: - -```php -// PHP 8.1 -class MyClass -{ - public function __construct( - private readonly Cache $cache, - ) { - } -} -``` - -Der DI-Container übergibt dem Konstruktor die Abhängigkeiten automatisch über [Autowiring |autowiring]. Argumente, die sich so nicht übergeben lassen (etwa Strings, Zahlen, boolesche Werte), [gibt man in der Konfiguration an |services#Argumente]. - - -Constructor Hell ----------------- - -Der Begriff *Constructor Hell* beschreibt die Situation, dass eine Kindklasse von einer Elternklasse erbt, deren Konstruktor Abhängigkeiten verlangt, und die Kindklasse ebenfalls Abhängigkeiten verlangt. Sie muss dann auch die Abhängigkeiten des Elternteils entgegennehmen und weiterreichen: - -```php -abstract class BaseClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass extends BaseClass -{ - private Database $db; - - // ⛔ CONSTRUCTOR HELL - public function __construct(Cache $cache, Database $db) - { - parent::__construct($cache); - $this->db = $db; - } -} -``` - -Das Problem zeigt sich, wenn wir den Konstruktor von `BaseClass` ändern wollen, etwa weil eine neue Abhängigkeit hinzukommt. Dann müssen auch alle Konstruktoren der Kindklassen angepasst werden. Was eine solche Änderung zur Hölle macht. - -Wie lässt sich dem vorbeugen? Die Lösung ist, **[Komposition der Vererbung vorzuziehen |faq#Warum ist Komposition der Vererbung vorzuziehen?]**. - -Wir entwerfen den Code also anders. Wir verzichten auf [abstrakte |nette:introduction-to-object-oriented-programming#Abstrakte Klassen] `Base*`-Klassen. Statt dass `MyClass` bestimmte Fähigkeiten erbt, indem sie von `BaseClass` abgeleitet wird, bekommt sie diese Fähigkeiten als Abhängigkeit übergeben: - -```php -final class SomeFunctionality -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass -{ - private SomeFunctionality $sf; - private Database $db; - - public function __construct(SomeFunctionality $sf, Database $db) // ✅ - { - $this->sf = $sf; - $this->db = $db; - } -} -``` - - -Setter Injection -================ - -Die Abhängigkeiten werden übergeben, indem eine Methode aufgerufen wird, die sie in einer privaten Property ablegt. Diese Methoden werden üblicherweise nach dem Muster `set*()` benannt, weshalb man sie Setter nennt, sie können aber natürlich auch anders heißen. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - $this->cache = $cache; - } -} - -$obj = new MyClass; -$obj->setCache($cache); -``` - -Dieser Weg eignet sich für optionale Abhängigkeiten, die für den Betrieb der Klasse nicht unerlässlich sind, denn es ist nicht garantiert, dass das Objekt die Abhängigkeit tatsächlich bekommt (also dass der Aufrufer die Methode aufruft). - -Zugleich erlaubt dieser Weg, den Setter wiederholt aufzurufen und die Abhängigkeit zu ändern. Ist das unerwünscht, ergänzen Sie in der Methode eine Prüfung oder kennzeichnen Sie die Property `$cache` seit PHP 8.1 mit dem Flag `readonly`. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - if (isset($this->cache)) { - throw new RuntimeException('Die Abhängigkeit wurde bereits gesetzt'); - } - $this->cache = $cache; - } -} -``` - -Der Aufruf des Setters wird in der Konfiguration des DI-Containers im [Schlüssel setup |services#Setup] festgelegt. Auch hier werden die Abhängigkeiten automatisch über Autowiring übergeben: - -```neon -services: - - create: MyClass - setup: - - setCache -``` - - -Property Injection -================== - -Die Abhängigkeiten werden übergeben, indem direkt in eine Property geschrieben wird: - -```php -class MyClass -{ - public Cache $cache; -} - -$obj = new MyClass; -$obj->cache = $cache; -``` - -Dieser Weg gilt als ungeeignet, weil die Property als `public` deklariert werden muss. Dadurch verlieren wir die Kontrolle darüber, dass die übergebene Abhängigkeit tatsächlich den verlangten Typ hat (das galt besonders vor den Typdeklarationen für Properties in PHP 7.4), und wir verlieren die Möglichkeit, auf eine neu zugewiesene Abhängigkeit mit eigener Logik zu reagieren, etwa um eine spätere Änderung zu verhindern. Zugleich wird die Property Teil der öffentlichen API der Klasse, was womöglich nicht beabsichtigt ist. - -Die Zuweisung an die Property wird in der Konfiguration des DI-Containers im [Abschnitt setup |services#Setup] festgelegt: - -```neon -services: - - create: MyClass - setup: - - $cache = @\Cache -``` - - -Inject -====== - -Während die vorigen drei Wege allgemein in allen objektorientierten Sprachen gelten, wird die Übergabe über die Methoden `inject*()` oder das Attribut `#[Inject]` üblicherweise bei Nette-Presentern verwendet, wo sie standardmäßig eingeschaltet ist; jeder andere Service kann sie über [`inject: true` |services#Inject-Modus] einschalten. Behandelt werden sie in einem [eigenen Kapitel |best-practices:inject-method-attribute]. - - -Welchen Weg wählen? -=================== - -- Der Konstruktor eignet sich für zwingende Abhängigkeiten, die die Klasse für ihren Betrieb unbedingt braucht. -- Der Setter eignet sich umgekehrt für optionale Abhängigkeiten oder für solche, die sich später ändern lassen sollen. -- Von öffentlichen Properties ist allgemein abzuraten. diff --git a/dependency-injection/de/services.texy b/dependency-injection/de/services.texy deleted file mode 100644 index 540b748d58..0000000000 --- a/dependency-injection/de/services.texy +++ /dev/null @@ -1,480 +0,0 @@ -Service-Definitionen -******************** - -.[perex] -In der Konfiguration sagen wir dem DI-Container, wie er die einzelnen Services erzeugen und wie er sie mit ihren Abhängigkeiten verbinden soll. Nette bietet dafür einen sehr übersichtlichen und eleganten Weg. - -Der Abschnitt `services` in der NEON-Konfigurationsdatei ist der Ort, an dem wir eigene Services und ihre Konfiguration definieren. Sehen wir uns ein einfaches Beispiel an, das einen Service namens `database` definiert, der eine Instanz der Klasse `PDO` darstellt: - -```neon -services: - database: PDO('sqlite::memory:') -``` - -Aus der obigen Konfiguration entsteht im [DI-Container|container] diese Factory-Methode: - -```php -public function createServiceDatabase(): PDO -{ - return new PDO('sqlite::memory:'); -} -``` - -Über die Namen der Services lassen sich diese in anderen Teilen der Konfigurationsdatei referenzieren, und zwar in der Form `@serviceName`. Wenn es nicht nötig ist, dem Service einen Namen zu geben, können wir einfach einen Aufzählungspunkt (`-`) verwenden: - -```neon -services: - - PDO('sqlite::memory:') -``` - -Um einen Service aus dem DI-Container zu holen, können wir die Methode `getService()` mit dem Namen des Services als Parameter verwenden oder die Methode `getByType()` mit dem Typ des Services: - -```php -$database = $container->getService('database'); -$database = $container->getByType(PDO::class); -``` - - -Erstellung eines Services -========================= - -Üblicherweise erzeugen wir einen Service einfach, indem wir eine bestimmte Klasse instanziieren. Zum Beispiel: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Wenn wir die Konfiguration um weitere Schlüssel erweitern müssen, lässt sich die Definition auf mehrere Zeilen aufteilen: - -```neon -services: - database: - create: PDO('sqlite::memory:') - setup: ... -``` - -Der Schlüssel `create` hat den Alias `factory`; beide Varianten sind gebräuchlich. Wir empfehlen jedoch, `create` zu verwenden. - -Die Argumente für den Konstruktor oder die Factory-Methode lassen sich alternativ über den Schlüssel `arguments` angeben: - -```neon -services: - database: - create: PDO - arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] -``` - -Services müssen nicht zwingend durch einfaches Instanziieren einer Klasse entstehen; sie können auch das Ergebnis des Aufrufs statischer Methoden oder von Methoden anderer Services sein: - -```neon -services: - database: DatabaseFactory::create() - router: @routerFactory::create() -``` - -Beachten Sie, dass der Einfachheit halber `::` statt `->` verwendet wird, siehe [#Ausdruckssprache]. Es entstehen diese Factory-Methoden: - -```php -public function createServiceDatabase(): PDO -{ - return DatabaseFactory::create(); -} - -public function createServiceRouter(): RouteList -{ - return $this->getService('routerFactory')->create(); -} -``` - -Der DI-Container muss den Typ des erzeugten Services kennen. Erzeugen wir einen Service über eine Methode ohne angegebenen Rückgabetyp, müssen wir diesen Typ in der Konfiguration ausdrücklich angeben: - -```neon -services: - database: - create: DatabaseFactory::create() - type: PDO -``` - - -Argumente -========= - -Argumente übergeben wir Konstruktoren und Methoden sehr ähnlich, wie es in PHP selbst geschieht: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Für bessere Lesbarkeit lassen sich die Argumente auf einzelne Zeilen schreiben. In diesem Fall sind die Kommas optional: - -```neon -services: - database: PDO( - 'mysql:host=127.0.0.1;dbname=test' - root - secret - ) -``` - -Sie können die Argumente auch benennen und müssen sich dann nicht um ihre Reihenfolge kümmern: - -```neon -services: - database: PDO( - username: root - password: secret - dsn: 'mysql:host=127.0.0.1;dbname=test' - ) -``` - -Wenn Sie bestimmte Argumente weglassen und ihre Standardwerte verwenden oder einen Service über [Autowiring|autowiring] einsetzen lassen wollen, verwenden Sie einen Unterstrich (`_`): - -```neon -services: - foo: Foo(_, %appDir%) -``` - -Argumente können Services, Parameter und vieles mehr enthalten, siehe [#Ausdruckssprache]. - - -Setup -===== - -Im Abschnitt `setup` legen wir die Methoden fest, die beim Erzeugen des Services aufgerufen werden sollen. - -```neon -services: - database: - create: PDO(%dsn%, %user%, %password%) - setup: - - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) -``` - -In PHP sähe das so aus: - -```php -public function createServiceDatabase(): PDO -{ - $service = new PDO('...', '...', '...'); - $service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - return $service; -} -``` - -Neben Methodenaufrufen lassen sich auch Werte an Properties zuweisen. Ebenso wird das Hinzufügen von Elementen zu Arrays unterstützt, wobei der Array-Zugriff in Anführungszeichen stehen muss, um Konflikte mit der NEON-Syntax zu vermeiden: - -```neon -services: - foo: - create: Foo - setup: - - $value = 123 - - '$onClick[]' = [@bar, clickHandler] -``` - -Im PHP-Code sähe das so aus: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - $service->value = 123; - $service->onClick[] = [$this->getService('bar'), 'clickHandler']; - return $service; -} -``` - -Im Setup lassen sich aber auch statische Methoden oder Methoden anderer Services aufrufen. Wenn Sie den aktuellen Service selbst als Argument übergeben müssen, verweisen Sie mit `@self` auf ihn: - -```neon -services: - foo: - create: Foo - setup: - - My\Helpers::initializeFoo(@self) - - @anotherService::setFoo(@self) -``` - -Beachten Sie, dass der Einfachheit halber `::` statt `->` verwendet wird, siehe [#Ausdruckssprache]. Es entsteht diese Factory-Methode: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - My\Helpers::initializeFoo($service); - $this->getService('anotherService')->setFoo($service); - return $service; -} -``` - - -Ausdruckssprache -================ - -Nette DI bietet eine außerordentlich reiche Ausdruckssprache, mit der sich fast alles definieren lässt. In den Konfigurationsdateien können wir also [Parameter |configuration#Parameter] verwenden: - -```neon -# Parameter -%wwwDir% - -# Wert eines Parameters unter einem Schlüssel -%mailer.user% - -# Parameter innerhalb eines Strings -'%wwwDir%/images' -``` - -Weiter Objekte erzeugen sowie Methoden und Funktionen aufrufen: - -```neon -# Objekt erzeugen -DateTime() - -# statische Methode aufrufen -Collator::create(%locale%) - -# PHP-Funktion aufrufen -::getenv(DB_USER) -``` - -Auf Services entweder über ihren Namen oder über ihren Typ verweisen: - -```neon -# Service nach Namen -@database - -# Service nach Typ -@Nette\Database\Connection -``` - -Die First-Class-Callable-Syntax verwenden: .{data-version:3.2.0} - -```neon -# Callback erzeugen, gleichbedeutend mit [@user, logout] -@user::logout(...) -``` - -Konstanten verwenden: - -```neon -# Klassenkonstante -FilesystemIterator::SKIP_DOTS - -# globale Konstante über die PHP-Funktion constant() holen -::constant(\PHP_VERSION) -``` - -Über `@service::member` greifen Sie auf öffentliche Properties und Konstanten eines Services zu. Ob der Name eine Property oder eine Konstante meint, entscheidet sein erster Buchstabe - ein kleiner Anfangsbuchstabe bedeutet eine öffentliche Property, ein großer eine Konstante: - -```neon -# öffentliche Property eines Services (beginnt mit einem Kleinbuchstaben) -@settings::apiUrl - -# Klassenkonstante eines Services (beginnt mit einem Großbuchstaben) -@settings::Version -``` - -Methodenaufrufe lassen sich genau wie in PHP verketten. Der Einfachheit halber wird `::` statt `->` verwendet: - -```neon -DateTime()::format('Y-m-d') -# PHP: (new DateTime())->format('Y-m-d') - -@http.request::getUrl()::getHost() -# PHP: $this->getService('http.request')->getUrl()->getHost() -``` - -Diese Ausdrücke können Sie überall verwenden, beim [Erstellen von Services |#Erstellung eines Services], in den [#Argumente], im Abschnitt [#Setup] oder in den [Parametern |configuration#Parameter]: - -```neon -parameters: - ipAddress: @http.request::getRemoteAddress() - -services: - database: - create: DatabaseFactory::create( @anotherService::getDsn() ) - setup: - - initialize( ::getenv('DB_USER') ) -``` - - -Spezielle Funktionen --------------------- - -In den Konfigurationsdateien können Sie die folgenden speziellen Funktionen verwenden: - -- `not()` negiert einen Wert -- `bool()`, `int()`, `float()`, `string()` verlustfreie Umwandlung in den angegebenen Typ .{data-version:3.0.5} -- `typed()` erzeugt ein Array aller Services des angegebenen Typs -- `tagged()` erzeugt ein Array aller Services mit dem angegebenen Tag - -```neon -services: - - Foo( - id: int(::getenv('ProjectId')) - productionMode: not(%debugMode%) - ) -``` - -Anders als die übliche Umwandlung in PHP, etwa `(int)`, wirft die verlustfreie Umwandlung bei nicht numerischen Werten eine Exception. - -Die Funktion `typed()` erzeugt ein Array aller Services des angegebenen Typs (Klasse oder Interface). Services mit abgeschaltetem Autowiring lässt sie aus. Es lassen sich auch mehrere Typen angeben, getrennt durch Kommas. - -```neon -services: - - BarsDependent( typed(Bar) ) -``` - -Ein Array von Services eines bestimmten Typs lässt sich auch automatisch über [Autowiring |autowiring#Sammlung von Services] als Argument übergeben. - -Die Funktion `tagged()` erzeugt dann ein Array aller Services mit einem bestimmten Tag. Auch hier lassen sich mehrere Tags durch Kommas getrennt angeben. - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - - -Autowiring -========== - -Über den Schlüssel `autowired` können Sie das Verhalten des Autowirings für einen bestimmten Service beeinflussen. Einzelheiten finden Sie im [Kapitel über Autowiring|autowiring]. - -```neon -services: - foo: - create: Foo - autowired: false # der Service foo ist vom Autowiring ausgenommen -``` - - -Lazy Services .{data-version:3.2.4} -=================================== - -Lazy Loading ist eine Technik, die das Erzeugen eines Services aufschiebt, bis er tatsächlich gebraucht wird. In der globalen Konfiguration können Sie das [lazy Erzeugen |configuration#Lazy Services] für alle Services auf einmal einschalten. Für einzelne Services lässt sich dieses Verhalten dann überschreiben: - -```neon -services: - foo: - create: Foo - lazy: false -``` - -Ist ein Service als lazy definiert, bekommen wir beim Anfordern aus dem DI-Container ein besonderes Proxy-Objekt. Dieses Proxy sieht aus und verhält sich genau wie der tatsächliche Service, die eigentliche Initialisierung (Aufruf des Konstruktors und der Setup-Aufrufe) geschieht aber erst beim ersten Zugriff auf eine seiner Methoden oder Properties. - -Denken Sie daran: Weil der Service später erzeugt wird, zeigen sich auch Fehler in seiner Konfiguration später. Falsche Zugangsdaten zur Datenbank verraten sich zum Beispiel nicht beim Start der Anwendung, sondern erst bei der ersten Query. - -Das lazy Erzeugen entschärft auch zirkuläre Abhängigkeiten, also die Situation, dass Service A den Service B braucht und B zugleich A. Ohne es meldet der Container den Fehler `Circular reference detected`. Mit einem lazy Proxy bekommt Service A nur ein Proxy von Service B, das sich erst initialisiert, wenn es tatsächlich verwendet wird, also zu einem Zeitpunkt, an dem A bereits existiert. Trotzdem ist eine zirkuläre Abhängigkeit ein Zeichen für einen fehlerhaften Entwurf, und es ist besser, sie loszuwerden. - -.[note] -Lazy Loading erfordert PHP 8.4 oder neuer und funktioniert nur für Services, die durch direktes Instanziieren einer Klasse entstehen (etwa `create: Foo`), nicht für solche, die eine Factory-Methode erzeugt. Ebenso lässt es sich nicht für Klassen verwenden, die letztlich von einer internen PHP-Klasse erben. Wenn sich Lazy Loading nicht anwenden lässt, wird das Flag `lazy: true` stillschweigend ignoriert. - - -Tags -==== - -Tags dienen dazu, Services um ergänzende Informationen zu erweitern. Sie können einem Service einen oder mehrere Tags zuweisen: - -```neon -services: - foo: - create: Foo - tags: - - cached -``` - -Tags können auch Werte tragen: - -```neon -services: - foo: - create: Foo - tags: - logger: monolog.logger.event -``` - -Um alle Services mit bestimmten Tags zu holen, können Sie die Funktion `tagged()` verwenden: - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - -Innerhalb des DI-Containers holen Sie die Namen aller Services mit einem bestimmten Tag über die Methode `findByTag()`: - -```php -$names = $container->findByTag('logger'); -// $names ist ein Array mit den Namen der Services als Schlüsseln und den Werten der Tags als Werten -// z. B. ['foo' => 'monolog.logger.event', ...] -``` - - -Inject-Modus -============ - -Das Flag `inject: true` schaltet die Übergabe von Abhängigkeiten über öffentliche Properties mit dem Attribut [Inject |best-practices:inject-method-attribute#Inject-Attribute] und über Methoden [inject*() |best-practices:inject-method-attribute#inject*()-Methoden] ein. - -```neon -services: - articles: - create: App\Model\Articles - inject: true -``` - -Standardmäßig ist der Modus `inject` nur für Presenter eingeschaltet. - - -Änderungen an Services -====================== - -Der DI-Container enthält zahlreiche Services, die über eingebaute oder [eigene Extensions|extensions] hinzugekommen sind. Die Definitionen dieser bestehenden Services können Sie direkt in der Konfiguration ändern. So können Sie zum Beispiel für den Service `application.application` die Klasse, die standardmäßig `Nette\Application\Application` ist, gegen eine andere austauschen: - -```neon -services: - application.application: - create: MyApplication - alteration: true -``` - -Das Flag `alteration` zeigt an, dass wir einen bestehenden Service lediglich ändern. Es dient zugleich als Absicherung: Existiert der geänderte Service nicht, scheitert die Kompilierung mit einer Exception. - -Wir können auch das Setup ergänzen: - -```neon -services: - application.application: - create: MyApplication - alteration: true - setup: - - '$onStartup[]' = [@resource, init] -``` - -Sie müssen einen Service nicht über seinen internen Namen ansprechen - Sie können stattdessen auf seinen Typ verweisen. Das vorige Beispiel lässt sich auch so schreiben: - -```neon -services: - @Nette\Application\Application: - create: MyApplication -``` - -Beim Ändern eines Services möchten wir vielleicht die ursprünglichen Argumente, Setup-Einträge oder Tags entfernen; dafür gibt es den Schlüssel `reset`: - -```neon -services: - application.application: - create: MyApplication - alteration: true - reset: - arguments: true - setup: true - tags: true -``` - -Wenn Sie einen Service entfernen wollen, den eine Extension hinzugefügt hat, geht das so: - -```neon -services: - cache.journal: false -``` diff --git a/dependency-injection/de/upgrading.texy b/dependency-injection/de/upgrading.texy deleted file mode 100644 index 389c86ea75..0000000000 --- a/dependency-injection/de/upgrading.texy +++ /dev/null @@ -1,49 +0,0 @@ -Upgrade -******* - - -Upgrade auf Version 3.1 -======================= - -- das Autowiring übergibt einem nullable Parameter ohne Standardwert nicht mehr `null`; übergeben Sie das Argument ausdrücklich oder geben Sie dem Parameter einen Standardwert -- die Unterstützung der Annotation `@return` wurde eingestellt; verwenden Sie einen Rückgabetyp oder geben Sie den Typ in der Service-Definition über `type:` an -- der Schlüssel `dynamic` wurde in `imported` und `class` in `type` umbenannt -- das Symbol für ein weggelassenes Argument hat sich von `...` zu `_` geändert, etwa `MyService(_, 123)` -- in NEON-Dateien muss das Zeichen `@` am Anfang eines Strings nicht mehr escapt werden -- der Schlüssel `parameters` innerhalb der Definitionen generierter Factories ist veraltet -- die Methode `Nette\DI\Config\Loader::save()` ist veraltet; exportieren Sie die Konfiguration über `Nette\DI\Config\Adapters\NeonAdapter::dump()` - -Version 3.1 ist eine Übergangsversion: Sie bringt keine neuen Fähigkeiten, warnt aber mit Notices vor allem, was später anders funktionieren wird. Siehe den Artikel [Nette DI 3.1: transition release |https://blog.nette.org/en/nette-di-3-1-transition-release]. - - -Upgrade auf Version 3.0 -======================= - -- die Unterstützung für INI-Dateien wurde entfernt -- das direkte Schreiben von PHP-Code in die Konfiguration über Fragezeichen (etwa `"$service->onError[] = ?"(...)`) wurde entfernt; verwenden Sie stattdessen die Array-Syntax `'$onError[]' = [...]` -- verwenden Sie in Konfigurationsdateien `factory: PDO(...)` statt `class: PDO(...)` -- der Tag `nette.presenter` wird für Presenter nicht mehr verwendet - - -Für Autoren von Compiler-Extensions ------------------------------------ - -Während Nette 2.4 intern jeden Service als `Nette\DI\ServiceDefinition` beschrieb, gibt es jetzt mehrere Typen von Definitionen: `Nette\DI\Definitions\ImportedDefinition` für importierte (dynamische) Services, `Nette\DI\Definitions\FactoryDefinition` für generierte Factories auf Basis von Interfaces, `Nette\DI\Definitions\AccessorDefinition` für generierte Accessors und `Nette\DI\Definitions\ServiceDefinition` für gewöhnliche Services. - -Neben `ContainerBuilder::addDefinition()` gibt es deshalb mehrere weitere Methoden, um eine neue Definition anzulegen: `addFactoryDefinition()`, `addAccessorDefinition()` und `addImportedDefinition()`. - - -Upgrade auf Version 2.4 -======================= - -- Abschnitte der Konfiguration (etwa production, development) in einer einzigen Konfigurationsdatei sind veraltet; verwenden Sie das Paar `config.neon` und `config.local.neon` -- das Erben von Service-Definitionen ist veraltet -- `Statement::setEntity()` ist veraltet - - -Upgrade auf Version 2.3 -======================= - -- die Unterstützung dafür, Services innerhalb des Abschnitts einer Extension in der Konfigurationsdatei abzulegen, wurde entfernt -- die Unterstützung dynamisch hinzugefügter Extensions wurde entfernt -- beim dynamischen Ersetzen eines Services (über `removeService()`, `addService()`) muss der neue Service eine Instanz desselben Interfaces bzw. derselben Klasse wie der ursprüngliche sein diff --git a/dependency-injection/en/@home.texy b/dependency-injection/en/@home.texy deleted file mode 100644 index a743741116..0000000000 --- a/dependency-injection/en/@home.texy +++ /dev/null @@ -1,25 +0,0 @@ -Nette DI -******** - -.[perex] -Dependency Injection is a design pattern that will fundamentally change the way you look at code and development. It opens the way to a world of cleanly designed and sustainable applications. - -- [What is Dependency Injection? |introduction] -- [Global State & Singletons |global-state] -- [Passing Dependencies |passing-dependencies] -- [What is a DI Container? |container] -- [Frequently Asked Questions |faq] - - -The `nette/di` package provides an extremely advanced compiled DI container for PHP. - -- [Nette DI Container |nette-container] -- [Configuration |configuration] -- [Service Definitions |services] -- [Autowiring |autowiring] -- [Generated Factories |factory] -- [Creating Extensions for Nette DI |extensions] -- [Container Compilation Explained |compilation-internals] - -{{maintitle: Nette DI: Dependency Injection Container for PHP}} -{{description: Learn what dependency injection is and why it changes how you write code, then use Nette DI, an advanced compiled DI container for PHP.}} diff --git a/dependency-injection/en/@left-menu.texy b/dependency-injection/en/@left-menu.texy deleted file mode 100644 index 251ff50487..0000000000 --- a/dependency-injection/en/@left-menu.texy +++ /dev/null @@ -1,30 +0,0 @@ -Dependency Injection -******************** -- [What is DI? |introduction] -- [Global State & Singletons |global-state] -- [Passing Dependencies |passing-dependencies] -- [What is a DI Container? |container] -- [Frequently Asked Questions |faq] - - -Nette DI --------- -- [Nette DI Container |nette-container] -- [Configuration |configuration] -- [Service Definitions |services] -- [Autowiring |autowiring] -- [Generated Factories |factory] -- [Creating Extensions for Nette DI |extensions] -- [Compilation Explained |compilation-internals] -- [Upgrading] - -- "Course: DI by Example .[link-external]":https://github.com/nette-examples/di-by-example .{padding-top:1em} - - -Further Reading -*************** -- [Nette Documentation |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Best practices |best-practices:] -- [Troubleshooting |nette:troubleshooting] diff --git a/dependency-injection/en/@meta.texy b/dependency-injection/en/@meta.texy deleted file mode 100644 index 42471908b0..0000000000 --- a/dependency-injection/en/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Documentation}} diff --git a/dependency-injection/en/autowiring.texy b/dependency-injection/en/autowiring.texy deleted file mode 100644 index b6a77b517f..0000000000 --- a/dependency-injection/en/autowiring.texy +++ /dev/null @@ -1,282 +0,0 @@ -Autowiring -********** - -.[perex] -Autowiring is a great feature that automatically passes required services to the constructor and other methods, so we don't have to specify them explicitly. It saves you a lot of time. - -Thanks to this, we can omit the vast majority of arguments when writing service definitions. Instead of: - -```neon -services: - articles: Model\ArticleRepository(@database, @cache.storage) -``` - -Just write: - -```neon -services: - articles: Model\ArticleRepository -``` - -Autowiring is guided by types, so for it to work, the `ArticleRepository` class must be defined roughly as follows: - -```php -namespace Model; - -class ArticleRepository -{ - public function __construct(\PDO $db, \Nette\Caching\Storage $storage) - {} -} -``` - -Autowiring never uses service names. It is guided solely by the type system of PHP, so it also knows that a class satisfies the interfaces it implements and the classes it inherits from. Thanks to this, the name of a service is merely an auxiliary identifier, and renaming it will not break anything in the application. - -To be able to use autowiring, there must be **exactly one service** of each type in the container. If there were more, autowiring wouldn't know which one to pass and would throw an exception: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - tempDb: PDO('sqlite::memory:') - articles: Model\ArticleRepository # THROWS EXCEPTION, both mainDb and tempDb match -``` - -One solution is to bypass autowiring and explicitly specify the service name (e.g., `articles: Model\ArticleRepository(@mainDb)`). However, a more convenient approach is to either [disable |#Disabling Autowiring] autowiring for one of the services, or to [prefer |#Autowiring Preference] one service over the others. - - -Disabling Autowiring --------------------- - -We can disable autowiring for a service using the `autowired: false` option: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - - tempDb: - create: PDO('sqlite::memory:') - autowired: false # service tempDb is excluded from autowiring - - articles: Model\ArticleRepository # therefore passes mainDb to the constructor -``` - -The `articles` service will not throw an exception about two matching `PDO` services (`mainDb` and `tempDb`) being available for the constructor, because it only considers the `mainDb` service. - -Autowiring can also be disabled globally for entire types using the [`di › excluded` |configuration#DI] configuration option, which lists the types (and their descendants) that should never be autowired. - -.[note] -Autowiring configuration in Nette differs from Symfony. In Symfony, `autowire: false` means autowiring shouldn't be used for the service's constructor arguments. In Nette, autowiring applies to constructor arguments and any other methods invoked via the container (like setter injection). The `autowired: false` option prevents the container from automatically passing this service instance as a dependency to other services. - - -Autowiring Preference ---------------------- - -If we have multiple services of the same type and specify the `autowired` option for one of them, this service becomes the preferred one: - -```neon -services: - mainDb: - create: PDO(%dsn%, %user%, %password%) - autowired: PDO # becomes preferred - - tempDb: - create: PDO('sqlite::memory:') - - articles: Model\ArticleRepository -``` - -The `articles` service will not throw an exception about multiple matching `PDO` services (`mainDb` and `tempDb`), but will use the preferred one, which is `mainDb`. - - -Collection of Services ----------------------- - -Autowiring can also pass arrays of services of a specific type. Since PHP doesn't natively support specifying array item types in type hints, you must supplement the `array` type hint with a phpDoc comment specifying the item type, like `ClassName[]`: - -```php -namespace Model; - -class ShipManager -{ - /** - * @param Shipper[] $shippers - */ - public function __construct(array $shippers) - {} -} -``` - -The DI container then automatically passes an array of services corresponding to the given type. It omits services that have [autowiring disabled |#Disabling Autowiring] and never includes the service currently being created in its own collection. Unlike passing an individual service, [narrowing |#Narrowing Autowiring] autowiring to a specific type or marking a service as [preferred |#Autowiring Preference] has no effect here - the array always contains every service of the given type. - -The type in the comment can also be of the form `array<int, Class>` or `list<Class>`. If you can't control the form of the phpDoc comment, you can pass an array of services directly in the configuration using [`typed()` |services#Special Functions]. - - -Scalar Arguments ----------------- - -Autowiring only works for objects and arrays of objects. Scalar arguments (e.g., strings, numbers, booleans) must be [specified in the configuration |services#Arguments]. An alternative is to create a [settings object|best-practices:passing-settings-to-presenters] that encapsulates the scalar value (or multiple values). This object can then be passed using autowiring. - -```php -class MySettings -{ - public function __construct( - // readonly can be used since PHP 8.1 - public readonly bool $value, - ) - {} -} -``` - -You register it as a service by adding it to the configuration: - -```neon -services: - - MySettings('any value') -``` - -Other classes can then request it via autowiring. - - -Optional Dependencies ---------------------- - -If a constructor or method parameter has a default value and no service of the required type exists in the container, autowiring does not throw an exception - it simply skips the argument, so the default value is used. This is how you declare optional dependencies: - -```php -class Foo -{ - public function __construct( - private ?Logger $logger = null, - ) {} -} -``` - -In contrast, for a parameter without a default value, a missing service always causes an exception. - - -Narrowing Autowiring --------------------- - -For individual services, autowiring can be narrowed down to specific classes or interfaces. - -Normally, autowiring passes a service to every method parameter whose type the service matches. Narrowing means we establish conditions that the types specified for the method parameters must meet for the service to be passed to them. - -Let's take an example: - -```php -class ParentClass -{} - -class ChildClass extends ParentClass -{} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -If we registered them all as services, autowiring would fail: - -```neon -services: - parent: ParentClass - child: ChildClass - parentDep: ParentDependent # THROWS EXCEPTION, both parent and child services match - childDep: ChildDependent # autowiring passes the child service to the constructor -``` - -The `parentDep` service throws the exception `Multiple services of type ParentClass found: child, parent`, because both the `parent` and `child` services fit into its constructor, and autowiring cannot decide which one to choose. - -For the `child` service, we can therefore narrow its autowiring to the type `ChildClass`: - -```neon -services: - parent: ParentClass - child: - create: ChildClass - autowired: ChildClass # can also be written as 'autowired: self' - - parentDep: ParentDependent # autowiring passes the parent service to the constructor - childDep: ChildDependent # autowiring passes the child service to the constructor -``` - -Now, the `parent` service is passed to the `parentDep` service's constructor, as it is now the only matching object. The `child` service is no longer passed there by autowiring. Yes, the `child` service is still of type `ParentClass`, but the narrowing condition `autowired: ChildClass` means it will only be passed to parameters explicitly typed as `ChildClass` (or its subtypes). Since `ParentDependent` requires `ParentClass`, the `child` service is no longer considered a candidate for autowiring there. - -For the `child` service, `autowired: ChildClass` could also be written as `autowired: self`, since `self` is a placeholder for the current service's class. - -In the `autowired` key, it is also possible to specify multiple classes or interfaces as an array: - -```neon -autowired: [ParentClass, FooInterface] -``` - -Let's try to add interfaces to the example: - -```php -interface FooInterface -{} - -interface BarInterface -{} - -class ParentClass implements FooInterface -{} - -class ChildClass extends ParentClass implements BarInterface -{} - -class FooDependent -{ - function __construct(FooInterface $obj) - {} -} - -class BarDependent -{ - function __construct(BarInterface $obj) - {} -} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -If we don't restrict the `child` service in any way, it will fit into the constructors of all `FooDependent`, `BarDependent`, `ParentDependent`, and `ChildDependent` classes, and autowiring will pass it there. - -However, if we narrow its autowiring to `ChildClass` using `autowired: ChildClass` (or `self`), autowiring will only pass it to the `ChildDependent` constructor, because it requires an argument of type `ChildClass` and it holds that `ChildClass` *is of type* `ChildClass`. None of the other parameters' required types is `ChildClass` or a subtype of it, so the service is not passed to them. - -If we restrict it to `ParentClass` using `autowired: ParentClass`, autowiring will again pass it to the `ChildDependent` constructor (because the required `ChildClass` is a subtype of `ParentClass`) and now also to the `ParentDependent` constructor, because the required type `ParentClass` is also suitable. - -If we restrict it to `FooInterface`, it will still be autowired into `ParentDependent` (the required `ParentClass` is a subtype of `FooInterface`) and `ChildDependent`, and additionally into the `FooDependent` constructor, but not into `BarDependent`, because `BarInterface` is not a subtype of `FooInterface`. - -```neon -services: - child: - create: ChildClass - autowired: FooInterface - - fooDep: FooDependent # autowiring passes the child service to the constructor - barDep: BarDependent # THROWS EXCEPTION, no service matches - parentDep: ParentDependent # autowiring passes the child service to the constructor - childDep: ChildDependent # autowiring passes the child service to the constructor -``` - -{{maintitle: PHP Autowiring in Nette DI: Pass Services Automatically}} -{{description: Let Nette DI pass services to constructors and other methods for you. Disable or narrow autowiring, set a preferred service and collect services into an array.}} diff --git a/dependency-injection/en/compilation-internals.texy b/dependency-injection/en/compilation-internals.texy deleted file mode 100644 index 0f8af18d72..0000000000 --- a/dependency-injection/en/compilation-internals.texy +++ /dev/null @@ -1,225 +0,0 @@ -Container Compilation Explained -******************************* - -.[perex] -This page opens up container compilation: the phases it goes through, when configuration parameters are expanded, when `@service` strings turn into real references, and - the question extension authors ask most - in which phase you can safely search for services by type. It is the deeper companion to [Creating Extensions |extensions]. - -You don't need any of this to write a normal application, or even a normal extension. But once your extension starts inspecting or reshaping the service graph, timing becomes everything: the same `getByType()` call gives a reliable answer in one phase and a misleading one in another. This page explains why, so you always know where your code belongs. - - -Two Worlds: Compilation vs. Runtime -=================================== - -The most important thing to understand is that a Nette container is **not assembled on every request**. It is built once into an optimized PHP class, that class is stored on disk, and every subsequent request merely `include`s the finished file. All the machinery described below - extensions, resolvers, the code generator - runs **only during (re)compilation**. - -This splits the world into two representations that never coexist: - -| | during compilation | at runtime -|---|---|--- -| What exists | **definitions** (recipes) in `ContainerBuilder` | **instances** of services in `Container` -| Key classes | `Compiler`, `ContainerBuilder`, `Resolver`, `PhpGenerator` | `Container` (parent of the generated class) -| `%param%`, `@service` | textual markers still being translated | already translated / baked into the code - -The generated class extends `Nette\DI\Container` and has a `createServiceXxx()` method for each service. Its parameters and autowiring metadata are precomputed, so at runtime there is nothing left to resolve - only to instantiate services on demand. - -.[note] -In developer mode the container is rebuilt automatically whenever a configuration file or an extension class changes; both are tracked as dependencies. In production it is compiled once and never checked again, which is where the speed comes from. - - -The Phases at a Glance -====================== - -Compilation is orchestrated by `Compiler::compile()`, and it comes down to three steps: - -```php -public function compile(): string -{ - $this->processExtensions(); // PHASE A: schemas + loadConfiguration() - $this->processBeforeCompile(); // PHASE B: resolve + beforeCompile() + complete - return $this->generateCode(); // PHASE C: code generation + afterCompile() -} -``` - -The whole mental model fits into a single idea - **each phase knows more than the previous one:** - -- **Phase A** fills the graph with definitions. Service **types are not yet reliably known**, because a type may come from a factory's return value that nobody has looked at yet. -- **Phase B** first resolves all types (`resolve`), then lets extensions reshape the graph (`beforeCompile`), and finally [autowires |autowiring] arguments (`complete`). -- **Phase C** turns the finished graph into PHP and lets extensions touch the generated code. - -That growing knowledge is exactly why the same operation is safe in one phase and unreliable in another. The rest of this page walks through the phases with that idea in mind. - - -Phase A: Registering Definitions -================================ - -In this phase, Nette calls three methods on every extension - `getConfigSchema()`, then `setConfig()`, then `loadConfiguration()` - but in a **carefully controlled order**, because here the order genuinely matters. - - -Why the Order Matters ---------------------- - -- **`ParametersExtension` and `ExtensionsExtension` go first.** The former must run before anything else so it can expand `%param%` across the whole configuration - every other extension then receives its own section with the values already filled in. The latter registers further extensions listed in the `extensions:` section, so it too has to exist before the rest are processed. -- **`ServicesExtension` goes last.** The user's `services:` section therefore always has the final word and can override anything the extensions set up. -- **`InjectExtension` is moved to the very end** so that its work sees the setups added by all the other extensions. - -The takeaway for you: by the time your extension's `loadConfiguration()` runs, parameters are already expanded, but the user's services are not there yet. That single fact drives most of the timing rules below. - - -Turning services: into Definitions ----------------------------------- - -The user's `services:` section is turned into [definition objects |extensions#Definition Types] here, in the last step of phase A. Each NEON entry is normalized (shorthand notations are unified), its kind is detected (ordinary service, factory, accessor, ...), and a matching definition is created in the builder. This is also the first moment simple `@name` / `@Type` arguments become references - see [below |#References: When @service Becomes a Reference]. - -At the end of phase A, all definitions are present - every extension and the user have registered what they wanted - but the picture is not yet sharp: - -- **types are not resolved** for definitions whose type comes from a factory's return value, -- **arguments are not autowired**, -- some `@service` references are still plain strings. - -This is exactly why searching by type here is unreliable - more on that [below |#Introspecting ContainerBuilder: When It's Safe]. - - -Parameters: When %param% Is Expanded -==================================== - -One of the two headline questions. The answer is short: **once, at the very start of phase A, across the entire configuration tree.** - -`ParametersExtension` runs first, and one of the first things it does is expand `%param%` placeholders - first inside the parameters themselves (a parameter may reference another), then throughout the rest of the configuration. So by the time any other extension, including `ServicesExtension`, receives its section, the placeholders are already gone. Extensions work with concrete values, never with `%...%`. - -When a placeholder is the whole string, its value is returned *as is* - including arrays and objects - so `%mailer%` can expand to an entire array. Anywhere else it is concatenated into a string, and the dotted notation `%foo.bar%` reaches into nested arrays. - - -Static vs. Dynamic Parameters ------------------------------ - -Not every value can be baked into the code. A parameter whose value differs per environment - an environment variable, the `baseUrl` derived from the request - must stay **dynamic**. You declare such parameters via `setDynamicParameterNames()` or `Expect::...->dynamic()` in a schema; more in [Dynamic Parameters |application:bootstrapping#Dynamic Parameters]. - -A dynamic parameter is not replaced by a value but by an expression that reads it *at runtime*. So `%env.DB_HOST%` does not freeze into a string; it becomes a runtime lookup in the generated container. Everything else is static and gets frozen at compile time - which is the usual source of the surprise "my `getenv()` value is the same in every environment": the parameter was simply static. - -The opposite operation is **escaping**: to keep a literal `%` or `@` from being interpreted, it is doubled (`%%`, `@@`). Nette does this automatically for the parameters it injects for you, so their values are never mistaken for placeholders or references. - - -References: When @service Becomes a Reference -============================================= - -The second headline question. Translating `@service` happens **in several steps across different phases**, depending on how complex the string is. You rarely need to trace this by hand, but knowing the steps explains why some references resolve earlier than others. - -- **Parsing (config load).** A `@service` used *as an entity* - the thing that creates a service, as in `Foo(@bar)` - becomes a reference immediately. A `@service` used *as an argument* stays a plain string for now. A quoted `@` is escaped to `@@`, so it counts as literal text, not a reference. -- **Phase A (`loadConfiguration`).** When definitions are processed, a clean `@name` or `@Type` argument is turned into a `Reference` object. This catches only the simple forms; `@service::CONST` or a `@` inside a larger expression is left for later. -- **Phase B (`complete`).** The real "smart" translation happens here: `@service` → reference, `@service::CONSTANT` → a literal class constant, `@service::property` → reading that property, `@@x` → the literal text `@x`. - -There is a second translation hidden in the word *reference* itself. A `Reference` may point either by **name** or by **type** (`@Namespace\Type`). A type reference is **not yet a service name** - it is resolved to a concrete name by autowiring, and that happens only in the **complete** step, once the autowiring index is built. This is the bridge to the next section: autowiring lookups are deliberately postponed until the index is ready. - -| Form | Becomes a reference/expression in | Resolved to a concrete service in -|---|---|--- -| entity (`@foo` as a factory) | parsing | complete -| argument `@foo`, `@Type` | phase A | complete -| `@foo::CONST`, `@foo::prop` | phase B | complete -| type reference `@Type` | phase A/B | complete (autowiring) - - -Introspecting ContainerBuilder: When It's Safe -============================================== - -Now the question extension authors ask most: **in which method can I search for services by type?** The answer follows from one simple rule about how the builder tracks its own state. - -Searching **by type** (`getByType()`, `getDefinitionByType()`, `findByType()`) needs the service graph to be *resolved* - every type known, the autowiring index built. So whenever you call one of these and the graph has changed since the last resolve, the builder **resolves the whole known graph on the spot**. During the resolve itself, any search by type is forbidden and throws `NotAllowedDuringResolvingException`. - -Searching **by tag** (`findByTag()`) has no such requirement - tags don't depend on types, so it works in **every phase**. - -Phase by phase: - -- **`loadConfiguration()` (phase A) - searching by type is unreliable.** The graph is incomplete: extensions that run later haven't registered their services yet, and above all the user's `services:` (which runs last) isn't there. A `getByType()` call does work - it triggers an early resolve of a partial graph - but the answer comes from an incomplete picture, and the premature resolve wastes effort. Rule of thumb: **in `loadConfiguration()` only register definitions; don't search by type.** `findByTag()` is fine. -- **`beforeCompile()` (phase B) - the right place to introspect.** By now **all** definitions exist (including the user's), **types are resolved**, and the **autowiring index is built**, so `getByType()`, `findByType()`, and `findByTag()` all return **reliable** answers. Arguments are *not* autowired yet - that is the very next step (`complete`), after all `beforeCompile()` calls. When you modify a definition here, the next `getByType()` transparently re-resolves the graph, so you can freely alternate edits and queries. -- **`afterCompile()` (phase C) - code only.** It works over the generated class, not the builder. The graph is finished; here you shape the resulting PHP. - -| I want to... | Phase -|---|--- -| register a service | `loadConfiguration()` -| search by **tag** and modify definitions | `loadConfiguration()` or `beforeCompile()` -| search by **type** (`getByType`/`findByType`) | **`beforeCompile()`** -| depend on which services autowiring chose for arguments | not at compile time - inspect it at runtime -| touch the generated code | `afterCompile()` -| run code after the container starts | [initialization code |extensions#Initialization Code] - - -Inside Phase B: resolve and complete -==================================== - -Phase B is two passes with the `beforeCompile()` calls sandwiched between them: - -```php -$this->builder->resolve(); // types resolved, autowiring index built -foreach ($this->extensions as $extension) { - $extension->beforeCompile(); -} -$this->builder->complete(); // ONLY NOW are arguments autowired -``` - -**`resolve()`** determines the type of every service - taken from its declared `type`, or deduced from its factory: the return type of a factory method, the class it instantiates, or the service a reference points to - and then builds the autowiring index that maps each type (the class plus its parents and interfaces) to a service name. A service marked `autowired: false` is left out of the index; `autowired: [A, B]` narrows the types under which it is visible. Crucially, resolve settles *types*, not *arguments* - autowiring arguments would need the finished index, which only exists after this pass. - -**`complete()`** is where autowiring of arguments actually happens. For every definition it fills in the missing constructor and setup arguments by looking their types up in the now-complete index. This is why type references were left unresolved during resolve: the lookup belongs here, once there is a reliable index to look into. - - -Phase C: Generating the Code -============================ - -`generateCode()` hands the finished graph to `PhpGenerator`, which produces a class extending `Container` with a `createServiceXxx()` method per service, plus the precomputed `aliases`, `tags`, and `wiring` metadata. Each `Statement` becomes PHP text (`new Foo(...)`, method calls, property access), and each `Reference` becomes a `$this->getService(...)` call. - -Extensions then get a final `afterCompile()` pass over the generated class - this is where, for instance, the static and dynamic parameter getters are emitted - plus the chance to add [initialization code |extensions#Initialization Code] that runs on every request. - - -The Timeline in One Picture -=========================== - -``` -COMPILATION (once, into the cache) -│ -├─ load config files NEON -> Statement/array; merge files -│ quoted @ -> @@ ; entities -> Statement -│ -▼ Compiler::compile() -│ -├─ PHASE A processExtensions() -│ ├─ ParametersExtension (FIRST) ── %param% EXPANDED across the whole config -│ │ dynamic ones -> runtime expression -│ ├─ ExtensionsExtension (FIRST) ── registers further extensions -│ ├─ ...other extensions... ── loadConfiguration(): only register definitions -│ └─ ServicesExtension (LAST) ── services: -> Definition objects -│ @name/@Type -> Reference -│ [graph complete in count; TYPES and ARGUMENTS not yet; search-by-type unreliable] -│ -├─ PHASE B processBeforeCompile() -│ ├─ builder.resolve() ── resolve all types; build autowiring index -│ │ [types ready; index ready] -│ ├─ beforeCompile() extensions ── SAFE getByType/findByType/findByTag here -│ │ (arguments not autowired yet) -│ └─ builder.complete() ── autowire ARGUMENTS; finish reference translation -│ type references -> service names -│ -└─ PHASE C generateCode() - ├─ PhpGenerator.generate() ── Statement -> PHP; createServiceXxx() methods - ├─ afterCompile() extensions ── tweak code; emit parameter getters - └─ toString() ── final PHP code -> cache - -──────────────────────────────────────────────────────────── - -RUNTIME (every request) -│ -├─ new Container($dynamicParams) -├─ initialize() ── extensions' boot code (session, headers, validation) -└─ getService()/getByType() ── lazy instances from precomputed metadata -``` - - -Common Misconceptions -===================== - -- "In `loadConfiguration()` I'll look up services by type." No - the graph is incomplete (the user's `services:` runs after you) and `getByType()` triggers a premature resolve of a partial graph. Move it to `beforeCompile()`. `findByTag()` is fine even here. -- "A `getenv()` value in a parameter will differ in each environment." Only if the parameter is dynamic. Otherwise it is baked in at compile time and stays the same everywhere. -- "A `@Type` reference is already a service name." It isn't - it is a type reference, resolved to a concrete name by autowiring only in the complete step. -- "My extension reads a helper file, but changes don't show up." Register it with `$builder->addDependency($file)`, otherwise the cache doesn't know about it and won't rebuild. -- "During `resolve()` I can call `getByType()`." No - it throws `NotAllowedDuringResolvingException`. Searching by type belongs in `beforeCompile()` or later, never in the middle of resolving. - -{{maintitle: Nette DI Container Compilation: Phases Explained}} -{{description: What happens when Nette compiles a DI container: its phases, when parameters expand, when @service becomes a reference and when you can safely find services.}} diff --git a/dependency-injection/en/configuration.texy b/dependency-injection/en/configuration.texy deleted file mode 100644 index c3bbbb8238..0000000000 --- a/dependency-injection/en/configuration.texy +++ /dev/null @@ -1,336 +0,0 @@ -Configuring DI Container -************************ - -.[perex] -Overview of configuration options for the Nette DI container. - - -Configuration File -================== - -The Nette DI container is easily controlled using configuration files. These are usually written in the [NEON format|neon:format]. We recommend using [editors with support |tools:ide] for this format. - -<pre> -"decorator .[prism-token prism-atrule]":[#Decorator]: "Decorator .[prism-token prism-comment]"<br> -"di .[prism-token prism-atrule]":[#DI]: "DI Container .[prism-token prism-comment]"<br> -"extensions .[prism-token prism-atrule]":[#Extensions]: "Install additional DI extensions .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#Including files]: "Including files .[prism-token prism-comment]"<br> -"parameters .[prism-token prism-atrule]":[#Parameters]: "Parameters .[prism-token prism-comment]"<br> -"search .[prism-token prism-atrule]":[#Search]: "Automatic service registration .[prism-token prism-comment]"<br> -"services .[prism-token prism-atrule]":[services]: "Services .[prism-token prism-comment]" -</pre> - -.[note] -To write a string containing the character `%`, you must escape it by doubling it to `%%`. - - -Parameters -========== - -In the configuration, you can define parameters that can then be used as part of service definitions. This allows you to clarify the configuration or to centralize values that might change. - -```neon -parameters: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: secret -``` - -We refer to the `dsn` parameter anywhere in the configuration using the notation `%dsn%`. Parameters can also be used inside strings like `'%wwwDir%/images'`. - -Parameters do not have to be just strings or numbers; they can also contain arrays: - -```neon -parameters: - mailer: - host: smtp.example.com - secure: ssl - user: franta@gmail.com - languages: [cs, en, de] -``` - -We refer to a specific key as `%mailer.user%`. - -If your code (e.g., a class) needs the value of a parameter, pass it to the class. For example, in the constructor. There is no global configuration object that classes can query for parameter values. That would be a violation of the dependency injection principle. - - -Services -======== - -See [separate chapter|services]. - - -Decorator -========= - -How can you modify multiple services of a certain type at once? For example, how to call a specific method on all presenters that inherit from a particular base class? That's what the decorator is for. - -```neon -decorator: - # for all services that are instances of this class or interface - App\Presentation\BasePresenter: - setup: - - setProjectId(10) # call this method - - $absoluteUrls = true # and set the variable -``` - -Decorators can also be used to set [tags |services#Tags] or enable [inject mode |services#Inject Mode]. - -```neon -decorator: - InjectableInterface: - tags: [mytag: 1] - inject: true -``` - - -DI -=== - -Technical settings of the DI container. - -```neon -di: - # show DIC in Tracy Bar? - debugger: ... # (bool) defaults to autodetection (enabled when Tracy is present) - - # parameter types that you never autowire - excluded: ... # (string[]) - - # enable lazy service creation? - lazy: ... # (bool) default is false - - # the class from which the DI container inherits - parentClass: ... # (string) defaults to Nette\DI\Container -``` - - -Lazy Services .{data-version:3.2.4} ------------------------------------ - -Setting `lazy: true` activates lazy (deferred) creation of services. This means that services are not actually created at the moment they are requested from the DI container, but only at the time of their first use. This can speed up application startup and reduce memory usage, as only the services actually needed for a given request are created. - -For a specific service, lazy creation can be [adjusted |services#Lazy Services]. - -.[note] -Lazy objects can only be used for user-defined classes, not for internal PHP classes. Requires PHP 8.4 or newer. - - -Metadata Export ---------------- - -The DI container class also contains a lot of metadata. You can reduce its size by reducing the metadata export. - -```neon -di: - export: - # export parameters? - parameters: false # (bool) defaults to true - - # export tags and which ones? - tags: # (string[]|bool) the default is all - - event.subscriber - - # export data for autowiring and which? - types: # (string[]|bool) the default is all - - Nette\Database\Connection - - Symfony\Component\Console\Application -``` - -If you don't use `$container->getParameters()`, you can disable parameter export. Furthermore, you can export only the tags that you actually use to retrieve services via `$container->findByTag(...)`. If you don't call this method at all, you can disable tag export completely using `false`. - -You can significantly reduce metadata for [autowiring|autowiring] by listing only the classes you actually request using `$container->getByType()`. Again, if you don't call this method (or only call it in the [bootstrap|application:bootstrapping] file, e.g., to get `Nette\Application\Application`), you can disable type export completely using `false`. - - -Extensions -========== - -Registration of additional DI extensions. This is how you add, for example, the DI extension `Dibi\Bridges\Nette\DibiExtension3` under the name `dibi`: - -```neon -extensions: - dibi: Dibi\Bridges\Nette\DibiExtension3 -``` - -You then configure it in the `dibi` section: - -```neon -dibi: - host: localhost -``` - -You can also add a class with parameters as an extension: - -```neon -extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, [%appDir%], %tempDir%/cache) -``` - - -Including Files -=============== - -Additional configuration files can be included in the `includes` section: - -```neon -includes: - - parameters.php - - services.neon - - presenters.neon -``` - -The name `parameters.php` is not a typo; the configuration can also be written in a PHP file that returns it as an array: - -```php -<?php -return [ - 'database' => [ - 'main' => [ - 'dsn' => 'sqlite::memory:', - ], - ], -]; -``` - -If items with the same keys appear in multiple configuration files, they will be overwritten, or [merged |#Merging] in the case of arrays. A file included later has higher priority than the previous one. The file in which the `includes` section is listed has a higher priority than the files included in it. - - -Search -====== - -Automatic registration of services in the DI container significantly simplifies development. Nette automatically adds presenters to the container, but you can easily add any other classes as well. - -Just specify in which directories (and subdirectories) the classes should be searched for: - -```neon -search: - - in: %appDir%/Forms - - in: %appDir%/Model -``` - -If you only need a single search rule, you can omit the list and write its keys directly under `search`: - -```neon -search: - in: %appDir% -``` - -Usually, however, we don't want to add absolutely all classes and interfaces, so we can filter them: - -```neon -search: - - in: %appDir%/Forms - - # filtering by file name (string|string[]) - files: - - *Factory.php - - # filtering by class name (string|string[]) - classes: - - *Factory -``` - -Or we can select classes that inherit or implement at least one of the listed classes: - - -```neon -search: - - in: %appDir% - extends: - - App\*Form - implements: - - App\*FormInterface -``` - -You can also define exclusion rules using class name masks or ancestors. If a class matches an exclusion rule, it won't be added to the DI container: - -```neon -search: - - in: %appDir% - exclude: - files: ... - classes: ... - extends: ... - implements: ... -``` - -Tags can be assigned to all automatically registered services: - -```neon -search: - - in: %appDir% - tags: ... -``` - -Besides classes, the search also registers interfaces that have a single `create()` or `get()` method - as [generated factories or accessors |factory]. Classes for which a service of the same type is already registered in the container are skipped, so no duplicates are created. - - -Merging -======= - -If elements with the same keys appear in multiple configuration files, they will be overwritten, or merged in the case of arrays. The later included file has higher priority than the previous one. - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>result</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> - <td> -```neon -items: - - 1 - - 2 - - 3 -``` - </td> -</tr> -</table> - -For arrays, merging can be prevented by adding an exclamation mark after the key name: - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>result</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items!: - - 3 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> -</tr> -</table> - -{{maintitle: Nette DI Container Configuration: Services and Parameters}} -{{description: Configure the Nette DI container in NEON: parameters, services, the decorator, extensions, including files, searching for services and merging.}} diff --git a/dependency-injection/en/container.texy b/dependency-injection/en/container.texy deleted file mode 100644 index df4dc113c0..0000000000 --- a/dependency-injection/en/container.texy +++ /dev/null @@ -1,142 +0,0 @@ -What Is a DI Container? -*********************** - -.[perex] -A Dependency Injection Container (DIC or DI container) is an object responsible for instantiating and configuring other objects (called services). - -It might surprise you, but in many cases, you don't need a dependency injection container to leverage the benefits of dependency injection (DI for short). After all, even in the [introductory chapter|introduction], we showed specific examples of DI, and no container was necessary. - -However, when managing a large number of objects with complex dependencies, a DI container becomes very useful. This is often the case for web applications built on a framework. - -In the previous chapter, we introduced the classes `Article` and `EditController`. Both have dependencies, namely the database and the factory `ArticleFactory`. And for these classes, we will now create a container. Of course, creating a container for such a simple example is overkill. But we'll create one to show how it looks and works. - -Here is a simple hardcoded container for the above example: - -```php -class Container -{ - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection('mysql:', 'root', '***'); - } - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->createDatabase()); - } - - public function createEditController(): EditController - { - return new EditController($this->createArticleFactory()); - } -} -``` - -The usage would look like this: - -```php -$container = new Container; -$controller = $container->createEditController(); -``` - -We simply request the object from the container, without needing to know how to create it or what its dependencies are; the container handles all of that. Dependencies are automatically injected by the container. That's its strength. - -Currently, the container has all the information hardcoded. So, let's take the next step and add parameters to make the container truly useful: - -```php -class Container -{ - public function __construct( - private array $parameters, - ) { - } - - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection( - $this->parameters['db.dsn'], - $this->parameters['db.user'], - $this->parameters['db.password'], - ); - } - - // ... -} - -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); -``` - -Sharp-eyed readers might notice a problem. Every time we retrieve an `EditController` object, new instances of `ArticleFactory` and the database connection are also created. We definitely don't want that. - -Therefore, we'll add a `getService()` method that will always return the same instances: - -```php -class Container -{ - private array $services = []; - - public function __construct( - private array $parameters, - ) { - } - - public function getService(string $name): object - { - if (!isset($this->services[$name])) { - // getService('Database') will call createDatabase() - $method = 'create' . $name; - $this->services[$name] = $this->$method(); - } - return $this->services[$name]; - } - - // ... -} -``` - -On the first call, for example `$container->getService('Database')`, it calls `createDatabase()` to create the database object, stores it in the `$services` array, and returns it. On subsequent calls, it returns the already stored instance directly. - -We also modify the rest of the container to use `getService()`: - -```php -class Container -{ - // ... - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->getService('Database')); - } - - public function createEditController(): EditController - { - return new EditController($this->getService('ArticleFactory')); - } -} -``` - -By the way, the term service refers to any object managed by the container. Hence the method name `getService()`. - -Done. We have a fully functional DI container! And we can use it: - -```php -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); - -$controller = $container->getService('EditController'); -$database = $container->getService('Database'); -``` - -As you can see, writing a DIC isn't difficult. It's worth noting that the objects themselves are unaware that a container is creating them. Consequently, it's possible to create any PHP object this way without modifying its source code. - -Manually creating and maintaining the container class can quickly become a nightmare. Therefore, in the next chapter, we will discuss the [Nette DI Container|nette-container], which can generate and update itself almost automatically. - -{{maintitle: What Is a Dependency Injection Container?}} -{{description: A DI container creates and configures your services. See what it does, how easy it is to write one by hand and why you will soon want one generated.}} diff --git a/dependency-injection/en/extensions.texy b/dependency-injection/en/extensions.texy deleted file mode 100644 index a20fe23637..0000000000 --- a/dependency-injection/en/extensions.texy +++ /dev/null @@ -1,446 +0,0 @@ -Creating Extensions for Nette DI -******************************** - -.[perex] -An extension is a class that hooks into the compilation of the DI container. It can register services programmatically, validate its own configuration section, modify services defined by others, and even alter the generated container code. This page teaches you how to write one, what happens when, and what to watch out for. - -Extensions are how packages integrate into Nette the native way: all `nette/*` packages use them, and yours can too. A typical extension does one or more of these things: - -- **integrates a library** - registers its services in the container and exposes a friendly, validated configuration section (that is where the `mail:` or `database:` sections come from) -- **automates registration** - registers many similar services in a loop or based on a rule, where listing them in `services:` would be tedious -- **makes cross-cutting changes** - finds services registered by others and completes them, e.g. attaches a logger to every service with a certain tag - -For everyday application work, you rarely need one - the [services |services] section of the configuration covers registering and wiring your classes. Reach for an extension when configuration alone stops being enough. - -An extension is activated in the `extensions` section. This is how you add an extension represented by the `BlogExtension` class under the name `blog`: - -```neon -extensions: - blog: BlogExtension -``` - -If its constructor takes arguments, pass them right there: - -```neon -extensions: - blog: BlogExtension(%debugMode%) -``` - - -How Compilation Works -===================== - -To write extensions confidently, you need to know one key thing: **when your code runs.** Nette does not wire services while handling requests. Instead, it *compiles* the container ahead of time: it reads all configuration files, lets extensions do their work, and generates an optimized PHP class, which it stores on disk. Every subsequent request just loads this finished class. Your extension code therefore runs only when the container is being (re)built - not on every request. - -This has an important consequence: during compilation, no services exist yet. What exists are **definitions** - recipes describing what class each service will be, how to create it, and what to call on it afterwards. The definitions live in the [ContainerBuilder |#ContainerBuilder] object. An extension is essentially *scriptable configuration*: anything you can declare in the `services:` section, you can also build in PHP - conditionally, in loops, or in reaction to what others have registered. - -Compilation proceeds in phases, and an extension can step into each of them: - -1) the configuration sections of all extensions are validated (`getConfigSchema()`) -2) each extension registers its services (`loadConfiguration()`); the user's `services:` section is processed last, so the application always has the final word -3) once all definitions are in place and service types are resolved, extensions may modify them (`beforeCompile()`) -4) the container class is generated; extensions can still adjust its code (`afterCompile()`) and emit code that will run when the application starts ([initialization |#Initialization Code]) - -.[note] -In developer mode, the container is automatically recompiled whenever you change a configuration file or the extension class itself - both are tracked as dependencies. So you can develop extensions without ever clearing a cache. - -.[tip] -For a deeper look at what happens in each phase - when parameters are expanded, when `@service` becomes a reference, and exactly when it is safe to search for services by type - see [Container Compilation Explained |compilation-internals]. - - -First Extension -=============== - -Here is a small but complete extension. We activate it and configure it in the same file: - -```neon -extensions: - blog: BlogExtension - -blog: - postsPerPage: 5 -``` - -And this is the whole class: - -```php -use Nette\Schema\Expect; - -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function getConfigSchema(): Nette\Schema\Schema - { - return Expect::structure([ - 'postsPerPage' => Expect::int(10), - 'allowComments' => Expect::bool(true), - ]); - } - - - public function loadConfiguration(): void - { - $builder = $this->getContainerBuilder(); - - $builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class, ['postsPerPage' => $this->config->postsPerPage]); - - if ($this->config->allowComments) { - $builder->addDefinition($this->prefix('comments')) - ->setFactory(Blog\Comments::class); - } - } -} -``` - -`getConfigSchema()` describes what the `blog:` section (named after the key under which we registered the extension) may contain, including types and default values - the validated values are then available in `$this->config`. In `loadConfiguration()`, we register the services. Note the names: `$this->prefix('articles')` produces `blog.articles`, so services of different extensions cannot clash. - -And the last few lines show why extensions exist at all: the `comments` service is only registered when comments are enabled. A plain configuration file cannot make such decisions. - -Services registered this way behave exactly as if they were written in `services:` - they are created lazily on demand, and autowiring passes them anywhere `Blog\Articles` is type-hinted. - -The following chapters describe the extension lifecycle in detail, then the [ContainerBuilder |#ContainerBuilder] API you will use inside the extension, and finally the [pitfalls |#Tips and Pitfalls] worth knowing about. - - -Extension Lifecycle -=================== - -An extension inherits from [api:Nette\DI\CompilerExtension] and overrides some of the four methods `getConfigSchema()`, `loadConfiguration()`, `beforeCompile()`, and `afterCompile()`, which the compiler calls in this order during compilation. - - -getConfigSchema(): Nette\Schema\Schema .[method] ------------------------------------------------- - -Defines the schema of the extension's configuration section. Thanks to it, users get validation and clear error messages for free: a typo or a wrong type in the `blog:` section is reported with an understandable message, without you writing a single check. - -The schema is described using the [Schema |schema:] library and can express types, default values, allowed values, and much more: - -```php -public function getConfigSchema(): Nette\Schema\Schema -{ - return Expect::structure([ - 'postsPerPage' => Expect::int(10), - 'storage' => Expect::anyOf('files', 'database')->firstIsDefault(), - ]); -} -``` - -The validated configuration is available in `$this->config` as an `stdClass` object (or as an array, if you append `castTo('array')` to the schema). - -If the value of an option cannot be known at compile time - it comes from an environment variable, for example - mark it with `dynamic()`, e.g. `Expect::int()->dynamic()`. More in [dynamic parameters |application:bootstrapping#Dynamic Parameters]. - - -loadConfiguration() .[method] ------------------------------ - -The place where the extension registers its services, using the [ContainerBuilder |#ContainerBuilder]: - -```php -public function loadConfiguration(): void -{ - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class); -} -``` - -If a service should also be available under a short name, add an alias. By convention, this is done only when the extension is registered under its usual name, so that multiple instances of the extension cannot fight over it: - -```php -if ($this->name === 'blog') { - $builder->addAlias('articles', $this->prefix('articles')); -} -``` - -When there are many services, it may be more convenient to define them in a separate NEON file using the familiar [services |services] syntax. The `@extension` prefix refers to the current extension: - -```neon -services: - articles: - create: MyBlog\ArticlesModel(@connection) - - comments: - create: MyBlog\CommentsModel(@connection, @extension.articles) -``` - -We load these definitions with `loadDefinitionsFromConfig()`; the names get prefixed automatically, and the file is tracked as a dependency, so changing it triggers recompilation: - -```php -public function loadConfiguration(): void -{ - $this->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/services.neon')['services'], - ); -} -``` - - -beforeCompile() .[method] -------------------------- - -When this method is called, the builder already holds **all** definitions: yours, those of other extensions, and those from the user's configuration files. Service types have also been resolved, so searching by type is reliable. That makes this phase ideal for inspecting and completing the final service graph. - -Typically, you search services by tag or by type and complete the found definitions: - -```php -public function beforeCompile(): void -{ - $builder = $this->getContainerBuilder(); - - foreach ($builder->findByTag('logaware') as $name => $attrs) { - $builder->getDefinition($name)->addSetup('setLogger'); - } -} -``` - -The `setLogger()` call has no explicit arguments - autowiring will supply them, just like it does in factories. - -You can also cooperate with other registered extensions, obtained via `$this->compiler->getExtensions()`, optionally filtered by class or interface: - -```php -foreach ($this->compiler->getExtensions(FooExtension::class) as $extension) { - // ... -} -``` - - -afterCompile(Nette\PhpGenerator\ClassType $class) .[method] ------------------------------------------------------------ - -In the last phase, the container class is generated as a [ClassType |php-generator:#Classes] object of the [PHP Generator |php-generator:] library. It contains a factory method for every service and is about to be written to the cache. You can still modify its code: - -```php -public function afterCompile(Nette\PhpGenerator\ClassType $class): void -{ - $method = $class->getMethod('__construct'); - // ... -} -``` - -You will need this phase only rarely. To add code that runs when the application starts, use initialization instead: - - -Initialization Code -------------------- - -All the previous phases influence how the container is *built*. In addition, an extension can emit code that runs at *runtime*, right after the container is created - for example, to start a session or launch services. The code is written into the `$this->initialization` object using its [addBody() |php-generator:#Method and Function Bodies] method: - -```php -public function loadConfiguration(): void -{ - // services with the 'run' tag must be created right after the container starts - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $attrs) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } -} -``` - -Nette itself uses initialization, for example, to automatically start the session or to send security HTTP headers. And keep in mind: unlike everything else in an extension, this code runs on **every request**, so keep it small. - - -ContainerBuilder -================ - -[api:Nette\DI\ContainerBuilder] is the object through which an extension talks to the compiler. It holds the [definitions |#How Compilation Works] of all services and offers methods for adding, looking up, and modifying them. You obtain it in `loadConfiguration()` and `beforeCompile()`: - -```php -$builder = $this->getContainerBuilder(); -``` - - -Adding Services ---------------- - -Registering a service is the same thing you do in the `services:` section of a NEON file - only written in PHP. Each configuration key has a matching method on the definition, so these two notations are equivalent: - -```neon -services: - articles: - create: Blog\Articles(@connection) - setup: - - setLogger(@logger) - tags: [logaware] -``` - -```php -$builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class, ['@connection']) - ->addSetup('setLogger', ['@logger']) - ->addTag('logaware'); -``` - -The definition returned by `addDefinition()` is a [ServiceDefinition |#Definition Types] offering the counterparts of the configuration keys: `setType()` (the service class), `setFactory()` (how to create it), `setArguments()`, `addSetup()`, `addTag()`, and `setAutowired()`. - -`addSetup()` mirrors the `setup:` list and accepts the same forms: a method call `addSetup('setLogger', ['@logger'])`, a property assignment `addSetup('$cache', ['@cache'])`, or a call on another service `addSetup('@Tracy\Bar::addPanel', [$panel])`. - -Besides ordinary services, the builder can also register [generated |factory] factories, accessors, and locators - each with its own method that returns the matching [definition type |#Definition Types]: - -| Method | Registers -|--------|---------- -| `addDefinition()` | an ordinary service (returns `ServiceDefinition`) -| `addFactoryDefinition()` | a generated [factory |factory] (interface with a `create()` method) -| `addAccessorDefinition()` | a generated [accessor |factory#Accessor] (interface with a `get()` method) -| `addLocatorDefinition()` | a [multifactory / locator |factory#Multifactory/Accessor] combining several factories -| `addImportedDefinition()` | a service passed to the container from outside at runtime -| `addAlias()` | a second name for an existing service - -With a factory, you configure the object it creates through `getResultDefinition()`; an accessor instead points to an existing service via `setReference()`: - -```php -$builder->addFactoryDefinition($this->prefix('latteFactory')) - ->setImplement(LatteFactory::class) - ->getResultDefinition() - ->setFactory(Latte\Engine::class) - ->addSetup('setStrictTypes', [true]); -``` - -`addLocatorDefinition()` and `addImportedDefinition()` are rarely needed - such services usually come from the `implement:` and imported-service keys in NEON rather than being written by hand. - - -Finding and Modifying Services ------------------------------- - -For searching and walking through the existing definitions, the builder provides: - -| Method | Description -|--------|------------ -| `getDefinition(string $name)` | the definition with the given name (throws if missing) -| `hasDefinition(string $name)` | whether a definition or alias with the name exists -| `getDefinitions()` | all definitions -| `removeDefinition(string $name)` | removes a definition -| `getByType(string $type)` | the name of the autowired service of the type, or `null` -| `getDefinitionByType(string $type)` | the autowired definition of the type -| `findByType(string $type)` | all definitions of the type as `name => definition` pairs -| `findByTag(string $tag)` | services carrying the tag as `name => tag value` pairs -| `addExcludedClasses(array $types)` | excludes classes and interfaces from autowiring - -A handy idiom is using `getByType()` to find out whether a service exists at all - for example, to hook into a logger only when the application has one: - -```php -if ($builder->getByType(Psr\Log\LoggerInterface::class)) { - $builder->getDefinition($this->prefix('articles')) - ->addSetup('setLogger'); -} -``` - - -Definition Types ----------------- - -Each `add*Definition()` method returns a different kind of definition. All of them extend the common ancestor `Nette\DI\Definitions\Definition`: - -- **`ServiceDefinition`** - an ordinary service; configured with `setType()`, `setFactory()`, `addSetup()`, `addTag()`, and `setAutowired()` -- **`FactoryDefinition`** - a [generated factory |factory]: an interface whose `create()` method returns a new object on each call -- **`AccessorDefinition`** - a [generated accessor |factory#Accessor]: an interface whose `get()` method returns an existing service -- **`LocatorDefinition`** - a [multifactory / locator |factory#Multifactory/Accessor] combining several factories or accessors in one interface -- **`ImportedDefinition`** - a service the container does not create itself but receives from outside at runtime - -Keep in mind that `getDefinition()` returns whatever kind of definition lives under the given name. If your code can encounter a generated factory, check the type first and configure the produced object via `getResultDefinition()`: - -```php -$def = $builder->getDefinition($name); -if ($def instanceof Nette\DI\Definitions\FactoryDefinition) { - $def = $def->getResultDefinition(); -} -$def->addSetup('setLogger'); -``` - - -Tips and Pitfalls -================= - - -Compile Time vs. Runtime ------------------------- - -The most common source of confusion: extension code runs when the container is being **compiled**, not when the application handles requests. In practice this means: - -- An extension never works with service instances - they don't exist yet. Don't instantiate services with `new`; register a definition and let the container create them. -- All configuration values are baked into the generated code. A value that can differ between environments (a path, a password from `getenv()`) must be marked as [dynamic |application:bootstrapping#Dynamic Parameters], otherwise it gets frozen at compile time. -- Strings passed to `$this->initialization->addBody()` are not executed now - they are PHP code emitted into the container, executed on every request. - - -File Dependencies ------------------ - -The container is recompiled when configuration files or extension classes change. But if your extension reads any other file - a list of entities, an XML configuration of a library - the container has no way of knowing about it. Register such files with: - -```php -$builder->addDependency($file); -``` - -Otherwise, you're in for a classic mystery: you edit the file, but the application keeps behaving the old way - the change only shows up once the container is rebuilt for some other reason. (Files read via `loadFromFile()` are tracked automatically.) - - -Conditional Registration ------------------------- - -An extension can adapt to its environment. Optional integrations are typically guarded by `class_exists()`: - -```php -if (class_exists(Symfony\Component\Console\Command\Command::class)) { - $builder->addDefinition($this->prefix('command')) - ->setFactory(Blog\Console\SitemapCommand::class); -} -``` - -And values like `%debugMode%` are best passed through the extension's constructor: - -```neon -extensions: - blog: BlogExtension(%debugMode%) -``` - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function __construct( - private bool $debugMode = false, - ) {} -} -``` - -A typical use is registering a Tracy panel only in developer mode. - - -Complex Arguments ------------------ - -Sometimes an argument for a factory or a setup call is not a plain value, a class name, or a `@service` reference. For those cases, there are: - -- `new Nette\DI\Definitions\Statement(Blog\Panel::class, [$args])` - an object created in place, an "anonymous service" used as an argument -- `new Nette\DI\Definitions\Reference('blog.articles')` - a reference to a service, the object counterpart of the `@name` string -- `$builder::literal('PHP_SAPI')` - a piece of raw PHP code inserted as-is into the generated container - -Example - registering a Tracy panel: - -```php -$builder->getDefinition($this->prefix('articles')) - ->addSetup('@Tracy\Bar::addPanel', [ - new Nette\DI\Definitions\Statement(Blog\ArticlesPanel::class), - ]); -``` - - -Exported Tags and Types ------------------------ - -The [metadata export |configuration#Metadata Export] can be restricted in the configuration so that the compiled container keeps only the tags and autowiring types the application actually uses. If your extension retrieves services at runtime using `$container->findByTag()` or `$container->getByType()`, such a restriction could remove the very metadata you rely on. - -To prevent this, tell the compiler which tags and types must always be exported: - -```php -public function loadConfiguration(): void -{ - // this tag will always be exported, even when the export is restricted - $this->compiler->addExportedTag('event.subscriber'); - - // this type will always be available for getByType() - $this->compiler->addExportedType(Nette\Database\Connection::class); -} -``` - -Both methods only add to the exported metadata; they never override the application's `di › export` configuration. So when the application restricts the export to a list, the tags and types your extension needs stay included; only switching the tags export off entirely (`tags: false`) discards them along with everything else. - -{{maintitle: Creating Nette DI Extensions: Register Services in Code}} -{{description: Write a Nette DI extension that registers services in code, validates its own configuration section, modifies other services and alters the generated container.}} diff --git a/dependency-injection/en/factory.texy b/dependency-injection/en/factory.texy deleted file mode 100644 index 5a760350ce..0000000000 --- a/dependency-injection/en/factory.texy +++ /dev/null @@ -1,236 +0,0 @@ -Generated Factories -******************* - -.[perex] -Nette DI can automatically generate factory code based on interfaces, saving you from writing code. - -A factory is a class that is responsible for creating objects and passing their dependencies. Please do not confuse this with the *factory method* design pattern, which describes a specific way of using factories and is unrelated to this topic. - -We have shown what such a factory looks like in the [introductory chapter |introduction#Factory]: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Nette DI can automatically generate factory code. All you need to do is create an interface, and Nette DI will generate the implementation. The interface must have exactly one method named `create` and declare a return type: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -So, the factory `ArticleFactory` has a method `create` that creates `Article` objects. The `Article` class might look like this, for example: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } -} -``` - -Add the factory to the configuration file: - -```neon -services: - - ArticleFactory -``` - -Nette DI will generate the corresponding factory implementation. - -In the code that uses the factory, request the object by its interface, and Nette DI will provide the generated implementation: - -```php -class UserController -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function foo() - { - // let the factory create an object - $article = $this->articleFactory->create(); - } -} -``` - - -Parameterized Factory -===================== - -The factory method `create` can accept parameters, which it then passes to the constructor. For example, let's add the article author's ID to the `Article` class: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - private int $authorId, - ) { - } -} -``` - -We will also add the parameter to the factory: - -```php -interface ArticleFactory -{ - function create(int $authorId): Article; -} -``` - -Since the parameter name in the constructor (`$authorId`) matches the parameter name in the factory method, Nette DI automatically passes it. - - -Advanced Definition -=================== - -The definition can also be written in multi-line form using the `implement` key: - -```neon -services: - articleFactory: - implement: ArticleFactory -``` - -Using this longer format allows specifying additional arguments for the constructor via the `arguments` key and further configuration using `setup`, similar to regular service definitions. - -Example: If the `create()` method didn't accept the `$authorId` parameter, we could provide a fixed value in the configuration to be passed to the `Article` constructor: - -```neon -services: - articleFactory: - implement: ArticleFactory - arguments: - authorId: 123 -``` - -Conversely, if `create()` accepted `$authorId`, but it wasn't part of the constructor and was instead passed via a method like `Article::setAuthorId()`, we would reference the parameter in the `setup` section: - -```neon -services: - articleFactory: - implement: ArticleFactory - setup: - - setAuthorId($authorId) -``` - - -Accessor -======== - -Besides factories, Nette can also generate so-called accessors. These are objects with a `get()` method that returns a specific service from the DI container. Repeated calls to `get()` always return the same instance. - -Accessors provide lazy-loading for dependencies. Consider a class that logs errors to a dedicated database. If this class received the database connection via constructor dependency injection, the connection would always be established, even if errors occur rarely and the connection remains unused most of the time. Instead, the class can receive an accessor. The database object (connection) is only created when the accessor's `get()` method is called for the first time. - -How to create an accessor? Just write an interface, and Nette DI will generate the implementation. The interface must have exactly one method named `get` that takes no parameters and declares the return type: - -```php -interface PDOAccessor -{ - function get(): PDO; -} -``` - -Add the accessor to the configuration file, along with the definition of the service it should return: - -```neon -services: - - PDOAccessor - - PDO(%dsn%, %user%, %password%) -``` - -Because the accessor returns a `PDO` service, and there's only one such service defined in the configuration, the accessor will return that specific service. If multiple services of that type exist, specify which one the accessor should return by name, e.g., `- PDOAccessor(@db1)`. - - -Multifactory/Accessor -===================== - -So far, our factories and accessors could only create or return a single type of object. However, you can easily create multifactories, which combine features of factories and accessors. The interface for such a component can contain multiple methods named `create<Name>()` and `get<Name>()`, for example: - -```php -interface MultiFactory -{ - function createArticle(): Article; - function getDb(): PDO; -} -``` - -So, instead of injecting multiple individual factories and accessors, you can inject a single, more comprehensive component. - -Alternatively, instead of multiple methods, `get()` with a parameter can be used: - -```php -interface MultiFactoryAlt -{ - function get($name): PDO; -} -``` - -Then, `MultiFactory::getDb()` does the same thing as `MultiFactoryAlt::get('db')`. However, this alternative notation has the disadvantage that the supported values for `$name` are not explicitly clear from the interface signature. Additionally, you cannot define different return types for different `$name` values within the interface. - -Instead of `get($name)`, the interface can declare `create($name)`, which returns a new instance on every call (whereas `get()` returns a shared one). The interface may contain only one such parameterized method. If the method's return type is nullable (e.g. `?PDO`), it returns `null` for an unknown `$name` instead of throwing an exception. - - -Definition with a List ----------------------- -You can define a multifactory in the configuration using a list, with the services written inline: .{data-version:3.2.0} - -```neon -services: - - MultiFactory( - article: Article() # defines createArticle() - db: PDO(%dsn%, %user%, %password%) # defines getDb() - ) -``` - -Alternatively, you can refer to existing services in the multifactory definition using references: - -```neon -services: - article: Article - - PDO(%dsn%, %user%, %password%) - - MultiFactory( - article: @article # defines createArticle() - db: @\PDO # defines getDb() - ) -``` - - -Definition with Tags --------------------- - -Another way to define a multifactory is to use [tags |services#Tags]. The value of the tag determines the name of the corresponding method: - -```neon -services: - article: - create: Article - tags: {multi: article} # defines createArticle() - db: - create: PDO(%dsn%, %user%, %password%) - tags: {multi: db} # defines getDb() - - - MultiFactory(tagged: multi) -``` - -{{maintitle: Nette DI Generated Factories: From Interface to Code}} -{{description: Write only an interface and Nette DI generates the factory code for you. Parameterized factories, accessors, multifactories and advanced definitions.}} diff --git a/dependency-injection/en/faq.texy b/dependency-injection/en/faq.texy deleted file mode 100644 index 2e9b2b6d05..0000000000 --- a/dependency-injection/en/faq.texy +++ /dev/null @@ -1,126 +0,0 @@ -DI Frequently Asked Questions (FAQ) -*********************************** - - -Is DI another name for IoC? ---------------------------- - -*Inversion of Control* (IoC) is a principle describing the flow of control in a program: is your code calling external code, or is external code (like a framework) calling your code? IoC is a broad concept that includes [events |nette:glossary#Events], the so-called [Hollywood Principle |application:components#Hollywood Style], and other aspects. This concept also includes factories, discussed in [Rule #3: Let the Factory Handle It |introduction#Rule #3: Let the Factory Handle It], which represent an inversion of the `new` operator. - -*Dependency Injection* (DI) focuses on how objects obtain their dependencies (i.e., other objects they need to work with). It's a design pattern advocating for passing dependencies explicitly to objects rather than having objects create or find them. - -Therefore, DI can be considered a specific form of IoC. However, not all forms of IoC promote clean code. For example, anti-patterns include techniques relying on [global state|global-state] or the [Service Locator |#What is a Service Locator] pattern. - - -What is a Service Locator? --------------------------- - -It's an alternative approach to Dependency Injection. It involves a central object (the locator) where all available services (dependencies) are registered. When an object needs a dependency, it requests it from the Service Locator. - -However, compared to DI, it lacks transparency. Dependencies are hidden within the object's code (calls to the locator) rather than being explicit in its API (constructor or methods), requiring code inspection to understand the connections. Testing is also more complex, as you can't simply pass mock dependencies when instantiating an object; you often need to manipulate the Service Locator itself. Furthermore, the Service Locator introduces an unnecessary dependency: objects become coupled to the locator, unlike with DI, where objects ideally remain unaware of the container. - - -When is it better not to use DI? --------------------------------- - -There are no known significant drawbacks to using the Dependency Injection design pattern correctly. On the contrary, obtaining dependencies from globally accessible locations (like static properties or singletons) leads to [numerous complications|global-state], as does using a Service Locator. Therefore, using DI is generally always advisable. This isn't dogma; it's simply that no better alternative for managing dependencies in a clean way has been widely adopted. - -However, there are specific, limited situations where accessing objects globally might be acceptable. For example, during debugging, when you need to dump a variable's value, measure execution time, or log a message at a specific point. In these cases, involving temporary actions that will later be removed from the code, using a globally accessible dumper, timer, or logger can be legitimate. These tools are not part of the application's core design. - - -Does using DI have its drawbacks? ---------------------------------- - -Does using Dependency Injection introduce disadvantages, like increased coding effort or reduced performance? What do we lose when we start writing code in accordance with DI? - -DI itself has negligible impact on runtime performance or memory usage. The performance of the DI container can be a factor, but [Nette DI |nette-container] compiles the container into plain PHP code, resulting in virtually zero overhead during application execution. - -When writing code following DI principles, you often need to create constructors that accept dependencies. While this might have seemed tedious in the past, modern IDEs and features like PHP 8's [constructor property promotion |https://blog.nette.org/en/php-8-0-complete-overview-of-news#toc-constructor-property-promotion] make it very quick. Factories can often be generated automatically by Nette DI, further reducing boilerplate. On the other hand, you eliminate the need to write singletons and static accessors. - -Overall, a well-designed application using DI is typically neither significantly shorter nor longer than one relying on singletons or global access. The code related to dependency creation and wiring is simply moved from individual classes to dedicated locations: the DI container configuration and factories. - - -How to rewrite a legacy application to DI? ------------------------------------------- - -Migrating from a legacy application to Dependency Injection can be a challenging process, especially for large and complex applications. It is important to approach this process systematically. - -- When moving to Dependency Injection, it is important that all team members understand the principles and practices being used. -- First, analyze the existing application to identify key components and their dependencies. Create a plan for which parts will be refactored and in what order. -- Implement a DI container or, better yet, use an existing library such as Nette DI. -- Gradually refactor parts of the application to use Dependency Injection. This may involve modifying constructors or methods to accept dependencies as parameters. -- Update the code where objects are instantiated to retrieve them from the container or use factories provided by the container. - -Remember that transitioning to Dependency Injection is an investment in code quality and long-term application maintainability. While it may be challenging to make these changes, the result should be cleaner, more modular, and easily testable code that is ready for future extensions and maintenance. - - -Why is composition preferred over inheritance? ----------------------------------------------- -Using [composition |nette:introduction-to-object-oriented-programming#Composition] is generally preferred over [inheritance |nette:introduction-to-object-oriented-programming#Inheritance] for code reuse because it leads to looser coupling. With composition, you are less likely to face issues where changing a base class breaks dependent subclasses. A typical example is a situation referred to as [constructor hell |passing-dependencies#Constructor Hell]. - - -Can Nette DI Container be used outside of Nette? ------------------------------------------------- - -Absolutely. The Nette DI Container is part of Nette, but is designed as a standalone library that can be used independently of other parts of the framework. Just install it using Composer, create a configuration file defining your services, and then use a few lines of PHP code to create the DI container. And you can immediately start taking advantage of Dependency Injection in your projects. - -The chapter on [Nette DI Container |nette-container] describes a specific use case with code examples. - - -Why is the configuration in NEON files? ---------------------------------------- - -NEON is a simple and easily readable configuration language developed within Nette for setting up applications, services, and their dependencies. Compared to JSON or YAML, it offers much more intuitive and flexible options for this purpose. In NEON, you can naturally describe service definitions and relationships that would be difficult or impossible to express as clearly in JSON or YAML. - - -Does parsing NEON files slow down the application? --------------------------------------------------- - -Although NEON files parse very quickly, their parsing speed is largely irrelevant in production. This is because configuration files are parsed only once when the application first runs (or when they change). After parsing, the DI container code is generated, cached (stored on disk), and this compiled PHP code is executed on every subsequent request, eliminating the need for further parsing. - -This is how it works in a production environment. During development, NEON files are parsed every time their content changes, ensuring that the developer always has an up-to-date DI container. As mentioned, the parsing itself is very fast. - - -How do I access the parameters from the configuration file in my class? ------------------------------------------------------------------------ - -Keep in mind [Rule #1: Let It Be Passed to You |introduction#Rule #1: Let It Be Passed to You]. If a class needs information from the configuration file, don't try to figure out how the class can *fetch* it. Instead, simply request it, for example, via the class constructor. Then, provide this value in the configuration file. - -In this example, `%myParameter%` is a placeholder for the value of the `myParameter` parameter, which will be passed to the `MyClass` constructor: - -```neon -# config.neon -parameters: - myParameter: Some value - -services: - - MyClass(%myParameter%) -``` - -If you want to pass multiple parameters or use autowiring, it is useful to [wrap the parameters in an object |best-practices:passing-settings-to-presenters]. - - -Does Nette support PSR-11 Container interface? ----------------------------------------------- - -Yes. The class `Nette\Bridges\DIPsr\PsrContainer` exposes the [Nette DI Container |api:Nette\DI\Container] through the PSR-11 `ContainerInterface`, see [PSR-11 Container |nette-container#PSR-11 Container]. - - -What do the terms container, compiler, definition, etc. mean? -------------------------------------------------------------- - -A short vocabulary of the words that keep coming up around Nette DI, most of them when [writing extensions |extensions]: - -- **Container** - the compiled object (`Nette\DI\Container`) that creates services on demand and holds them at runtime. It is generated once as optimized PHP code. -- **Compiler** - the machinery that turns configuration files and extensions into that container class. -- **ContainerBuilder** - the mutable model of the container used during compilation; it holds the service definitions before any real service exists. See [Creating Extensions |extensions#ContainerBuilder]. -- **Service** - an object managed by the container, usually created once and shared (a singleton) - a database connection, a mailer, a logger. -- **Definition** - the recipe for a service: its type, how to create it, and what to do afterwards. Nette turns definitions into the container's factory methods; several kinds exist (see [definition types |extensions#Definition Types]). -- **Type** - the class or interface of a service, used by autowiring to match services to the places that require them. -- **Autowiring** - automatically passing services to constructors and methods by their type, so you don't wire dependencies by hand. -- **Tag** - a label attached to a definition (optionally with a value); an extension can then find all services carrying it with `findByTag()`. -- **Setup** - additional calls performed on a service right after it is created - method calls or property assignments, added with `addSetup()`. -- **Alias** - an alternative name for an existing service. - -{{maintitle: Dependency Injection FAQ: IoC, Service Locator and More}} -{{description: Is DI another name for IoC? What is a service locator? When is it better not to use DI? Clear answers to the most frequent dependency injection questions.}} diff --git a/dependency-injection/en/global-state.texy b/dependency-injection/en/global-state.texy deleted file mode 100644 index 706c957ea5..0000000000 --- a/dependency-injection/en/global-state.texy +++ /dev/null @@ -1,299 +0,0 @@ -Global State and Singletons -*************************** - -.[perex] -Warning: The following constructs are symptoms of poorly designed code: - -- `Foo::getInstance()` -- `DB::insert(...)` -- `Article::setDb($db)` -- `ClassName::$var` or `static::$var` - -Do any of these constructs appear in your code? If so, you have an opportunity for improvement. You might think these are common constructs, perhaps seen in example solutions from various libraries and frameworks. If so, their code design is flawed. - -We're not talking about some academic purity here. All these constructs share one common trait: they utilize global state. And global state has a detrimental impact on code quality. Classes become deceptive about their dependencies. The code becomes unpredictable. It confuses developers and reduces their efficiency. - -In this chapter, we'll explain why this is the case and how to avoid global state. - - -Global Interlinking -------------------- - -In an ideal world, an object should only communicate with objects that were [directly passed to it |passing-dependencies]. If I create two objects `A` and `B` and never pass a reference between them, then neither `A` nor `B` can access or modify the other's state. This is a highly desirable property of code. It's akin to having a battery and a light bulb; the bulb won't light up until you connect it to the battery with a wire. - -However, this doesn't hold true for global (static) variables or singletons. Object `A` could *wirelessly* access object `C` and modify it without any reference passing, by calling `C::changeSomething()`. If object `B` also taps into the global `C`, then `A` and `B` can influence each other through `C`. - -Using global variables introduces a new form of *wireless* coupling, invisible from the outside. It creates a smokescreen, making the code harder to understand and use. To truly grasp the dependencies, developers must read every line of source code, instead of just relying on the class interfaces. Furthermore, this coupling is entirely unnecessary. Global state is used because it's easily accessible from anywhere and allows, for instance, writing to a database through a global (static) method `DB::insert()`. However, as we will demonstrate, the perceived convenience is minimal compared to the severe complications it introduces. - -.[note] -In terms of behavior, there is no difference between a global and a static variable. They are equally harmful. - - -The Spooky Action at a Distance -------------------------------- - -"Spooky action at a distance" - that's what Albert Einstein famously called a phenomenon in quantum physics that gave him the creeps. -It refers to quantum entanglement, where measuring a property of one particle instantaneously affects another entangled particle, regardless of the distance separating them, even millions of light-years, which seemingly violates the fundamental law of the universe that nothing can travel faster than light. - -In the software world, 'spooky action at a distance' describes a situation where we execute a process believed to be isolated (since no dependencies were explicitly passed), yet unexpected interactions and state changes occur in distant parts of the system, unbeknownst to us. This can only occur through global state. - -Imagine joining a development team on a project with a large, mature codebase. Your new lead asks you to implement a new feature and, like a good developer, you start by writing a test. But because you're new to the project, you do a lot of exploratory 'what happens if I call this method' type tests. And you try to write the following test: - -```php -function testCreditCardCharge() -{ - $cc = new CreditCard('1234567890123456', 5, 2028); // your card number - $cc->charge(100); -} -``` - -You run the code, perhaps several times, and after a while, you notice bank notifications on your phone: $100 has been charged to your credit card with each execution! 🤦‍♂️ - -How on earth could the test cause an actual charge? Operating with a credit card isn't simple. You need to interact with a third-party web service, know its URL, authenticate, and so forth. None of this information is included in the test. Worse still, you don't know where this information resides, making it impossible to mock the external dependencies to prevent the $100 charge on each test run. And as a new developer, how were you supposed to know that what you were about to do would lead to you being $100 poorer? - -That's a spooky action at a distance! - -You're forced to sift through extensive source code and consult senior colleagues to understand the project's interconnections. This difficulty arises because the `CreditCard` class interface doesn't reveal the necessary global state initialization. Even examining the class's source code might not reveal which initialization method to call. At best, you might find the global variable being accessed and attempt to deduce how to initialize it. - -The classes in such a project are pathological liars. The `CreditCard` class pretends it can be simply instantiated and its `charge()` method called. However, it secretly interacts with another class, `PaymentGateway`, representing the payment gateway. Even the `PaymentGateway` interface might suggest independent initialization, but in reality, it might pull credentials from a configuration file, and so on. The original developers understand that `CreditCard` requires `PaymentGateway`. They wrote the code this way. But for newcomers, it's a complete mystery that hinders their ability to learn and contribute effectively. - -How to fix the situation? Easy. **Let the API declare dependencies.** - -```php -function testCreditCardCharge() -{ - $gateway = new PaymentGateway(/* ... */); - $cc = new CreditCard('1234567890123456', 5, 2028); - $cc->charge($gateway, 100); -} -``` - -Notice how the interdependencies within the code become immediately apparent. Because the `charge()` method declares its need for a `PaymentGateway`, you no longer need to guess or ask about this dependency. You know you need to create an instance, and in doing so, you'll discover the required access parameters. Without them, the code wouldn't even run. - -And most importantly, you can now mock the payment gateway so you won't be charged $100 every time you run a test. - -Global state allows objects to secretly access dependencies not declared in their APIs, effectively turning your APIs into pathological liars. - -You may not have thought of it this way before, but whenever you use global state, you're creating secret wireless communication channels. This spooky action at a distance forces developers to read every line of code to understand potential interactions, reducing productivity and confusing new team members. If you're the one who created the code, you know the real dependencies, but anyone who comes after you is clueless. - -Avoid writing code that relies on global state; prefer passing dependencies explicitly. Embrace dependency injection. - - -Fragility of Global State -------------------------- - -In code utilizing global state and singletons, you can never be certain when or by whom the state was modified. This risk manifests even during initialization. The following code intends to create a database connection and initialize a payment gateway, but it repeatedly throws exceptions, and debugging the cause is extremely tedious: - -```php -PaymentGateway::init(); -DB::init('mysql:', 'user', 'password'); -``` - -You must meticulously trace the code to discover that the `PaymentGateway` object wirelessly accesses other objects, some requiring a database connection. Therefore, the database must be initialized before `PaymentGateway`. However, the smokescreen of global state hides this from you. How much time would be saved if the APIs of these classes were honest and declared their dependencies? - -```php -$db = new DB('mysql:', 'user', 'password'); -$gateway = new PaymentGateway($db, /* ... */); -``` - -A similar problem arises when using global access to a database connection: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public function save(): void - { - DB::insert(/* ... */); - } -} -``` - -When calling the `save()` method, it's uncertain whether a database connection has been established or who is responsible for establishing it. If we need to change the database connection dynamically (e.g., for testing), we might resort to adding methods like `DB::reconnect(...)` or `DB::reconnectForTest()`. - -Consider an example: - -```php -$article = new Article; -// ... -DB::reconnectForTest(); -Foo::doSomething(); -$article->save(); -``` - -How can we be sure that the test database is actually used when `$article->save()` is called? What if the `Foo::doSomething()` method changed the global database connection? To determine this, we'd need to inspect the source code of `Foo` and potentially many other classes. This investigation would only provide a temporary answer, as the situation could change later. - -What if we move the database connection to a static variable inside the `Article` class? - -```php -class Article -{ - private static DB $db; - - public static function setDb(DB $db): void - { - self::$db = $db; - } - - public function save(): void - { - self::$db->insert(/* ... */); - } -} -``` - -This doesn't change anything at all. The problem is the global state itself, regardless of which class it's hidden within. In this scenario, just like the previous one, when `$article->save()` is called, we have no certainty about which database the data will be written to. Anyone, anywhere in the application, could have changed the database at any time using `Article::setDb()`. Without our knowledge. - -The global state makes our application **extremely fragile**. - -However, there is a simple way to deal with this problem. Just have the API declare dependencies to ensure proper functionality. - -```php -class Article -{ - public function __construct( - private DB $db, - ) { - } - - public function save(): void - { - $this->db->insert(/* ... */); - } -} - -$article = new Article($db); -// ... -Foo::doSomething(); -$article->save(); -``` - -This approach eliminates concerns about hidden or unexpected changes to the database connection. We now have certainty about where the article is being saved, and modifications within unrelated classes can no longer affect it. The code is no longer fragile, but stable. - -Avoid writing code that relies on global state; prefer passing dependencies explicitly. Embrace dependency injection. - - -Singleton ---------- - -Singleton is a design pattern that, by [definition |https://en.wikipedia.org/wiki/Singleton_pattern] from the famous Gang of Four publication, restricts a class to a single instance and offers global access to it. The implementation of this pattern usually resembles the following code: - -```php -class Singleton -{ - private static self $instance; - - public static function getInstance(): self - { - self::$instance ??= new self; - return self::$instance; - } - - // and other methods that perform the functions of the class -} -``` - -Unfortunately, the singleton introduces global state into the application. And as we have shown above, global state is undesirable. That's why the singleton is considered an antipattern. - -Don't use singletons in your code and replace them with other mechanisms. You really don't need singletons. However, if you need to ensure only one instance of a class exists throughout the application, delegate this responsibility to the [DI container |container]. This creates an application-scoped singleton, commonly referred to as a service. The class itself is then freed from managing its uniqueness (i.e., it won't have a `getInstance()` method or a static instance property) and can focus solely on its responsibilities. Thus, it will stop violating the single responsibility principle. - - -Global State Versus Tests -------------------------- - -When writing tests, we ideally assume each test is an isolated unit, with no external state entering or leaving it. After a test completes, any state associated with it should be automatically cleaned up by the garbage collector. This makes the tests isolated. Therefore, we can run the tests in any order. - -However, when global states or singletons are present, these beneficial assumptions crumble. State can leak into and out of tests. Suddenly, the order of the tests may matter. - -To even test code involving singletons, developers often have to compromise their integrity, for example, by allowing the singleton instance to be replaced. Such solutions are, at best, hacks that lead to code that is difficult to maintain and understand. Any test (or its `tearDown()` method) that modifies global state must meticulously revert those changes. - -Global state is the biggest headache in unit testing! - -How to fix this? Simple. Avoid writing code that uses singletons; prefer passing dependencies explicitly. Embrace dependency injection. - - -Global Constants ----------------- - -Global state is not limited to the use of singletons and static variables, but can also apply to global constants. - -Constants whose values represent universal truths (`M_PI`) or provide self-contained information (`PREG_BACKTRACK_LIMIT_ERROR`) are generally acceptable. Conversely, constants used as a way to *wirelessly* inject information into code are effectively hidden dependencies. Like `LOG_FILE` in the following example. Using the `FILE_APPEND` constant is perfectly correct. - -```php -const LOG_FILE = '...'; - -class Foo -{ - public function doSomething() - { - // ... - file_put_contents(LOG_FILE, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Instead, we should declare the log file path as a parameter in the `Foo` class's constructor, making it an explicit part of its API: - -```php -class Foo -{ - public function __construct( - private string $logFile, - ) { - } - - public function doSomething() - { - // ... - file_put_contents($this->logFile, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Now, we explicitly pass the path to the log file. We can easily change it as needed, which simplifies testing and code maintenance. - - -Global Functions and Static Methods ------------------------------------ - -We want to emphasize that using static methods and global functions is not inherently problematic. We explained the issues with methods like `DB::insert()`, but the core problem was always the underlying global state, typically stored in a static variable. The `DB::insert()` method relies on a static variable to hold the database connection. Without this variable, it would be impossible to implement the method. - -Using deterministic static methods and functions like `Closure::fromCallable()`, `strlen()`, and many others is perfectly compatible with dependency injection. These functions are predictable because they always return the same result for the same input parameters. They do not use any global state. - -However, there are functions in PHP that are not deterministic. These include, for example, the `htmlspecialchars()` function. Its third parameter, `$encoding`, if omitted, defaults to the value of the `default_charset` configuration option (`ini_get('default_charset')`). Therefore, it's recommended to always specify this parameter to prevent potential unpredictable behavior. Nette consistently does this. - -Some functions, like `strtolower()` and `strtoupper()`, exhibited non-deterministic behavior in the recent past, depending on the locale setting (`setlocale()`). This caused many complications, most often when working with the Turkish language. This is because Turkish distinguishes between dotted and dotless 'I' in both lowercase and uppercase. Consequently, `strtolower('I')` returned `ı` (dotless lowercase i), and `strtoupper('i')` returned `İ` (dotted uppercase I), leading to numerous mysterious application errors. However, this problem was fixed in PHP version 8.2 and the functions are no longer locale-dependent. - -This serves as a good example of how global state (locale setting) troubled thousands of developers worldwide. The eventual solution involved making the functions locale-independent, effectively removing the hidden dependency. - - -When Is It Possible to Use Global State? ----------------------------------------- - -There are specific, limited situations where using global state might be acceptable. For example, during debugging, when you need to dump a variable's value or measure the execution time of a specific code segment. In these cases, involving temporary actions that will later be removed from the code, using a globally accessible dumper or timer can be legitimate. These tools are not part of the application's core design. - -Another example involves PHP's regular expression functions (`preg_*`), which internally cache compiled regular expressions in static memory. When you call functions with the same regular expression multiple times across your code, the expression is compiled only once. This caching improves performance and is entirely invisible to the user, making this use of internal static state generally acceptable. - - -Summary -------- - -We have discussed why it makes sense to: - -1) Eliminate all mutable static properties (global state) from your code -2) Explicitly declare dependencies -3) And utilize dependency injection - -When designing your code, remember that every mutable `static $foo` is a potential source of problems. To create a DI-friendly environment, it's crucial to completely eliminate global state and replace it with dependency injection. - -During this process, you might discover the need to split classes that have multiple responsibilities. Don't hesitate to do so; strive for the Single Responsibility Principle. - -Would you rather experience all this than just read about it? The course [Dependency Injection by Example |https://github.com/nette-examples/di-by-example] has a chapter where an innocent-looking test quietly drains an account, and you can run it yourself. - -*I would like to thank Miško Hevery, whose articles such as [Flaw: Brittle Global State & Singletons |https://web.archive.org/web/20230321084133/http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/] form the basis of this chapter.* - -{{maintitle: PHP Singleton and Global State: Why to Avoid Them}} -{{description: Singletons, static methods and global constants are symptoms of poor design. See why global state makes code fragile and hard to test, and when it is fine.}} diff --git a/dependency-injection/en/introduction.texy b/dependency-injection/en/introduction.texy deleted file mode 100644 index 2237ad36f4..0000000000 --- a/dependency-injection/en/introduction.texy +++ /dev/null @@ -1,528 +0,0 @@ -What is Dependency Injection? -***************************** - -.[perex] -This chapter introduces the basic programming practices you should follow when writing any application. These are the fundamentals necessary for writing clean, understandable, and maintainable code. - -If you adopt and follow these rules, Nette will support you at every step. It will handle routine tasks for you and provide maximum convenience, allowing you to focus on the logic itself. - -The principles we will demonstrate here are quite simple. There's nothing to fear. - - -Remember Your First Program? ----------------------------- - -We don't know what language you wrote it in, but if it were PHP, it probably looked something like this: - -```php -function addition(float $a, float $b): float -{ - return $a + $b; -} - -echo addition(23, 1); // prints 24 -``` - -A few trivial lines of code, yet they hide so many key concepts. That variables exist. That code is divided into smaller units, such as functions. That we pass input arguments to them and they return results. Only conditions and loops are missing. - -The fact that we pass input data to a function and it returns a result is a perfectly understandable concept, used in other fields as well, like mathematics. - -A function has its signature, consisting of its name, a list of parameters and their types, and finally, the return value type. As users, we are interested in the signature; we usually don't need to know anything about the internal implementation. - -Now imagine the function signature looked like this: - -```php -function addition(float $x): float -``` - -Summation with one parameter? That's strange… How about this? - -```php -function addition(): float -``` - -That's really strange now, isn't it? How is the function used? - -```php -echo addition(); // what will it print? -``` - -Looking at such code, we would be confused. Not only would a beginner not understand it, but even a skilled programmer wouldn't understand such code. - -Are you wondering what such a function would actually look like inside? Where would it get the numbers to add? It would probably procure them *somehow* itself, perhaps like this: - -```php -function addition(): float -{ - $a = Input::get('a'); - $b = Input::get('b'); - return $a + $b; -} -``` - -In the function body, we discovered hidden dependencies on other global functions or static methods. To find out where the numbers actually come from, we need to investigate further. - - -Not This Way! -------------- - -The design we just showed is the essence of many negative characteristics: - -- The function signature pretended it didn't need the numbers to add, which confused us. -- We have no idea how to make the function sum two different numbers. -- We had to look into the code to find out where it gets the numbers. -- We discovered hidden dependencies. -- Fully understanding it requires examining these dependencies as well. - -And is it even the task of the summation function to obtain the inputs? Of course not. Its responsibility is only the summation itself. - - -We don't want to encounter such code, and we certainly don't want to write it. The fix is simple: return to the basics and simply use parameters: - - -```php -function addition(float $a, float $b): float -{ - return $a + $b; -} -``` - - -Rule #1: Let It Be Passed to You --------------------------------- - -The most important rule is: **all the data that functions or classes require must be provided to them**. - -Instead of inventing hidden ways for them to obtain the data, simply provide the parameters. You will save time spent inventing hidden pathways that definitely won't improve your code. - -If you always follow this rule everywhere, you are on the path towards code without hidden dependencies. Towards code that is understandable not only to the author but also to anyone who reads it later. Where everything is understandable from the signatures of functions and classes, and there's no need to search for hidden details in the implementation. - -This technique is professionally called **Dependency Injection**. And the data are called **dependencies.** It's just plain parameter passing, nothing more. - -.[note] -Please do not confuse Dependency Injection, which is a design pattern, with a "Dependency Injection container," which is a tool, something fundamentally different. We will discuss containers later. - - -From Functions to Classes -------------------------- - -And how does this apply to classes? A class is a more complex entity than a simple function, but Rule #1 applies fully here as well. There are just [more ways to pass arguments |passing-dependencies]. For example, quite similarly to the function case: - -```php -class Math -{ - public function sum(float $a, float $b): float - { - return $a + $b; - } -} - -$math = new Math; -echo $math->sum(23, 1); // 24 -``` - -Or using other methods, or directly the constructor: - -```php -class Sum -{ - public function __construct( - private float $a, - private float $b, - ) { - } - - public function calculate(): float - { - return $this->a + $this->b; - } -} - -$sum = new Sum(23, 1); -echo $sum->calculate(); // 24 -``` - -Both examples are fully compliant with Dependency Injection. - - -Real-Life Examples ------------------- - -In the real world, you won't be writing classes for summing numbers. Let's move on to practical examples. - -Let's have an `Article` class representing a blog article: - -```php -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - // save the article to the database - } -} -``` - -and the usage will be as follows: - -```php -$article = new Article; -$article->title = '10 Things You Need to Know About Losing Weight'; -$article->content = 'Every year millions of people in ...'; -$article->save(); -``` - -The `save()` method will save the article to a database table. Implementing it using [Nette Database |database:] would be straightforward, if not for one catch: where does `Article` get the database connection, i.e., an object of the `Nette\Database\Connection` class? - -It seems we have many options. It could take it from a static variable. Or by inheriting from a class that provides the database connection. Or use a [singleton |global-state#Singleton]. Or so-called facades, as used in Laravel: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - DB::insert( - 'INSERT INTO articles (title, content) VALUES (?, ?)', - [$this->title, $this->content], - ); - } -} -``` - -Great, we solved the problem. - -Or did we? - -Let's recall [#Rule #1: Let It Be Passed to You]: all dependencies that the class needs must be passed to it. Because if we break the rule, we've embarked on the path to messy code full of hidden dependencies, lack of clarity, and the result will be an application that is a challenge to maintain and develop. - -The user of the `Article` class has no idea where the `save()` method stores the article. In a database table? Which one, the production or test database? And how can it be changed? - -The user has to look at how the `save()` method is implemented and finds the use of the `DB::insert()` method. So, they must investigate further how this method obtains the database connection. And hidden dependencies can form a rather long chain. - -In clean and well-designed code, there are never hidden dependencies, Laravel facades, or static variables. In clean and well-designed code, arguments are provided: - -```php -class Article -{ - public function save(Nette\Database\Connection $db): void - { - $db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -Even more practical, as we will see later, is using the constructor: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function save(): void - { - $this->db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -.[note] -If you are an experienced programmer, you might think that `Article` shouldn't have a `save()` method at all; it should represent purely a data structure, and a separate repository should handle the saving. That makes sense. But that would take us well beyond the scope of the topic, which is Dependency Injection, and the goal of providing simple examples. - -If you write a class that requires, for example, a database for its operation, don't invent where to get it from, but have it passed to you. Perhaps as a parameter of the constructor or another method. Acknowledge dependencies. Acknowledge them in your class's API. You will get understandable and predictable code. - -And what about this class, which logs error messages: - -```php -class Logger -{ - public function log(string $message): void - { - $file = LOG_DIR . '/log.txt'; - file_put_contents($file, $message . "\n", FILE_APPEND); - } -} -``` - -What do you think, did we follow [#Rule #1: Let It Be Passed to You]? - -We did not. - -The key piece of information, the directory containing the log file, is *obtained by the class itself* from a constant. - -Look at the usage example: - -```php -$logger = new Logger; -$logger->log('Temperature is 23 °C'); -$logger->log('Temperature is 10 °C'); -``` - -Without knowing the implementation, could you answer where the messages are being written? Would it occur to you that the existence of the `LOG_DIR` constant is required for its operation? And could you create a second instance that would write elsewhere? Certainly not. - -Let's fix the class: - -```php -class Logger -{ - public function __construct( - private string $file, - ) { - } - - public function log(string $message): void - { - file_put_contents($this->file, $message . "\n", FILE_APPEND); - } -} -``` - -The class is now much more understandable, configurable, and thus more useful. - -```php -$logger = new Logger('/path/to/log.txt'); -$logger->log('Temperature is 15 °C'); -``` - - -But I Don't Care! ------------------ - -*"When I create an Article object and call save(), I don't want to deal with the database; I just want it to be saved in the one I have configured."* - -*"When I use Logger, I just want the message to be written, and I don't want to deal with where. Let the global settings be used."* - -These are valid points. - -As an example, let's show a class that distributes newsletters and logs the outcome: - -```php -class NewsletterDistributor -{ - public function distribute(): void - { - $logger = new Logger(/* ... */); - try { - $this->sendEmails(); - $logger->log('Emails have been sent out'); - - } catch (Exception $e) { - $logger->log('An error occurred during sending'); - throw $e; - } - } -} -``` - -The improved `Logger`, which no longer uses the `LOG_DIR` constant, requires the file path in the constructor. How can this be resolved? The `NewsletterDistributor` class is not concerned with where messages are written; it simply wants to log them. - -The solution is again [#Rule #1: Let It Be Passed to You]: we pass all the data the class needs. - -So, does this mean we pass the log path through the constructor, which we then use when creating the `Logger` object? - -```php -class NewsletterDistributor -{ - public function __construct( - private string $file, // ⛔ NOT LIKE THIS! - ) { - } - - public function distribute(): void - { - $logger = new Logger($this->file); -``` - -Not like this! Because the path is **not** data that the `NewsletterDistributor` class requires; the `Logger` requires it. Do you perceive the difference? The `NewsletterDistributor` class needs the logger itself. So, we will pass the logger itself: - -```php -class NewsletterDistributor -{ - public function __construct( - private Logger $logger, // ✅ - ) { - } - - public function distribute(): void - { - try { - $this->sendEmails(); - $this->logger->log('Emails have been sent out'); - - } catch (Exception $e) { - $this->logger->log('An error occurred during sending'); - throw $e; - } - } -} -``` - -Now it's clear from the signature of the `NewsletterDistributor` class that logging is part of its function. And the task of substituting the logger with another, perhaps for testing, is entirely straightforward. Moreover, if the `Logger` class constructor were to change, it would have no impact on our class. - - -Rule #2: Take What's Yours --------------------------- - -Don't be confused and don't accept the dependencies of your dependencies. Accept only your own dependencies. - -Thanks to this, code that uses other objects will be completely independent of changes to their constructors. Its API will be more accurate. And most importantly, it will be straightforward to substitute these dependencies with others. - - -New Family Member ------------------ - -The development team has decided to create a second logger, one that writes to the database. So, we create a `DatabaseLogger` class. Now we have two classes, `Logger` and `DatabaseLogger`; one writes to a file, the other to the database... doesn't the naming seem somewhat odd? Wouldn't it be better to rename `Logger` to `FileLogger`? Certainly. - -But let's do it cleverly. We create an interface using the original name: - -```php -interface Logger -{ - function log(string $message): void; -} -``` - -… which both loggers will implement: - -```php -class FileLogger implements Logger -// ... - -class DatabaseLogger implements Logger -// ... -``` - -And thanks to this, there will be no need to modify anything in the rest of the code where the logger is utilized. For example, the `NewsletterDistributor` class constructor will still be content with requiring `Logger` as a parameter. And it will be up to us which instance we provide to it. - -**That's why we never add the `Interface` suffix or the `I` prefix to interface names.** Otherwise, it wouldn't be possible to extend the code so elegantly. - - -Houston, We Have a Problem --------------------------- - -While in the entire application we can get by with a single instance of the logger, whether file-based or database-based, and simply pass it wherever logging occurs, the situation is quite different with the `Article` class. We create its instances as required, even multiple times. How do we handle the database dependency in its constructor? - -An example could be a controller that should save an article to the database after submitting a form: - -```php -class EditController extends Controller -{ - public function formSubmitted($data) - { - $article = new Article(/* ... */); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -A potential solution seems obvious: let's have the database object passed via the constructor into `EditController` and use `$article = new Article($this->db)`. - -Just as in the previous case involving `Logger` and the file path, this is not the correct approach. The database is not a dependency of `EditController`, but rather of `Article`. Passing the database thus violates [Rule #2: Take What's Yours |#Rule #2: Take What's Yours]. If the `Article` class constructor changes (a new parameter is added), you will need to modify the code in all places where instances are created. Oof. - -Houston, what's your suggestion? - - -Rule #3: Let the Factory Handle It ----------------------------------- - -By eliminating hidden dependencies and passing all dependencies as arguments, we have gained more configurable and flexible classes. Therefore, we need something additional to create and configure these more flexible classes for us. We'll call these factories. - -The rule is: If a class has dependencies, delegate the creation of its instances to a factory. - -Factories are a smarter alternative to the `new` operator in the world of Dependency Injection. - -.[note] -Please do not confuse this with the *factory method* design pattern, which describes a specific way of utilizing factories and is unrelated to this topic. - - -Factory -------- - -A factory is a method or class that creates and configures objects. We will call the class that produces `Article` `ArticleFactory`, and it might look like this: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Its usage in the controller will be as follows: - -```php -class EditController extends Controller -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function formSubmitted($data) - { - // let the factory create the object - $article = $this->articleFactory->create(); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -At this point, if the signature of the `Article` class constructor changes, the only part of the code that needs to respond is the `ArticleFactory` itself. All other code that interacts with `Article` objects, such as `EditController`, will remain unaffected. - -You might be scratching your head now, wondering if we've actually improved the situation. The amount of code has grown, and the whole thing is starting to look suspiciously complex. - -Don't worry, we'll get to the Nette DI container soon. And it has several tricks up its sleeve that will greatly simplify the construction of applications using Dependency Injection. For example, instead of the `ArticleFactory` class, it will suffice to [write just an interface |factory]: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -But we are getting ahead of ourselves, stay tuned :-) - - -Summary -------- - -At the beginning of this chapter, we promised to show a process for designing clean code. Simply ensure that classes: - -1) [are passed the dependencies they need |#Rule #1: Let It Be Passed to You] -2) [and conversely, are not passed what they don't directly need |#Rule #2: Take What's Yours] -3) [and that objects with dependencies are best created in factories |#Rule #3: Let the Factory Handle It] - -It might not seem apparent at first glance, but these three rules have far-reaching consequences. They lead to a radically different perspective on code design. Is it worthwhile? Programmers who have abandoned old habits and started consistently using Dependency Injection consider this step a defining moment in their professional careers. It opened up a world of clear and maintainable applications for them. - -But what if the code doesn't consistently use Dependency Injection? What if it's built upon static methods or singletons? Does this lead to problems? [Yes, it does, and very significant ones |global-state]. - -{{maintitle: What Is Dependency Injection? PHP Guide with Examples}} -{{description: Dependency injection explained from scratch with PHP examples: three simple rules that lead you to clean, understandable and maintainable code.}} diff --git a/dependency-injection/en/nette-container.texy b/dependency-injection/en/nette-container.texy deleted file mode 100644 index c5c11f59bf..0000000000 --- a/dependency-injection/en/nette-container.texy +++ /dev/null @@ -1,145 +0,0 @@ -Nette DI Container -****************** - -.[perex] -Nette DI is one of Nette's most interesting libraries. It can generate and automatically update compiled DI containers that are extremely fast and remarkably easy to configure. - -The form of services that the DI container should create is usually defined using configuration files in [NEON format|neon:format]. The container we manually created in the [previous chapter|container] would be written like this: - -```neon -parameters: - db: - dsn: 'mysql:' - user: root - password: '***' - -services: - - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - - ArticleFactory - - EditController -``` - -The syntax is very concise. - -All dependencies declared in the constructors of the `ArticleFactory` and `EditController` classes are discovered and passed automatically by Nette DI thanks to so-called [autowiring|autowiring], so there's no need to specify anything in the configuration file. Thus, even if parameters change, you don't need to change anything in the configuration. During development, Nette automatically regenerates the container. You can focus purely on application development. - -If we want to pass dependencies using setters, we use the [setup |services#Setup] section for this. - -Nette DI generates PHP code for the container directly. The result is thus a `.php` file that you can open and examine. This allows you to see exactly how the container functions. You can also debug it in your IDE and step through its execution. And most importantly: the generated PHP code is extremely fast. - -Nette DI can also generate [factory|factory] code based on a provided interface. Therefore, instead of the `ArticleFactory` class, we only need to create an interface in the application: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -You can find the full example in the course [Dependency Injection by Example |https://github.com/nette-examples/di-by-example], where you first write the container by hand and only then let Nette DI generate it. - - -Standalone Use --------------- - -Integrating the Nette DI library into an application is very easy. First, we install it using Composer (because downloading zip files is so outdated): - -```shell -composer require nette/di -``` - -The following code uses the [Compiler |api:Nette\DI\Compiler] to create an instance of the DI container according to the configuration stored in the `config.neon` file: - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp'); -$class = $loader->load(function ($compiler) { - $compiler->loadConfig(__DIR__ . '/config.neon'); -}); -$container = new $class; -``` - -The container is generated only once, its code is written to the cache (the `__DIR__ . '/temp'` directory), and on subsequent requests, it is only loaded from there. - -On its own, the `Compiler` enables only the `services` and `parameters` sections in the configuration. To use others - such as `search`, `decorator`, `di`, or `inject` - register their extensions first. And to allow registering extensions from the `extensions` section of the config, add the `ExtensionsExtension`: - -```php -$compiler->addExtension('search', new Nette\DI\Extensions\SearchExtension($tempDir)); -$compiler->addExtension('extensions', new Nette\DI\Extensions\ExtensionsExtension); -``` - -The [Configurator |application:bootstrapping] used in full Nette applications registers all of these automatically. - -If you keep several different containers in the same cache directory, distinguish them with a key passed as the second argument to `load()`; it becomes part of the generated class name: - -```php -$class = $loader->load( - fn($compiler) => $compiler->loadConfig(__DIR__ . '/config.neon'), - 'my-key', -); -``` - -The `getService()` or `getByType()` methods are used to create and retrieve services. This is how we create the `EditController` object: - -```php -$controller = $container->getByType(EditController::class); -$controller->someMethod(); -``` - -During development, it's useful to activate auto-refresh mode, where the container automatically regenerates if any class or configuration file is modified. Just provide `true` as the second argument in the [ContainerLoader |api:Nette\DI\ContainerLoader] constructor. - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp', true); -``` - -Auto-refresh works even within a single process, which matters for long-running workers, development servers and CLI tools. Because PHP cannot redeclare a class that is already loaded, every rebuild declares the container under a new class name and `load()` returns the name matching the current configuration. So call `load()` again before each unit of work and create a new container whenever the returned name changes; a rebuild done by another process over the same cache is picked up as well. Never store the class name, it is not stable across rebuilds. .{data-version:3.2.7} - - -Working with the Container --------------------------- - -Besides `getService()` and `getByType()`, the container object offers several other useful methods: - -- `getByType(string $type, bool $throw = true): ?object` returns the service of the given type. If you pass `false` as the second argument, it returns `null` instead of throwing an exception when no such service exists. -- `hasService(string $name): bool` and `isCreated(string $name): bool` tell you whether a service is defined and whether it has already been instantiated. -- `getServiceDescriptors(): array<string, ServiceDescriptor>` describes all registered services, keyed by name: type, autowiring, tags, aliases, and the instance if one already exists. Nothing is instantiated. .{data-version:3.2.7} -- `addService(string $name, object $service): static` puts a service into the container; a closure with a declared return type registers a factory instead of a ready instance. `removeService(string $name): void` removes it. -- `getParameters(): array` returns all container parameters, `getParameter($key)` returns a single one. -- `createInstance(string $class, array $args = []): object` creates a new instance of the given class and passes its constructor dependencies via autowiring. -- `callMethod(callable $function, array $args = []): mixed` calls the given callable and passes its arguments via autowiring. -- `callInjects(object $service): void` calls all `inject*()` methods on the given object and passes dependencies to them. - -The container constructor also accepts an array of parameters that supplement those defined in the configuration: - -```php -$container = new $class(['host' => 'localhost']); -``` - - -PSR-11 Container .{data-version:3.2.7} --------------------------------------- - -The class `Nette\Bridges\DIPsr\PsrContainer` wraps the container and exposes it through the PSR-11 `ContainerInterface`, so you can hand it to any library or framework that expects one: - -```php -$psr = new Nette\Bridges\DIPsr\PsrContainer($container); - -$psr->get(PDO::class); // a type, resolved by autowiring -$psr->get('database'); // a service name -``` - -The identifier is treated first as a type resolved by autowiring, and only then as a service name. If several autowired services match the type, `get()` throws `NotFoundException` instead of picking one of them, while `has()` returns `false`. The bridge's exceptions implement the PSR-11 interfaces, so `catch (Psr\Container\NotFoundExceptionInterface)` works as expected. - -The `psr/container` package is not a dependency of `nette/di`, so add it to your application yourself: - -```shell -composer require psr/container -``` - - -Using with Nette Framework --------------------------- - -As we have shown, the use of Nette DI is not restricted to applications built with Nette Framework; you can integrate it anywhere with just three lines of code. However, if you are developing applications using the Nette Framework, the configuration and creation of the container are handled by [Bootstrap |application:bootstrapping#DI Container Configuration]. - -{{maintitle: Nette DI Container: Fast Compiled Container for PHP}} -{{description: Nette DI generates and automatically updates compiled containers that are extremely fast and easy to configure in NEON. See how to use it in practice.}} diff --git a/dependency-injection/en/passing-dependencies.texy b/dependency-injection/en/passing-dependencies.texy deleted file mode 100644 index 0f151961ab..0000000000 --- a/dependency-injection/en/passing-dependencies.texy +++ /dev/null @@ -1,218 +0,0 @@ -Passing Dependencies -******************** - -<div class=perex> - -Arguments, or 'dependencies' in DI terminology, can be passed to classes in the following main ways: - -* Constructor injection -* Method injection (so-called setter injection) -* Property injection -* Using the `inject*()` method or the `#[Inject]` attribute - -</div> - -Let's demonstrate each variant with specific examples. - - -Constructor Injection -===================== - -Dependencies are provided as constructor arguments at the time the object is instantiated: - -```php -class MyClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -$obj = new MyClass($cache); -``` - -This approach is suitable for mandatory dependencies that the class absolutely requires for its operation, because without them, the instance cannot be created. - -Since PHP 8.0, we can use a shorter notation ([constructor property promotion |https://blog.nette.org/en/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), which is functionally equivalent: - -```php -// PHP 8.0 -class MyClass -{ - public function __construct( - private Cache $cache, - ) { - } -} -``` - -Since PHP 8.1, a property can be marked with the `readonly` flag, which declares that the property's value will not change after initialization: - -```php -// PHP 8.1 -class MyClass -{ - public function __construct( - private readonly Cache $cache, - ) { - } -} -``` - -The DI container passes dependencies to the constructor automatically using [autowiring |autowiring]. Arguments that cannot be provided this way (e.g., strings, numbers, booleans) [are specified in the configuration |services#Arguments]. - - -Constructor Hell ----------------- - -The term *constructor hell* describes a situation where a child class inherits from a parent class whose constructor requires dependencies, and the child class also requires dependencies. It must then accept and pass on the parent's dependencies as well: - -```php -abstract class BaseClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass extends BaseClass -{ - private Database $db; - - // ⛔ CONSTRUCTOR HELL - public function __construct(Cache $cache, Database $db) - { - parent::__construct($cache); - $this->db = $db; - } -} -``` - -The problem arises when we want to change the constructor of the `BaseClass`, for example, when a new dependency is added. Then, it becomes necessary to modify all the constructors of the child classes as well, which turns such a modification into hell. - -How can this be prevented? The solution is to **prefer [composition over inheritance |faq#Why is composition preferred over inheritance?]**. - -So, we design the code differently. We will avoid [abstract |nette:introduction-to-object-oriented-programming#Abstract Classes] `Base*` classes. Instead of `MyClass` acquiring certain functionality by inheriting from `BaseClass`, it will have this functionality passed as a dependency: - -```php -final class SomeFunctionality -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass -{ - private SomeFunctionality $sf; - private Database $db; - - public function __construct(SomeFunctionality $sf, Database $db) // ✅ - { - $this->sf = $sf; - $this->db = $db; - } -} -``` - - -Setter Injection -================ - -Dependencies are provided by calling a method that stores them in a private property. The common naming convention for these methods is the `set*()` pattern, hence they are called setters, but they can, of course, be named differently. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - $this->cache = $cache; - } -} - -$obj = new MyClass; -$obj->setCache($cache); -``` - -This approach is suitable for optional dependencies that are not essential for the class's operation, as it's not guaranteed that the object will actually receive the dependency (i.e., that the caller will invoke the method). - -At the same time, this method allows the setter to be called repeatedly to change the dependency. If this is undesirable, add a check within the method, or since PHP 8.1, mark the `$cache` property with the `readonly` flag. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - if (isset($this->cache)) { - throw new RuntimeException('The dependency has already been set'); - } - $this->cache = $cache; - } -} -``` - -The setter call is defined in the DI container configuration in the [setup key |services#Setup]. Here too, automatic dependency provision via autowiring is used: - -```neon -services: - - create: MyClass - setup: - - setCache -``` - - -Property Injection -================== - -Dependencies are provided by writing directly to a member property: - -```php -class MyClass -{ - public Cache $cache; -} - -$obj = new MyClass; -$obj->cache = $cache; -``` - -This method is considered inappropriate because the member property must be declared as `public`. Consequently, we lose control over ensuring the passed dependency is actually of the required type (this was particularly true before PHP 7.4 introduced type hinting for properties), and we lose the ability to react to a newly assigned dependency with custom logic, for example, to prevent subsequent modification. At the same time, the property becomes part of the class's public API, which might not be intended. - -Property assignment is defined in the DI container configuration in the [setup section |services#Setup]: - -```neon -services: - - create: MyClass - setup: - - $cache = @\Cache -``` - - -Inject -====== - -While the previous three approaches apply generally in all object-oriented languages, injection via the `inject*()` methods or the `#[Inject]` attribute is typically used with Nette presenters, where it is enabled by default; any other service can opt in via [`inject: true` |services#Inject Mode]. They are discussed in a [separate chapter |best-practices:inject-method-attribute]. - - -Which Method to Choose? -======================= - -- The constructor is suitable for mandatory dependencies that the class absolutely requires for its operation. -- The setter, conversely, is suitable for optional dependencies, or dependencies that might need to be changed later. -- Public properties are generally not recommended. - -{{maintitle: PHP Constructor, Setter and Property Injection Compared}} -{{description: How to pass dependencies to a class: constructor injection, setter injection, property injection and the inject method. How they differ and which one to choose.}} diff --git a/dependency-injection/en/services.texy b/dependency-injection/en/services.texy deleted file mode 100644 index 1a2b4235b2..0000000000 --- a/dependency-injection/en/services.texy +++ /dev/null @@ -1,483 +0,0 @@ -Service Definitions -******************* - -.[perex] -Configuration is where we instruct the DI container how to create individual services and how to connect them with their dependencies. Nette offers a very clear and elegant way to achieve this. - -The `services` section in the NEON configuration file is where we define our own services and their configurations. Let's look at a simple example defining a service named `database`, which represents an instance of the `PDO` class: - -```neon -services: - database: PDO('sqlite::memory:') -``` - -The configuration above results in the following factory method in the [DI container|container]: - -```php -public function createServiceDatabase(): PDO -{ - return new PDO('sqlite::memory:'); -} -``` - -Service names enable referencing them in other parts of the configuration file, using the `@serviceName` format. If there's no need to assign a name to the service, we can simply use a bullet point (`-`): - -```neon -services: - - PDO('sqlite::memory:') -``` - -To retrieve a service from the DI container, we can use the `getService()` method with the service name as a parameter, or the `getByType()` method with the service type: - -```php -$database = $container->getService('database'); -$database = $container->getByType(PDO::class); -``` - - -Service Creation -================ - -Usually, we create a service simply by instantiating a specific class. For example: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -If we need to expand the configuration with additional keys, the definition can be split across multiple lines: - -```neon -services: - database: - create: PDO('sqlite::memory:') - setup: ... -``` - -The `create` key has an alias `factory`; both variants are commonly used. However, we recommend using `create`. - -Arguments for the constructor or the factory method can alternatively be specified using the `arguments` key: - -```neon -services: - database: - create: PDO - arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] -``` - -Services don't necessarily have to be created by simple class instantiation; they can also be the result of invoking static methods or methods of other services: - -```neon -services: - database: DatabaseFactory::create() - router: @routerFactory::create() -``` - -Note that for simplicity, `::` is used instead of `->`, see [#Expression Language]. These factory methods will be generated: - -```php -public function createServiceDatabase(): PDO -{ - return DatabaseFactory::create(); -} - -public function createServiceRouter(): RouteList -{ - return $this->getService('routerFactory')->create(); -} -``` - -The DI container needs to know the type of the service being created. If we create a service using a method that lacks a specified return type, we must explicitly declare this type in the configuration: - -```neon -services: - database: - create: DatabaseFactory::create() - type: PDO -``` - - -Arguments -========= - -We pass arguments to constructors and methods in a way very similar to how it's done in PHP itself: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -For better readability, we can list arguments on separate lines. In this case, using commas becomes optional: - -```neon -services: - database: PDO( - 'mysql:host=127.0.0.1;dbname=test' - root - secret - ) -``` - -You can also name the arguments, eliminating the need to worry about their order: - -```neon -services: - database: PDO( - username: root - password: secret - dsn: 'mysql:host=127.0.0.1;dbname=test' - ) -``` - -If you want to omit certain arguments and use their default values or have a service injected via [autowiring|autowiring], use an underscore (`_`): - -```neon -services: - foo: Foo(_, %appDir%) -``` - -Arguments can include services, parameters, and much more, see [#Expression Language]. - - -Setup -===== - -In the `setup` section, we define methods that should be invoked upon service creation. - -```neon -services: - database: - create: PDO(%dsn%, %user%, %password%) - setup: - - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) -``` - -This would look like this in PHP: - -```php -public function createServiceDatabase(): PDO -{ - $service = new PDO('...', '...', '...'); - $service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - return $service; -} -``` - -In addition to method calls, values can also be assigned to properties. Adding elements to arrays is also supported, which requires enclosing the array access in quotes to avoid conflicts with NEON syntax: - -```neon -services: - foo: - create: Foo - setup: - - $value = 123 - - '$onClick[]' = [@bar, clickHandler] -``` - -Which would look like the following in PHP code: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - $service->value = 123; - $service->onClick[] = [$this->getService('bar'), 'clickHandler']; - return $service; -} -``` - -However, in the setup, you can also invoke static methods or methods of other services. If you need to pass the current service itself as an argument, refer to it using `@self`: - -```neon -services: - foo: - create: Foo - setup: - - My\Helpers::initializeFoo(@self) - - @anotherService::setFoo(@self) -``` - -Note that for simplicity, `::` is used instead of `->`, see [#Expression Language]. Such a factory method will be generated: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - My\Helpers::initializeFoo($service); - $this->getService('anotherService')->setFoo($service); - return $service; -} -``` - - -Expression Language -=================== - -Nette DI provides an exceptionally rich expression language, through which we can define almost anything. In configuration files, we can thus use [parameters |configuration#Parameters]: - -```neon -# parameter -%wwwDir% - -# value of a parameter under a key -%mailer.user% - -# parameter inside a string -'%wwwDir%/images' -``` - -Furthermore, create objects, call methods and functions: - -```neon -# create object -DateTime() - -# call static method -Collator::create(%locale%) - -# call PHP function -::getenv(DB_USER) -``` - -Refer to services either by their name or by type: - -```neon -# service by name -@database - -# service by type -@Nette\Database\Connection -``` - -Use first-class callable syntax: .{data-version:3.2.0} - -```neon -# create callback, equivalent to [@user, logout] -@user::logout(...) -``` - -Use constants: - -```neon -# class constant -FilesystemIterator::SKIP_DOTS - -# get global constant using PHP function constant() -::constant(\PHP_VERSION) -``` - -Access a service's public properties and constants via `@service::member`. Whether the name resolves to a property or to a constant is decided by its first letter - a lowercase initial means a public property, an uppercase one means a constant: - -```neon -# public property of a service (starts with a lowercase letter) -@settings::apiUrl - -# class constant of a service (starts with an uppercase letter) -@settings::Version -``` - -Method calls can be chained just like in PHP. For simplicity, `::` is used instead of `->`: - -```neon -DateTime()::format('Y-m-d') -# PHP: (new DateTime())->format('Y-m-d') - -@http.request::getUrl()::getHost() -# PHP: $this->getService('http.request')->getUrl()->getHost() -``` - -You can use these expressions anywhere, when [creating services |#Service Creation], in [#arguments], in the [#setup] section, or in [parameters |configuration#Parameters]: - -```neon -parameters: - ipAddress: @http.request::getRemoteAddress() - -services: - database: - create: DatabaseFactory::create( @anotherService::getDsn() ) - setup: - - initialize( ::getenv('DB_USER') ) -``` - - -Special Functions ------------------ - -In configuration files, you can use the following special functions: - -- `not()` negates a value -- `bool()`, `int()`, `float()`, `string()` lossless casting to the specified type .{data-version:3.0.5} -- `typed()` creates an array of all services of the specified type -- `tagged()` creates an array of all services with the given tag - -```neon -services: - - Foo( - id: int(::getenv('ProjectId')) - productionMode: not(%debugMode%) - ) -``` - -Unlike standard PHP casting, such as `(int)`, lossless casting throws an exception for non-numeric values. - -The `typed()` function creates an array of all services of the specified type (class or interface). It excludes services that have autowiring disabled. Multiple types can also be specified, separated by commas. - -```neon -services: - - BarsDependent( typed(Bar) ) -``` - -An array of services of a certain type can also be passed as an argument automatically using [autowiring |autowiring#Collection of Services]. - -The `tagged()` function then creates an array of all services with a specific tag. Here too, you can specify multiple tags separated by commas. - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - - -Autowiring -========== - -The `autowired` key allows you to influence the autowiring behavior for a specific service. For details, see the [chapter on autowiring|autowiring]. - -```neon -services: - foo: - create: Foo - autowired: false # the foo service is excluded from autowiring -``` - - -Lazy Services .{data-version:3.2.4} -=================================== - -Lazy loading is a technique that defers the creation of a service until it is actually needed. In the global configuration, you can [enable lazy creation |configuration#Lazy Services] for all services at once. For individual services, you can then override this behavior: - -```neon -services: - foo: - create: Foo - lazy: false -``` - -When a service is defined as lazy, upon requesting it from the DI container, we receive a special proxy object. This proxy looks and behaves identically to the actual service, but the actual initialization (constructor invocation and setup calls) occurs only upon the first access to any of its methods or properties. - -Keep in mind that because the service is created later, errors in its configuration also manifest themselves later. For example, incorrect database credentials will not reveal themselves when the application starts, but only upon the first query. - -Lazy creation also mitigates circular dependencies, i.e., a situation where service A requires service B and B at the same time requires A. Without it, the container reports the error `Circular reference detected`. With a lazy proxy, service A receives only a proxy of service B, which initializes itself when it is actually used, at a moment when A already exists. Nevertheless, a circular dependency signals a flawed design, and it is better to get rid of it. - -.[note] -Lazy loading requires PHP 8.4 or newer and works only for services created by directly instantiating a class (e.g. `create: Foo`), not for those created by a factory method. It also cannot be used for classes that ultimately extend an internal PHP class. When lazy loading cannot be applied, the `lazy: true` flag is silently ignored. - - -Tags -==== - -Tags serve to add supplementary information to services. You can assign one or more tags to a service: - -```neon -services: - foo: - create: Foo - tags: - - cached -``` - -Tags can also hold values: - -```neon -services: - foo: - create: Foo - tags: - logger: monolog.logger.event -``` - -To retrieve all services associated with specific tags, you can use the `tagged()` function: - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - -Within the DI container, you can retrieve the names of all services with a specific tag using the `findByTag()` method: - -```php -$names = $container->findByTag('logger'); -// $names is an array containing service names as keys and tag values as values -// e.g., ['foo' => 'monolog.logger.event', ...] -``` - - -Inject Mode -=========== - -Using the `inject: true` flag enables dependency injection via public properties with the [Inject |best-practices:inject-method-attribute#Inject Attributes] attribute and [inject*() |best-practices:inject-method-attribute#inject Methods] methods. - -```neon -services: - articles: - create: App\Model\Articles - inject: true -``` - -By default, `inject` mode is enabled only for presenters. - - -Service Modifications -===================== - -The DI container holds numerous services added via built-in or [user extensions|extensions]. You can modify the definitions of these existing services directly in the configuration. For example, you can change the class for the `application.application` service, which defaults to `Nette\Application\Application`, to a different one: - -```neon -services: - application.application: - create: MyApplication - alteration: true -``` - -The `alteration` flag indicates that we are merely modifying an existing service. It also acts as a safeguard: if the service being modified doesn't exist, compilation fails with an exception. - -We can also supplement the setup: - -```neon -services: - application.application: - create: MyApplication - alteration: true - setup: - - '$onStartup[]' = [@resource, init] -``` - -You don't have to identify a service by its internal name - you can refer to it by type instead. The previous example can also be written as: - -```neon -services: - @Nette\Application\Application: - create: MyApplication -``` - -When modifying a service, we might want to remove original arguments, setup items, or tags, using the `reset` key: - -```neon -services: - application.application: - create: MyApplication - alteration: true - reset: - arguments: true - setup: true - tags: true -``` - -If you want to remove a service added by an extension, you can do so as follows: - -```neon -services: - cache.journal: false -``` - -{{maintitle: Nette DI Service Definitions: Configure Services in NEON}} -{{description: Tell the DI container how to create your services and connect their dependencies: arguments, setup, expression language, lazy services, tags and inject mode.}} diff --git a/dependency-injection/en/upgrading.texy b/dependency-injection/en/upgrading.texy deleted file mode 100644 index 8e6b452c21..0000000000 --- a/dependency-injection/en/upgrading.texy +++ /dev/null @@ -1,52 +0,0 @@ -Upgrading -********* - - -Upgrading to Version 3.1 -======================== - -- autowiring no longer passes `null` to a nullable parameter without a default value; pass the argument explicitly, or give the parameter a default value -- support for the `@return` annotation was dropped; use a return type, or state the type in the service definition using `type:` -- the key `dynamic` was renamed to `imported` and `class` to `type` -- the symbol for an omitted argument changed from `...` to `_`, e.g. `MyService(_, 123)` -- in NEON files, the `@` character at the beginning of a string no longer needs to be escaped -- the `parameters` key inside definitions of generated factories is deprecated -- the method `Nette\DI\Config\Loader::save()` is deprecated; export the configuration using `Nette\DI\Config\Adapters\NeonAdapter::dump()` - -Version 3.1 is a transition release: it does not bring new features, but it warns with notices about everything that will work differently later. See the article [Nette DI 3.1: transition release |https://blog.nette.org/en/nette-di-3-1-transition-release]. - - -Upgrading to Version 3.0 -======================== - -- support for INI files has been removed -- direct writing of PHP code into the configuration using question marks (e.g. `"$service->onError[] = ?"(...)`) was removed; use the array syntax `'$onError[]' = [...]` instead -- in configuration files, use `factory: PDO(...)` instead of `class: PDO(...)` -- the `nette.presenter` tag is no longer used for presenters - - -For Compiler Extension Authors ------------------------------- - -While Nette 2.4 internally described every service as `Nette\DI\ServiceDefinition`, there are now several definition types: `Nette\DI\Definitions\ImportedDefinition` for imported (dynamic) services, `Nette\DI\Definitions\FactoryDefinition` for generated interface-based factories, `Nette\DI\Definitions\AccessorDefinition` for generated accessors, and `Nette\DI\Definitions\ServiceDefinition` for common services. - -Therefore, in addition to `ContainerBuilder::addDefinition()`, there are several other methods for creating a new definition: `addFactoryDefinition()`, `addAccessorDefinition()`, and `addImportedDefinition()`. - - -Upgrading to Version 2.4 -======================== - -- configuration sections (e.g. production, development) in a single config file are deprecated; use a pair of files `config.neon` and `config.local.neon` -- inheritance of service definitions is deprecated -- `Statement::setEntity()` is deprecated - - -Upgrading to Version 2.3 -======================== - -- support for placing services inside the extension section of the configuration file was removed -- support for dynamically added extensions was removed -- when replacing a service dynamically (via `removeService()`, `addService()`), the new service must be an instance of the same interface/class as the original - -{{maintitle: Upgrading Nette DI: What Changed in Each Version}} -{{description: What changed in Nette DI 3.1, 3.0, 2.4 and 2.3, and how to update your code and NEON configuration: autowiring of nullable parameters, renamed keys and more.}} diff --git a/dependency-injection/es/@home.texy b/dependency-injection/es/@home.texy deleted file mode 100644 index 4a950bb72e..0000000000 --- a/dependency-injection/es/@home.texy +++ /dev/null @@ -1,22 +0,0 @@ -Nette DI -******** - -.[perex] -La inyección de dependencias es un patrón de diseño que cambiará de raíz su forma de ver el código y el desarrollo. Le abre la puerta a un mundo de aplicaciones limpiamente diseñadas y sostenibles. - -- [¿Qué es la inyección de dependencias? |introduction] -- [Estado global y singletons |global-state] -- [Paso de dependencias |passing-dependencies] -- [¿Qué es un contenedor DI? |container] -- [Preguntas frecuentes |faq] - - -El paquete `nette/di` proporciona un contenedor DI compilado extremadamente avanzado para PHP. - -- [Contenedor DI de Nette |nette-container] -- [Configuración |configuration] -- [Definición de servicios |services] -- [Autowiring |autowiring] -- [Factories generadas |factory] -- [Creación de extensiones para Nette DI |extensions] -- [La compilación del contenedor en detalle |compilation-internals] diff --git a/dependency-injection/es/@left-menu.texy b/dependency-injection/es/@left-menu.texy deleted file mode 100644 index 7b1c9af8f0..0000000000 --- a/dependency-injection/es/@left-menu.texy +++ /dev/null @@ -1,28 +0,0 @@ -Dependency Injection -******************** -- [¿Qué es la DI? |introduction] -- [Estado global y singletons |global-state] -- [Paso de dependencias |passing-dependencies] -- [¿Qué es un contenedor DI? |container] -- [Preguntas frecuentes |faq] - - -Nette DI --------- -- [Contenedor DI de Nette |nette-container] -- [Configuración|configuration] -- [Definición de servicios |services] -- [Autowiring|autowiring] -- [Factories generadas |factory] -- [Creación de extensiones para Nette DI |extensions] -- [La compilación en detalle |compilation-internals] -- [Actualización|upgrading] - - -Lecturas adicionales -******************** -- [Documentación de Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Buenas prácticas |best-practices:] -- [Solución de problemas |nette:troubleshooting] diff --git a/dependency-injection/es/@meta.texy b/dependency-injection/es/@meta.texy deleted file mode 100644 index 3798d9cda4..0000000000 --- a/dependency-injection/es/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentación de Nette}} diff --git a/dependency-injection/es/autowiring.texy b/dependency-injection/es/autowiring.texy deleted file mode 100644 index 83be9bbc3c..0000000000 --- a/dependency-injection/es/autowiring.texy +++ /dev/null @@ -1,279 +0,0 @@ -Autowiring -********** - -.[perex] -El autowiring es una gran característica que pasa automáticamente al constructor y a otros métodos los servicios necesarios, de modo que no tenemos que indicarlos explícitamente. Le ahorrará mucho tiempo. - -Gracias a él podemos omitir la gran mayoría de los argumentos al escribir las definiciones de los servicios. En lugar de: - -```neon -services: - articles: Model\ArticleRepository(@database, @cache.storage) -``` - -Basta con escribir: - -```neon -services: - articles: Model\ArticleRepository -``` - -El autowiring se guía por los tipos, así que para que funcione la clase `ArticleRepository` debe estar definida más o menos así: - -```php -namespace Model; - -class ArticleRepository -{ - public function __construct(\PDO $db, \Nette\Caching\Storage $storage) - {} -} -``` - -El autowiring nunca usa los nombres de los servicios. Se guía únicamente por el sistema de tipos de PHP, así que sabe también que una clase satisface las interfaces que implementa y las clases de las que hereda. Gracias a eso, el nombre de un servicio es solo un identificador auxiliar y renombrarlo no romperá nada en la aplicación. - -Para poder usar el autowiring debe haber en el contenedor **exactamente un servicio** de cada tipo. Si hubiera más, el autowiring no sabría cuál pasar y lanzaría una excepción: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - tempDb: PDO('sqlite::memory:') - articles: Model\ArticleRepository # LANZA UNA EXCEPCIÓN, coinciden mainDb y tempDb -``` - -Una solución es saltarse el autowiring e indicar explícitamente el nombre del servicio (p. ej. `articles: Model\ArticleRepository(@mainDb)`). Un enfoque más cómodo es, sin embargo, [desactivar |#Desactivar el autowiring] el autowiring para uno de los servicios o [preferir |#Preferencia de autowiring] un servicio sobre los demás. - - -Desactivar el autowiring ------------------------- - -Podemos desactivar el autowiring de un servicio con la opción `autowired: false`: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - - tempDb: - create: PDO('sqlite::memory:') - autowired: false # el servicio tempDb queda excluido del autowiring - - articles: Model\ArticleRepository # por eso pasa mainDb al constructor -``` - -El servicio `articles` no lanzará una excepción por haber dos servicios `PDO` coincidentes (`mainDb` y `tempDb`) disponibles para el constructor, porque solo tiene en cuenta el servicio `mainDb`. - -El autowiring también se puede desactivar globalmente para tipos enteros con la opción de configuración [`di › excluded` |configuration#DI], que enumera los tipos (y sus descendientes) que nunca deben autoconectarse. - -.[note] -La configuración del autowiring en Nette se diferencia de la de Symfony. En Symfony, `autowire: false` significa que el autowiring no debe usarse para los argumentos del constructor del servicio. En Nette, el autowiring se aplica a los argumentos del constructor y a cualquier otro método invocado a través del contenedor (como la inyección por setter). La opción `autowired: false` impide que el contenedor pase automáticamente esta instancia del servicio como dependencia a otros servicios. - - -Preferencia de autowiring -------------------------- - -Si tenemos varios servicios del mismo tipo e indicamos la opción `autowired` en uno de ellos, ese servicio pasa a ser el preferido: - -```neon -services: - mainDb: - create: PDO(%dsn%, %user%, %password%) - autowired: PDO # pasa a ser el preferido - - tempDb: - create: PDO('sqlite::memory:') - - articles: Model\ArticleRepository -``` - -El servicio `articles` no lanzará una excepción por haber varios servicios `PDO` coincidentes (`mainDb` y `tempDb`), sino que usará el preferido, que es `mainDb`. - - -Colección de servicios ----------------------- - -El autowiring también puede pasar arrays de servicios de un tipo concreto. Como PHP no admite de forma nativa indicar el tipo de los elementos de un array en los type hints, hay que complementar el type hint `array` con un comentario phpDoc que indique el tipo de los elementos, del estilo `ClassName[]`: - -```php -namespace Model; - -class ShipManager -{ - /** - * @param Shipper[] $shippers - */ - public function __construct(array $shippers) - {} -} -``` - -El contenedor DI pasa entonces automáticamente un array de los servicios que corresponden al tipo indicado. Omite los servicios que tienen el [autowiring desactivado |#Desactivar el autowiring] y nunca incluye en su propia colección el servicio que se está creando. A diferencia de lo que ocurre al pasar un servicio individual, aquí no tienen ningún efecto el [estrechamiento |#Estrechar el autowiring] del autowiring a un tipo concreto ni marcar un servicio como [preferido |#Preferencia de autowiring]: el array contiene siempre todos los servicios del tipo dado. - -El tipo del comentario también puede tener la forma `array<int, Class>` o `list<Class>`. Si no puede controlar la forma del comentario phpDoc, puede pasar el array de servicios directamente en la configuración con [`typed()` |services#Funciones especiales]. - - -Argumentos escalares --------------------- - -El autowiring solo funciona para objetos y arrays de objetos. Los argumentos escalares (p. ej. cadenas, números, booleanos) hay que [indicarlos en la configuración |services#Argumentos]. Una alternativa es crear un [objeto de ajustes|best-practices:passing-settings-to-presenters] que encapsule el valor escalar (o varios valores). Ese objeto se puede pasar después mediante autowiring. - -```php -class MySettings -{ - public function __construct( - // readonly se puede usar desde PHP 8.1 - public readonly bool $value, - ) - {} -} -``` - -Lo registra como servicio añadiéndolo a la configuración: - -```neon -services: - - MySettings('any value') -``` - -Las demás clases pueden pedirlo entonces mediante autowiring. - - -Dependencias opcionales ------------------------ - -Si un parámetro del constructor o de un método tiene valor por defecto y en el contenedor no existe ningún servicio del tipo requerido, el autowiring no lanza una excepción: simplemente se salta el argumento, así que se usa el valor por defecto. Así se declaran las dependencias opcionales: - -```php -class Foo -{ - public function __construct( - private ?Logger $logger = null, - ) {} -} -``` - -En cambio, en un parámetro sin valor por defecto, la ausencia del servicio provoca siempre una excepción. - - -Estrechar el autowiring ------------------------ - -En los servicios concretos, el autowiring se puede estrechar a clases o interfaces determinadas. - -Normalmente, el autowiring pasa un servicio a todos los parámetros de método cuyo tipo coincida con el del servicio. Estrechar significa que establecemos condiciones que los tipos indicados en los parámetros de los métodos deben cumplir para que se les pase el servicio. - -Veamos un ejemplo: - -```php -class ParentClass -{} - -class ChildClass extends ParentClass -{} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Si los registráramos todos como servicios, el autowiring fallaría: - -```neon -services: - parent: ParentClass - child: ChildClass - parentDep: ParentDependent # LANZA UNA EXCEPCIÓN, coinciden los servicios parent y child - childDep: ChildDependent # el autowiring pasa el servicio child al constructor -``` - -El servicio `parentDep` lanza la excepción `Multiple services of type ParentClass found: child, parent`, porque tanto el servicio `parent` como el `child` encajan en su constructor y el autowiring no puede decidir cuál elegir. - -Para el servicio `child` podemos, por tanto, estrechar su autowiring al tipo `ChildClass`: - -```neon -services: - parent: ParentClass - child: - create: ChildClass - autowired: ChildClass # también se puede escribir como 'autowired: self' - - parentDep: ParentDependent # el autowiring pasa el servicio parent al constructor - childDep: ChildDependent # el autowiring pasa el servicio child al constructor -``` - -Ahora, al constructor del servicio `parentDep` se le pasa el servicio `parent`, porque es el único objeto coincidente. El servicio `child` ya no se le pasa por autowiring. Sí, el servicio `child` sigue siendo del tipo `ParentClass`, pero la condición de estrechamiento `autowired: ChildClass` significa que solo se pasará a parámetros tipados explícitamente como `ChildClass` (o sus subtipos). Como `ParentDependent` requiere `ParentClass`, el servicio `child` ya no se considera candidato para el autowiring ahí. - -Para el servicio `child`, `autowired: ChildClass` también se podría escribir como `autowired: self`, porque `self` es un marcador de posición para la clase del servicio actual. - -En la clave `autowired` también es posible indicar varias clases o interfaces como array: - -```neon -autowired: [ParentClass, FooInterface] -``` - -Probemos a añadir interfaces al ejemplo: - -```php -interface FooInterface -{} - -interface BarInterface -{} - -class ParentClass implements FooInterface -{} - -class ChildClass extends ParentClass implements BarInterface -{} - -class FooDependent -{ - function __construct(FooInterface $obj) - {} -} - -class BarDependent -{ - function __construct(BarInterface $obj) - {} -} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Si no restringimos el servicio `child` de ninguna manera, encajará en los constructores de todas las clases `FooDependent`, `BarDependent`, `ParentDependent` y `ChildDependent`, y el autowiring lo pasará a todos. - -Sin embargo, si estrechamos su autowiring a `ChildClass` con `autowired: ChildClass` (o `self`), el autowiring lo pasará solo al constructor de `ChildDependent`, porque este requiere un argumento del tipo `ChildClass` y se cumple que `ChildClass` *es del tipo* `ChildClass`. Ninguno de los tipos requeridos por los demás parámetros es `ChildClass` ni un subtipo suyo, así que el servicio no se les pasa. - -Si lo restringimos a `ParentClass` con `autowired: ParentClass`, el autowiring lo pasará de nuevo al constructor de `ChildDependent` (porque el `ChildClass` requerido es un subtipo de `ParentClass`) y ahora también al constructor de `ParentDependent`, porque el tipo requerido `ParentClass` también es adecuado. - -Si lo restringimos a `FooInterface`, se seguirá autoconectando a `ParentDependent` (el `ParentClass` requerido es un subtipo de `FooInterface`) y a `ChildDependent`, y además al constructor de `FooDependent`, pero no a `BarDependent`, porque `BarInterface` no es un subtipo de `FooInterface`. - -```neon -services: - child: - create: ChildClass - autowired: FooInterface - - fooDep: FooDependent # el autowiring pasa el servicio child al constructor - barDep: BarDependent # LANZA UNA EXCEPCIÓN, no coincide ningún servicio - parentDep: ParentDependent # el autowiring pasa el servicio child al constructor - childDep: ChildDependent # el autowiring pasa el servicio child al constructor -``` diff --git a/dependency-injection/es/compilation-internals.texy b/dependency-injection/es/compilation-internals.texy deleted file mode 100644 index ad0f71ebad..0000000000 --- a/dependency-injection/es/compilation-internals.texy +++ /dev/null @@ -1,222 +0,0 @@ -La compilación del contenedor en detalle -**************************************** - -.[perex] -Esta página abre la compilación del contenedor: las fases por las que pasa, cuándo se expanden los parámetros de configuración, cuándo las cadenas `@servicio` se convierten en referencias reales y, la pregunta que más hacen los autores de extensiones, en qué fase se puede buscar servicios por tipo con seguridad. Es la compañera profunda de [Creación de extensiones |extensions]. - -No necesita nada de esto para escribir una aplicación normal, ni siquiera una extensión normal. Pero en cuanto su extensión empieza a inspeccionar o a remodelar el grafo de servicios, el momento lo es todo: la misma llamada a `getByType()` da una respuesta fiable en una fase y engañosa en otra. Esta página explica por qué, para que siempre sepa dónde encaja su código. - - -Dos mundos: compilación frente a tiempo de ejecución -==================================================== - -Lo más importante que hay que entender es que un contenedor de Nette **no se monta en cada petición**. Se construye una vez en una clase PHP optimizada, esa clase se guarda en disco y cada petición posterior se limita a hacer `include` del archivo terminado. Toda la maquinaria descrita abajo (extensiones, resolvers, el generador de código) se ejecuta **solo durante la (re)compilación**. - -Eso divide el mundo en dos representaciones que nunca coexisten: - -| | durante la compilación | en tiempo de ejecución -|---|---|--- -| Qué existe | **definiciones** (recetas) en `ContainerBuilder` | **instancias** de los servicios en `Container` -| Clases clave | `Compiler`, `ContainerBuilder`, `Resolver`, `PhpGenerator` | `Container` (antecesor de la clase generada) -| `%param%`, `@servicio` | marcadores textuales todavía por traducir | ya traducidos / grabados en el código - -La clase generada extiende `Nette\DI\Container` y tiene un método `createServiceXxx()` por cada servicio. Sus parámetros y los metadatos de autowiring están precalculados, así que en tiempo de ejecución no queda nada por resolver, solo instanciar los servicios cuando se piden. - -.[note] -En modo de desarrollo, el contenedor se reconstruye automáticamente siempre que cambia un archivo de configuración o una clase de extensión; ambos se registran como dependencias. En producción se compila una vez y no se vuelve a comprobar, y de ahí viene la velocidad. - - -Las fases de un vistazo -======================= - -La compilación la orquesta `Compiler::compile()` y se reduce a tres pasos: - -```php -public function compile(): string -{ - $this->processExtensions(); // FASE A: esquemas + loadConfiguration() - $this->processBeforeCompile(); // FASE B: resolve + beforeCompile() + complete - return $this->generateCode(); // FASE C: generación del código + afterCompile() -} -``` - -Todo el modelo mental cabe en una única idea: **cada fase sabe más que la anterior.** - -- La **fase A** llena el grafo de definiciones. Los **tipos de los servicios todavía no se conocen de forma fiable**, porque un tipo puede venir del valor de retorno de una factory que nadie ha mirado aún. -- La **fase B** primero resuelve todos los tipos (`resolve`), después deja que las extensiones remodelen el grafo (`beforeCompile`) y por último [autoconecta |autowiring] los argumentos (`complete`). -- La **fase C** convierte el grafo terminado en PHP y deja que las extensiones toquen el código generado. - -Ese conocimiento creciente es justamente el motivo por el que la misma operación es segura en una fase y poco fiable en otra. El resto de la página recorre las fases con esa idea en mente. - - -Fase A: registrar las definiciones -================================== - -En esta fase, Nette llama a tres métodos de cada extensión (`getConfigSchema()`, luego `setConfig()`, luego `loadConfiguration()`), pero en un **orden cuidadosamente controlado**, porque aquí el orden importa de verdad. - - -Por qué importa el orden ------------------------- - -- **`ParametersExtension` y `ExtensionsExtension` van primero.** La primera debe ejecutarse antes que nada para poder expandir `%param%` por toda la configuración; todas las demás extensiones reciben así su propia sección con los valores ya rellenados. La segunda registra las demás extensiones indicadas en la sección `extensions:`, así que también tiene que existir antes de que se procesen las otras. -- **`ServicesExtension` va la última.** La sección `services:` del usuario tiene por tanto siempre la última palabra y puede sobrescribir cualquier cosa que hayan preparado las extensiones. -- **`InjectExtension` se desplaza al final del todo** para que su trabajo vea los setups añadidos por todas las demás extensiones. - -Lo que esto significa para usted: cuando se ejecuta el `loadConfiguration()` de su extensión, los parámetros ya están expandidos, pero los servicios del usuario todavía no están ahí. Ese único hecho gobierna la mayoría de las reglas de temporización de más abajo. - - -Convertir services: en definiciones ------------------------------------ - -La sección `services:` del usuario se convierte aquí en [objetos de definición |extensions#Tipos de definición], en el último paso de la fase A. Cada entrada NEON se normaliza (las notaciones abreviadas se unifican), se detecta su tipo (servicio corriente, factory, accessor, ...) y se crea la definición correspondiente en el builder. Este es también el primer momento en que los argumentos simples `@nombre` / `@Tipo` se convierten en referencias, véase [más abajo |#Referencias: cuándo @servicio se convierte en referencia]. - -Al final de la fase A están presentes todas las definiciones (cada extensión y el usuario han registrado lo que querían), pero la imagen todavía no está nítida: - -- **los tipos no están resueltos** en las definiciones cuyo tipo viene del valor de retorno de una factory, -- **los argumentos no están autoconectados**, -- algunas referencias `@servicio` siguen siendo simples cadenas. - -Justamente por eso, buscar aquí por tipo es poco fiable; más sobre ello [abajo |#Inspeccionar el ContainerBuilder: cuándo es seguro]. - - -Parámetros: cuándo se expande %param% -===================================== - -Una de las dos preguntas estrella. La respuesta es corta: **una vez, al principio de la fase A, en todo el árbol de configuración.** - -`ParametersExtension` se ejecuta la primera, y una de las primeras cosas que hace es expandir los marcadores `%param%`: primero dentro de los propios parámetros (un parámetro puede referirse a otro) y después por todo el resto de la configuración. Así que, cuando cualquier otra extensión, incluida `ServicesExtension`, recibe su sección, los marcadores ya han desaparecido. Las extensiones trabajan con valores concretos, nunca con `%...%`. - -Cuando el marcador es la cadena entera, su valor se devuelve *tal cual*, incluidos arrays y objetos, así que `%mailer%` puede expandirse a un array entero. En cualquier otro lugar se concatena en una cadena, y la notación con puntos `%foo.bar%` llega hasta arrays anidados. - - -Parámetros estáticos frente a dinámicos ---------------------------------------- - -No todos los valores se pueden grabar en el código. Un parámetro cuyo valor difiere según el entorno (una variable de entorno, la `baseUrl` derivada de la petición) debe seguir siendo **dinámico**. Esos parámetros se declaran con `setDynamicParameterNames()` o con `Expect::...->dynamic()` en un esquema; más en [parámetros dinámicos |application:bootstrapping#Parámetros dinámicos]. - -Un parámetro dinámico no se sustituye por un valor, sino por una expresión que lo lee *en tiempo de ejecución*. Así, `%env.DB_HOST%` no se congela en una cadena, sino que se convierte en una consulta en tiempo de ejecución dentro del contenedor generado. Todo lo demás es estático y se congela en tiempo de compilación, lo que es la fuente habitual de la sorpresa "mi valor de `getenv()` es el mismo en todos los entornos": el parámetro simplemente era estático. - -La operación contraria es el **escapado**: para que un `%` o un `@` literal no se interprete, se duplica (`%%`, `@@`). Nette lo hace automáticamente para los parámetros que le inyecta, así que sus valores nunca se confunden con marcadores ni con referencias. - - -Referencias: cuándo @servicio se convierte en referencia -======================================================== - -La segunda pregunta estrella. La traducción de `@servicio` ocurre **en varios pasos repartidos por distintas fases**, según lo compleja que sea la cadena. Rara vez tendrá que rastrearlo a mano, pero conocer los pasos explica por qué unas referencias se resuelven antes que otras. - -- **Parseo (carga de la configuración).** Un `@servicio` usado *como entidad*, es decir, como aquello que crea un servicio, como en `Foo(@bar)`, se convierte en referencia de inmediato. Un `@servicio` usado *como argumento* sigue siendo de momento una simple cadena. Un `@` entrecomillado se escapa a `@@`, así que cuenta como texto literal, no como referencia. -- **Fase A (`loadConfiguration`).** Al procesar las definiciones, un argumento limpio `@nombre` o `@Tipo` se convierte en un objeto `Reference`. Esto solo abarca las formas simples; `@servicio::CONST` o un `@` dentro de una expresión mayor se dejan para después. -- **Fase B (`complete`).** Aquí ocurre la traducción "inteligente" de verdad: `@servicio` -> referencia, `@servicio::CONSTANTE` -> una constante de clase literal, `@servicio::propiedad` -> la lectura de esa propiedad, `@@x` -> el texto literal `@x`. - -Hay una segunda traducción escondida en la propia palabra *referencia*. Una `Reference` puede apuntar por **nombre** o por **tipo** (`@Namespace\Type`). Una referencia por tipo **todavía no es un nombre de servicio**: se resuelve a un nombre concreto mediante el autowiring, y eso ocurre solo en el paso **complete**, una vez construido el índice de autowiring. Este es el puente con la siguiente sección: las búsquedas de autowiring se posponen deliberadamente hasta que el índice está listo. - -| Forma | Se convierte en referencia/expresión en | Se resuelve a un servicio concreto en -|---|---|--- -| entidad (`@foo` como factory) | parseo | complete -| argumento `@foo`, `@Tipo` | fase A | complete -| `@foo::CONST`, `@foo::prop` | fase B | complete -| referencia por tipo `@Tipo` | fase A/B | complete (autowiring) - - -Inspeccionar el ContainerBuilder: cuándo es seguro -================================================== - -Ahora, la pregunta que más hacen los autores de extensiones: **¿en qué método puedo buscar servicios por tipo?** La respuesta se deriva de una regla sencilla sobre cómo el builder lleva la cuenta de su propio estado. - -Buscar **por tipo** (`getByType()`, `getDefinitionByType()`, `findByType()`) necesita que el grafo de servicios esté *resuelto*: todos los tipos conocidos y el índice de autowiring construido. Por eso, cada vez que llama a uno de esos métodos y el grafo ha cambiado desde el último resolve, el builder **resuelve en el acto todo el grafo conocido**. Durante el propio resolve, cualquier búsqueda por tipo está prohibida y lanza `NotAllowedDuringResolvingException`. - -Buscar **por etiqueta** (`findByTag()`) no tiene ese requisito: las etiquetas no dependen de los tipos, así que funciona en **todas las fases**. - -Fase por fase: - -- **`loadConfiguration()` (fase A): buscar por tipo es poco fiable.** El grafo está incompleto: las extensiones que se ejecutan después todavía no han registrado sus servicios y, sobre todo, la sección `services:` del usuario (que va la última) no está ahí. Una llamada a `getByType()` sí funciona (provoca un resolve prematuro de un grafo parcial), pero la respuesta viene de una imagen incompleta y el resolve prematuro desperdicia trabajo. Regla práctica: **en `loadConfiguration()` limítese a registrar definiciones; no busque por tipo.** `findByTag()` no da problemas. -- **`beforeCompile()` (fase B): el lugar adecuado para inspeccionar.** A estas alturas existen **todas** las definiciones (incluidas las del usuario), los **tipos están resueltos** y el **índice de autowiring está construido**, así que `getByType()`, `findByType()` y `findByTag()` devuelven respuestas **fiables**. Los argumentos *todavía* no están autoconectados: eso es justo el paso siguiente (`complete`), después de todas las llamadas a `beforeCompile()`. Cuando modifica aquí una definición, el siguiente `getByType()` vuelve a resolver el grafo de forma transparente, así que puede alternar libremente ediciones y consultas. -- **`afterCompile()` (fase C): solo código.** Trabaja sobre la clase generada, no sobre el builder. El grafo está terminado; aquí da forma al PHP resultante. - -| Quiero... | Fase -|---|--- -| registrar un servicio | `loadConfiguration()` -| buscar por **etiqueta** y modificar definiciones | `loadConfiguration()` o `beforeCompile()` -| buscar por **tipo** (`getByType`/`findByType`) | **`beforeCompile()`** -| depender de qué servicios eligió el autowiring para los argumentos | no en tiempo de compilación: inspecciónelo en tiempo de ejecución -| tocar el código generado | `afterCompile()` -| ejecutar código después de arrancar el contenedor | [código de inicialización |extensions#Código de inicialización] - - -Dentro de la fase B: resolve y complete -======================================= - -La fase B son dos pasadas con las llamadas a `beforeCompile()` intercaladas entre ellas: - -```php -$this->builder->resolve(); // tipos resueltos, índice de autowiring construido -foreach ($this->extensions as $extension) { - $extension->beforeCompile(); -} -$this->builder->complete(); // SOLO AHORA se autoconectan los argumentos -``` - -**`resolve()`** determina el tipo de cada servicio (tomado de su `type` declarado o deducido de su factory: el tipo de retorno de un método fábrica, la clase que instancia o el servicio al que apunta una referencia) y después construye el índice de autowiring que asigna cada tipo (la clase más sus antecesores e interfaces) a un nombre de servicio. Un servicio marcado con `autowired: false` queda fuera del índice; `autowired: [A, B]` estrecha los tipos bajo los que es visible. Y algo crucial: resolve fija los *tipos*, no los *argumentos*; autoconectar los argumentos necesitaría el índice terminado, que solo existe después de esta pasada. - -**`complete()`** es donde ocurre realmente el autowiring de los argumentos. Para cada definición rellena los argumentos que faltan del constructor y del setup buscando sus tipos en el índice ya completo. Por eso las referencias por tipo se dejaron sin resolver durante resolve: la búsqueda pertenece a este punto, cuando ya hay un índice fiable en el que mirar. - - -Fase C: generar el código -========================= - -`generateCode()` entrega el grafo terminado a `PhpGenerator`, que produce una clase que extiende `Container` con un método `createServiceXxx()` por servicio, además de los metadatos precalculados `aliases`, `tags` y `wiring`. Cada `Statement` se convierte en texto PHP (`new Foo(...)`, llamadas a métodos, acceso a propiedades) y cada `Reference` se convierte en una llamada a `$this->getService(...)`. - -Las extensiones reciben después una última pasada `afterCompile()` sobre la clase generada; ahí es donde se emiten, por ejemplo, los getters de los parámetros estáticos y dinámicos, además de la oportunidad de añadir [código de inicialización |extensions#Código de inicialización] que se ejecuta en cada petición. - - -La línea de tiempo en una imagen -================================ - -``` -COMPILACIÓN (una vez, a la caché) -│ -├─ carga de la configuración NEON -> Statement/array; fusión de archivos -│ @ entrecomillado -> @@ ; entidades -> Statement -│ -▼ Compiler::compile() -│ -├─ FASE A processExtensions() -│ ├─ ParametersExtension (1.ª) ── %param% EXPANDIDOS en toda la configuración -│ │ los dinámicos -> expresión en tiempo de ejecución -│ ├─ ExtensionsExtension (1.ª) ── registra más extensiones -│ ├─ ...otras extensiones... ── loadConfiguration(): solo registrar definiciones -│ └─ ServicesExtension (ÚLTIMA) ── services: -> objetos Definition -│ @nombre/@Tipo -> Reference -│ [grafo completo en número; TIPOS y ARGUMENTOS todavía no; buscar por tipo no es fiable] -│ -├─ FASE B processBeforeCompile() -│ ├─ builder.resolve() ── resuelve los tipos; construye el índice de autowiring -│ │ [tipos listos; índice listo] -│ ├─ beforeCompile() extensiones ── getByType/findByType/findByTag SEGUROS aquí -│ │ (los argumentos aún no están autoconectados) -│ └─ builder.complete() ── autoconecta los ARGUMENTOS; termina las referencias -│ referencias por tipo -> nombres de servicio -│ -└─ FASE C generateCode() - ├─ PhpGenerator.generate() ── Statement -> PHP; métodos createServiceXxx() - ├─ afterCompile() extensiones ── retoca el código; emite los getters de parámetros - └─ toString() ── código PHP final -> caché - -──────────────────────────────────────────────────────────── - -TIEMPO DE EJECUCIÓN (cada petición) -│ -├─ new Container($dynamicParams) -├─ initialize() ── código de arranque de las extensiones (sesión, cabeceras) -└─ getService()/getByType() ── instancias lazy a partir de los metadatos precalculados -``` - - -Malentendidos habituales -======================== - -- "En `loadConfiguration()` buscaré los servicios por tipo." No: el grafo está incompleto (la sección `services:` del usuario se ejecuta después que usted) y `getByType()` provoca un resolve prematuro de un grafo parcial. Muévalo a `beforeCompile()`. `findByTag()` sí vale incluso aquí. -- "Un valor de `getenv()` en un parámetro será distinto en cada entorno." Solo si el parámetro es dinámico. Si no, queda grabado en tiempo de compilación y es el mismo en todas partes. -- "Una referencia `@Tipo` ya es un nombre de servicio." No lo es: es una referencia por tipo, que el autowiring resuelve a un nombre concreto solo en el paso complete. -- "Mi extensión lee un archivo auxiliar, pero los cambios no aparecen." Regístrelo con `$builder->addDependency($file)`; si no, la caché no sabe de él y no se reconstruirá. -- "Durante `resolve()` puedo llamar a `getByType()`." No: lanza `NotAllowedDuringResolvingException`. Buscar por tipo corresponde a `beforeCompile()` o después, nunca en mitad del resolve. diff --git a/dependency-injection/es/configuration.texy b/dependency-injection/es/configuration.texy deleted file mode 100644 index 7dfc6fd876..0000000000 --- a/dependency-injection/es/configuration.texy +++ /dev/null @@ -1,335 +0,0 @@ -Configuración del contenedor DI -******************************* - -.[perex] -Resumen de las opciones de configuración del contenedor DI de Nette. - - -Archivo de configuración -======================== - -El contenedor DI de Nette se controla fácilmente con archivos de configuración. Normalmente se escriben en el [formato NEON|neon:format]. Para editarlos recomendamos [editores con soporte |tools:ide] para este formato. - -<pre> -"decorator .[prism-token prism-atrule]":[#Decorator]: "Decorator .[prism-token prism-comment]"<br> -"di .[prism-token prism-atrule]":[#DI]: "Contenedor DI .[prism-token prism-comment]"<br> -"extensions .[prism-token prism-atrule]":[#Extensiones]: "Instalación de extensiones DI adicionales .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#Inclusión de archivos]: "Inclusión de archivos .[prism-token prism-comment]"<br> -"parameters .[prism-token prism-atrule]":[#Parámetros]: "Parámetros .[prism-token prism-comment]"<br> -"search .[prism-token prism-atrule]":[#Search]: "Registro automático de servicios .[prism-token prism-comment]"<br> -"services .[prism-token prism-atrule]":[services]: "Servicios .[prism-token prism-comment]" -</pre> - -.[note] -Para escribir una cadena que contenga el carácter `%`, hay que escaparlo duplicándolo a `%%`. - - -Parámetros -========== - -En la configuración puede definir parámetros que después se pueden usar como parte de las definiciones de los servicios. Eso le permite aclarar la configuración o centralizar valores que pueden cambiar. - -```neon -parameters: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: secret -``` - -Al parámetro `dsn` nos referimos en cualquier lugar de la configuración con la notación `%dsn%`. Los parámetros se pueden usar también dentro de cadenas como `'%wwwDir%/images'`. - -Los parámetros no tienen por qué ser solo cadenas o números: también pueden contener arrays: - -```neon -parameters: - mailer: - host: smtp.example.com - secure: ssl - user: franta@gmail.com - languages: [cs, en, de] -``` - -A una clave concreta nos referimos como `%mailer.user%`. - -Si su código (p. ej. una clase) necesita el valor de un parámetro, páseselo a la clase. Por ejemplo, en el constructor. No existe ningún objeto de configuración global al que las clases puedan preguntar por los valores de los parámetros. Eso sería una violación del principio de la inyección de dependencias. - - -Servicios -========= - -Véase el [capítulo aparte|services]. - - -Decorator -========= - -¿Cómo modificar de una vez varios servicios de un determinado tipo? Por ejemplo, ¿cómo llamar a un método concreto en todos los presenters que heredan de una determinada clase base? Para eso está el decorator. - -```neon -decorator: - # para todos los servicios que sean instancias de esta clase o interfaz - App\Presentation\BasePresenter: - setup: - - setProjectId(10) # llama a este método - - $absoluteUrls = true # y establece la variable -``` - -El decorator se puede usar también para poner [etiquetas |services#Etiquetas] o para activar el [modo inject |services#Modo inject]. - -```neon -decorator: - InjectableInterface: - tags: [mytag: 1] - inject: true -``` - - -DI -=== - -Ajustes técnicos del contenedor DI. - -```neon -di: - # ¿mostrar el DIC en la Tracy Bar? - debugger: ... # (bool) de forma predeterminada se autodetecta (activado si Tracy está presente) - - # tipos de parámetros que nunca se autoconectan - excluded: ... # (string[]) - - # ¿activar la creación lazy de los servicios? - lazy: ... # (bool) el valor predeterminado es false - - # la clase de la que hereda el contenedor DI - parentClass: ... # (string) el valor predeterminado es Nette\DI\Container -``` - - -Servicios lazy .{data-version:3.2.4} ------------------------------------- - -El ajuste `lazy: true` activa la creación lazy (diferida) de los servicios. Eso significa que los servicios no se crean realmente en el momento en que se piden al contenedor DI, sino solo en el momento de su primer uso. Esto puede acelerar el arranque de la aplicación y reducir el consumo de memoria, porque solo se crean los servicios realmente necesarios para cada petición. - -Para un servicio concreto, la creación lazy se puede [ajustar |services#Servicios lazy]. - -.[note] -Los objetos lazy solo se pueden usar para clases definidas por el usuario, no para clases internas de PHP. Requiere PHP 8.4 o superior. - - -Exportación de metadatos ------------------------- - -La clase del contenedor DI contiene también muchos metadatos. Puede reducir su tamaño reduciendo la exportación de metadatos. - -```neon -di: - export: - # ¿exportar los parámetros? - parameters: false # (bool) el valor predeterminado es true - - # ¿exportar las etiquetas y cuáles? - tags: # (string[]|bool) de forma predeterminada, todas - - event.subscriber - - # ¿exportar los datos para el autowiring y cuáles? - types: # (string[]|bool) de forma predeterminada, todos - - Nette\Database\Connection - - Symfony\Component\Console\Application -``` - -Si no usa `$container->getParameters()`, puede desactivar la exportación de parámetros. Además, puede exportar solo las etiquetas que realmente usa para obtener servicios con `$container->findByTag(...)`. Si no llama a ese método en absoluto, puede desactivar por completo la exportación de etiquetas con `false`. - -Puede reducir considerablemente los metadatos para el [autowiring|autowiring] enumerando solo las clases que realmente pide con `$container->getByType()`. De nuevo, si no llama a ese método (o lo llama solo en el archivo de [bootstrap|application:bootstrapping], p. ej. para obtener `Nette\Application\Application`), puede desactivar por completo la exportación de tipos con `false`. - - -Extensiones -=========== - -Registro de extensiones DI adicionales. Así se añade, por ejemplo, la extensión DI `Dibi\Bridges\Nette\DibiExtension3` bajo el nombre `dibi`: - -```neon -extensions: - dibi: Dibi\Bridges\Nette\DibiExtension3 -``` - -Después la configura en la sección `dibi`: - -```neon -dibi: - host: localhost -``` - -También puede añadir como extensión una clase con parámetros: - -```neon -extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, [%appDir%], %tempDir%/cache) -``` - - -Inclusión de archivos -===================== - -Se pueden incluir archivos de configuración adicionales en la sección `includes`: - -```neon -includes: - - parameters.php - - services.neon - - presenters.neon -``` - -El nombre `parameters.php` no es una errata; la configuración también se puede escribir en un archivo PHP que la devuelva como array: - -```php -<?php -return [ - 'database' => [ - 'main' => [ - 'dsn' => 'sqlite::memory:', - ], - ], -]; -``` - -Si en varios archivos de configuración aparecen elementos con las mismas claves, se sobrescriben o, en el caso de los arrays, se [fusionan |#Fusión]. Un archivo incluido después tiene mayor prioridad que el anterior. El archivo en el que está la sección `includes` tiene mayor prioridad que los archivos incluidos en él. - - -Search -====== - -El registro automático de servicios en el contenedor DI simplifica notablemente el desarrollo. Nette añade los presenters al contenedor automáticamente, pero puede añadir con facilidad cualquier otra clase. - -Basta con indicar en qué directorios (y subdirectorios) deben buscarse las clases: - -```neon -search: - - in: %appDir%/Forms - - in: %appDir%/Model -``` - -Si solo necesita una única regla de búsqueda, puede omitir la lista y escribir sus claves directamente bajo `search`: - -```neon -search: - in: %appDir% -``` - -Normalmente, sin embargo, no queremos añadir absolutamente todas las clases e interfaces, así que podemos filtrarlas: - -```neon -search: - - in: %appDir%/Forms - - # filtrado por nombre de archivo (string|string[]) - files: - - *Factory.php - - # filtrado por nombre de clase (string|string[]) - classes: - - *Factory -``` - -O podemos elegir las clases que heredan o implementan al menos una de las clases indicadas: - - -```neon -search: - - in: %appDir% - extends: - - App\*Form - implements: - - App\*FormInterface -``` - -También puede definir reglas de exclusión mediante máscaras de nombres de clase o de antecesores. Si una clase encaja con una regla de exclusión, no se añadirá al contenedor DI: - -```neon -search: - - in: %appDir% - exclude: - files: ... - classes: ... - extends: ... - implements: ... -``` - -A todos los servicios registrados automáticamente se les pueden asignar etiquetas: - -```neon -search: - - in: %appDir% - tags: ... -``` - -Además de las clases, la búsqueda registra también las interfaces que tienen un único método `create()` o `get()`, como [factories o accessors generados |factory]. Las clases para las que ya hay registrado en el contenedor un servicio del mismo tipo se omiten, así que no se crean duplicados. - - -Fusión -====== - -Si en varios archivos de configuración aparecen elementos con las mismas claves, se sobrescriben o, en el caso de los arrays, se fusionan. El archivo incluido después tiene mayor prioridad que el anterior. - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>resultado</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> - <td> -```neon -items: - - 1 - - 2 - - 3 -``` - </td> -</tr> -</table> - -En el caso de los arrays se puede impedir la fusión añadiendo un signo de exclamación tras el nombre de la clave: - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>resultado</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items!: - - 3 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> -</tr> -</table> - -{{maintitle: Configuración de la inyección de dependencias}} diff --git a/dependency-injection/es/container.texy b/dependency-injection/es/container.texy deleted file mode 100644 index db8f6c5753..0000000000 --- a/dependency-injection/es/container.texy +++ /dev/null @@ -1,142 +0,0 @@ -¿Qué es un contenedor DI? -************************* - -.[perex] -Un contenedor de inyección de dependencias (DIC o contenedor DI) es un objeto responsable de crear y configurar otros objetos (llamados servicios). - -Quizá le sorprenda, pero en muchos casos no hace falta un contenedor de inyección de dependencias para aprovechar las ventajas de la inyección de dependencias (DI, para abreviar). Al fin y al cabo, incluso en el [capítulo introductorio|introduction] mostramos ejemplos concretos de DI y no hizo falta ningún contenedor. - -Sin embargo, cuando hay que gestionar un gran número de objetos con dependencias complejas, un contenedor DI resulta muy útil. Es lo que suele ocurrir con las aplicaciones web construidas sobre un framework. - -En el capítulo anterior presentamos las clases `Article` y `EditController`. Ambas tienen dependencias, en concreto la base de datos y la factory `ArticleFactory`. Y para estas clases vamos a crear ahora un contenedor. Por supuesto, crear un contenedor para un ejemplo tan simple es una exageración. Pero lo crearemos para mostrar qué aspecto tiene y cómo funciona. - -Este es un contenedor sencillo, escrito a fuego, para el ejemplo anterior: - -```php -class Container -{ - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection('mysql:', 'root', '***'); - } - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->createDatabase()); - } - - public function createEditController(): EditController - { - return new EditController($this->createArticleFactory()); - } -} -``` - -Su uso sería así: - -```php -$container = new Container; -$controller = $container->createEditController(); -``` - -Simplemente le pedimos el objeto al contenedor, sin tener que saber cómo se crea ni cuáles son sus dependencias; de todo eso se ocupa el contenedor. Las dependencias las inyecta el contenedor automáticamente. En eso está su fuerza. - -De momento, el contenedor tiene toda la información escrita a fuego. Así que demos el siguiente paso y añadamos parámetros para que el contenedor sea de verdad útil: - -```php -class Container -{ - public function __construct( - private array $parameters, - ) { - } - - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection( - $this->parameters['db.dsn'], - $this->parameters['db.user'], - $this->parameters['db.password'], - ); - } - - // ... -} - -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); -``` - -Los lectores más atentos habrán detectado un problema. Cada vez que obtenemos un objeto `EditController` se crean también nuevas instancias de `ArticleFactory` y de la conexión a la base de datos. Eso desde luego no lo queremos. - -Por eso añadiremos un método `getService()` que devolverá siempre las mismas instancias: - -```php -class Container -{ - private array $services = []; - - public function __construct( - private array $parameters, - ) { - } - - public function getService(string $name): object - { - if (!isset($this->services[$name])) { - // getService('Database') llamará a createDatabase() - $method = 'create' . $name; - $this->services[$name] = $this->$method(); - } - return $this->services[$name]; - } - - // ... -} -``` - -En la primera llamada, por ejemplo `$container->getService('Database')`, llama a `createDatabase()` para crear el objeto de la base de datos, lo guarda en el array `$services` y lo devuelve. En las llamadas siguientes devuelve directamente la instancia ya guardada. - -Modificamos también el resto del contenedor para que use `getService()`: - -```php -class Container -{ - // ... - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->getService('Database')); - } - - public function createEditController(): EditController - { - return new EditController($this->getService('ArticleFactory')); - } -} -``` - -Por cierto, el término servicio se refiere a cualquier objeto gestionado por el contenedor. De ahí el nombre del método `getService()`. - -Listo. ¡Tenemos un contenedor DI plenamente funcional! Y podemos usarlo: - -```php -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); - -$controller = $container->getService('EditController'); -$database = $container->getService('Database'); -``` - -Como ve, escribir un DIC no es difícil. Conviene señalar que los propios objetos no saben que un contenedor los está creando. En consecuencia, así se puede crear cualquier objeto de PHP sin modificar su código fuente. - -Crear y mantener a mano la clase del contenedor puede convertirse rápidamente en una pesadilla. Por eso, en el siguiente capítulo hablaremos del [contenedor DI de Nette|nette-container], que sabe generarse y actualizarse casi por completo solo. - - -{{maintitle: ¿Qué es un contenedor de inyección de dependencias?}} diff --git a/dependency-injection/es/extensions.texy b/dependency-injection/es/extensions.texy deleted file mode 100644 index c68399c9df..0000000000 --- a/dependency-injection/es/extensions.texy +++ /dev/null @@ -1,443 +0,0 @@ -Creación de extensiones para Nette DI -************************************* - -.[perex] -Una extensión es una clase que se engancha a la compilación del contenedor DI. Puede registrar servicios mediante código, validar su propia sección de configuración, modificar servicios definidos por otros e incluso alterar el código generado del contenedor. Esta página le enseña cómo escribir una, qué ocurre en cada momento y a qué prestar atención. - -Las extensiones son la forma nativa en que los paquetes se integran en Nette: todos los paquetes `nette/*` las usan, y los suyos también pueden. Una extensión típica hace una o varias de estas cosas: - -- **integra una biblioteca**: registra sus servicios en el contenedor y expone una sección de configuración cómoda y validada (de ahí vienen las secciones `mail:` o `database:`) -- **automatiza el registro**: registra muchos servicios parecidos en un bucle o según una regla, cuando enumerarlos en `services:` resultaría tedioso -- **hace cambios transversales**: encuentra servicios registrados por otros y los completa, p. ej. engancha un logger a cada servicio con una determinada etiqueta - -Para el trabajo cotidiano con una aplicación rara vez necesitará una: la sección [services |services] de la configuración basta para registrar y conectar sus clases. Recurra a una extensión cuando la configuración por sí sola deje de ser suficiente. - -La extensión se activa en la sección `extensions`. Así se añade una extensión representada por la clase `BlogExtension` bajo el nombre `blog`: - -```neon -extensions: - blog: BlogExtension -``` - -Si su constructor acepta argumentos, páseselos ahí mismo: - -```neon -extensions: - blog: BlogExtension(%debugMode%) -``` - - -Cómo funciona la compilación -============================ - -Para escribir extensiones con confianza hay que saber una cosa clave: **cuándo se ejecuta su código**. Nette no conecta los servicios mientras atiende las peticiones. En su lugar *compila* el contenedor por adelantado: lee todos los archivos de configuración, deja que las extensiones hagan su trabajo y genera una clase PHP optimizada que guarda en disco. Cada petición posterior solo carga esa clase terminada. Por eso el código de su extensión se ejecuta únicamente cuando el contenedor se (re)construye, no en cada petición. - -Esto tiene una consecuencia importante: durante la compilación todavía no existe ningún servicio. Lo que existe son **definiciones**, recetas que describen de qué clase será cada servicio, cómo crearlo y qué llamar en él después. Las definiciones viven en el objeto [ContainerBuilder |#ContainerBuilder]. Una extensión es, en esencia, *configuración programable*: todo lo que puede declarar en la sección `services:` lo puede construir también en PHP, con condiciones, en bucles o reaccionando a lo que hayan registrado otros. - -La compilación transcurre por fases y una extensión puede intervenir en cada una de ellas: - -1) se validan las secciones de configuración de todas las extensiones (`getConfigSchema()`) -2) cada extensión registra sus servicios (`loadConfiguration()`); la sección `services:` del usuario se procesa la última, así que la aplicación siempre tiene la última palabra -3) una vez que todas las definiciones están en su sitio y los tipos de los servicios están resueltos, las extensiones pueden modificarlas (`beforeCompile()`) -4) se genera la clase del contenedor; las extensiones todavía pueden ajustar su código (`afterCompile()`) y emitir código que se ejecutará al arrancar la aplicación ([inicialización |#Código de inicialización]) - -.[note] -En modo de desarrollo, el contenedor se recompila automáticamente siempre que cambia un archivo de configuración o la propia clase de la extensión: ambos se registran como dependencias. Así puede desarrollar extensiones sin borrar nunca una caché. - -.[tip] -Para una mirada más profunda a lo que ocurre en cada fase (cuándo se expanden los parámetros, cuándo `@servicio` se convierte en una referencia y cuándo exactamente es seguro buscar servicios por tipo), véase [La compilación del contenedor en detalle |compilation-internals]. - - -Primera extensión -================= - -Aquí tiene una extensión pequeña pero completa. La activamos y la configuramos en el mismo archivo: - -```neon -extensions: - blog: BlogExtension - -blog: - postsPerPage: 5 -``` - -Y esta es la clase entera: - -```php -use Nette\Schema\Expect; - -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function getConfigSchema(): Nette\Schema\Schema - { - return Expect::structure([ - 'postsPerPage' => Expect::int(10), - 'allowComments' => Expect::bool(true), - ]); - } - - - public function loadConfiguration(): void - { - $builder = $this->getContainerBuilder(); - - $builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class, ['postsPerPage' => $this->config->postsPerPage]); - - if ($this->config->allowComments) { - $builder->addDefinition($this->prefix('comments')) - ->setFactory(Blog\Comments::class); - } - } -} -``` - -`getConfigSchema()` describe qué puede contener la sección `blog:` (llamada así por la clave bajo la que registramos la extensión), incluidos los tipos y los valores por defecto; los valores validados están después disponibles en `$this->config`. En `loadConfiguration()` registramos los servicios. Fíjese en los nombres: `$this->prefix('articles')` produce `blog.articles`, así que los servicios de distintas extensiones no pueden chocar. - -Y las últimas líneas muestran por qué existen las extensiones: el servicio `comments` solo se registra cuando los comentarios están activados. Un simple archivo de configuración no puede tomar decisiones así. - -Los servicios registrados de esta manera se comportan exactamente como si estuvieran escritos en `services:`: se crean de forma diferida cuando se necesitan y el autowiring los pasa allí donde haya un type hint de `Blog\Articles`. - -Los siguientes capítulos describen en detalle el ciclo de vida de la extensión, luego la API de [ContainerBuilder |#ContainerBuilder] que usará dentro de la extensión y, por último, las [trampas |#Consejos y trampas] que conviene conocer. - - -Ciclo de vida de la extensión -============================= - -Una extensión hereda de [api:Nette\DI\CompilerExtension] y sobrescribe algunos de los cuatro métodos `getConfigSchema()`, `loadConfiguration()`, `beforeCompile()` y `afterCompile()`, que el compilador llama en ese orden durante la compilación. - - -getConfigSchema(): Nette\Schema\Schema .[method] ------------------------------------------------- - -Define el esquema de la sección de configuración de la extensión. Gracias a él, los usuarios obtienen gratis validación y mensajes de error claros: una errata o un tipo equivocado en la sección `blog:` se comunica con un mensaje comprensible sin que usted escriba una sola comprobación. - -El esquema se describe con la biblioteca [Schema |schema:] y puede expresar tipos, valores por defecto, valores permitidos y mucho más: - -```php -public function getConfigSchema(): Nette\Schema\Schema -{ - return Expect::structure([ - 'postsPerPage' => Expect::int(10), - 'storage' => Expect::anyOf('files', 'database')->firstIsDefault(), - ]); -} -``` - -La configuración validada está disponible en `$this->config` como objeto `stdClass` (o como array, si añade `castTo('array')` al esquema). - -Si el valor de una opción no se puede conocer en tiempo de compilación, porque proviene, por ejemplo, de una variable de entorno, márquelo con `dynamic()`, p. ej. `Expect::int()->dynamic()`. Más en [parámetros dinámicos |application:bootstrapping#Parámetros dinámicos]. - - -loadConfiguration() .[method] ------------------------------ - -El lugar donde la extensión registra sus servicios, mediante el [ContainerBuilder |#ContainerBuilder]: - -```php -public function loadConfiguration(): void -{ - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class); -} -``` - -Si un servicio debe estar disponible también bajo un nombre corto, añada un alias. Por convención, esto se hace solo cuando la extensión está registrada con su nombre habitual, para que varias instancias de la extensión no se peleen por él: - -```php -if ($this->name === 'blog') { - $builder->addAlias('articles', $this->prefix('articles')); -} -``` - -Cuando hay muchos servicios, puede resultar más cómodo definirlos en un archivo NEON aparte con la conocida sintaxis de [services |services]. El prefijo `@extension` se refiere a la extensión actual: - -```neon -services: - articles: - create: MyBlog\ArticlesModel(@connection) - - comments: - create: MyBlog\CommentsModel(@connection, @extension.articles) -``` - -Estas definiciones las cargamos con `loadDefinitionsFromConfig()`; los nombres reciben el prefijo automáticamente y el archivo se registra como dependencia, así que cambiarlo provoca la recompilación: - -```php -public function loadConfiguration(): void -{ - $this->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/services.neon')['services'], - ); -} -``` - - -beforeCompile() .[method] -------------------------- - -Cuando se llama a este método, el builder ya contiene **todas** las definiciones: las suyas, las de las demás extensiones y las de los archivos de configuración del usuario. Los tipos de los servicios también están resueltos, así que buscar por tipo es fiable. Eso hace que esta fase sea ideal para inspeccionar y completar el grafo final de servicios. - -Normalmente se buscan los servicios por etiqueta o por tipo y se completan las definiciones encontradas: - -```php -public function beforeCompile(): void -{ - $builder = $this->getContainerBuilder(); - - foreach ($builder->findByTag('logaware') as $name => $attrs) { - $builder->getDefinition($name)->addSetup('setLogger'); - } -} -``` - -La llamada a `setLogger()` no lleva argumentos explícitos: los suministrará el autowiring, igual que hace en las factories. - -También puede colaborar con otras extensiones registradas, obtenidas con `$this->compiler->getExtensions()` y filtradas opcionalmente por clase o interfaz: - -```php -foreach ($this->compiler->getExtensions(FooExtension::class) as $extension) { - // ... -} -``` - - -afterCompile(Nette\PhpGenerator\ClassType $class) .[method] ------------------------------------------------------------ - -En la última fase, la clase del contenedor se genera como un objeto [ClassType |php-generator:#Clases] de la biblioteca [PHP Generator |php-generator:]. Contiene un método factory por cada servicio y está a punto de escribirse en la caché. Todavía puede modificar su código: - -```php -public function afterCompile(Nette\PhpGenerator\ClassType $class): void -{ - $method = $class->getMethod('__construct'); - // ... -} -``` - -Esta fase la necesitará solo rara vez. Para añadir código que se ejecute al arrancar la aplicación, use en su lugar la inicialización: - - -Código de inicialización ------------------------- - -Todas las fases anteriores influyen en cómo se *construye* el contenedor. Además, una extensión puede emitir código que se ejecute en *tiempo de ejecución*, justo después de crear el contenedor, por ejemplo para arrancar una sesión o poner en marcha servicios. El código se escribe en el objeto `$this->initialization` con su método [addBody() |php-generator:#Cuerpos de métodos y funciones]: - -```php -public function loadConfiguration(): void -{ - // los servicios con la etiqueta 'run' deben crearse justo después de arrancar el contenedor - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $attrs) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } -} -``` - -El propio Nette usa la inicialización, por ejemplo, para arrancar automáticamente la sesión o para enviar cabeceras HTTP de seguridad. Y tenga presente que, a diferencia de todo lo demás en una extensión, este código se ejecuta en **cada petición**, así que manténgalo pequeño. - - -ContainerBuilder -================ - -[api:Nette\DI\ContainerBuilder] es el objeto a través del cual la extensión habla con el compilador. Contiene las [definiciones |#Cómo funciona la compilación] de todos los servicios y ofrece métodos para añadirlas, buscarlas y modificarlas. Lo obtiene en `loadConfiguration()` y en `beforeCompile()`: - -```php -$builder = $this->getContainerBuilder(); -``` - - -Añadir servicios ----------------- - -Registrar un servicio es lo mismo que hace en la sección `services:` de un archivo NEON, solo que escrito en PHP. Cada clave de la configuración tiene un método correspondiente en la definición, así que estas dos notaciones son equivalentes: - -```neon -services: - articles: - create: Blog\Articles(@connection) - setup: - - setLogger(@logger) - tags: [logaware] -``` - -```php -$builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class, ['@connection']) - ->addSetup('setLogger', ['@logger']) - ->addTag('logaware'); -``` - -La definición que devuelve `addDefinition()` es una [ServiceDefinition |#Tipos de definición] que ofrece los equivalentes de las claves de configuración: `setType()` (la clase del servicio), `setFactory()` (cómo crearlo), `setArguments()`, `addSetup()`, `addTag()` y `setAutowired()`. - -`addSetup()` refleja la lista `setup:` y acepta las mismas formas: una llamada a método `addSetup('setLogger', ['@logger'])`, una asignación a una propiedad `addSetup('$cache', ['@cache'])` o una llamada sobre otro servicio `addSetup('@Tracy\Bar::addPanel', [$panel])`. - -Además de los servicios corrientes, el builder puede registrar también [factories generadas |factory], accessors y locators, cada uno con su propio método que devuelve el [tipo de definición |#Tipos de definición] correspondiente: - -| Método | Registra -|--------|---------- -| `addDefinition()` | un servicio corriente (devuelve `ServiceDefinition`) -| `addFactoryDefinition()` | una [factory |factory] generada (interfaz con un método `create()`) -| `addAccessorDefinition()` | un [accessor |factory#Accessor] generado (interfaz con un método `get()`) -| `addLocatorDefinition()` | una [multifactory / locator |factory#Multifactory/Accessor] que combina varias factories -| `addImportedDefinition()` | un servicio pasado al contenedor desde fuera en tiempo de ejecución -| `addAlias()` | un segundo nombre para un servicio existente - -Con una factory, el objeto que crea se configura mediante `getResultDefinition()`; un accessor, en cambio, apunta a un servicio existente con `setReference()`: - -```php -$builder->addFactoryDefinition($this->prefix('latteFactory')) - ->setImplement(LatteFactory::class) - ->getResultDefinition() - ->setFactory(Latte\Engine::class) - ->addSetup('setStrictTypes', [true]); -``` - -`addLocatorDefinition()` y `addImportedDefinition()` se necesitan rara vez: esos servicios suelen venir de las claves `implement:` y de los servicios importados en NEON, en lugar de escribirse a mano. - - -Buscar y modificar servicios ----------------------------- - -Para buscar y recorrer las definiciones existentes, el builder ofrece: - -| Método | Descripción -|--------|------------ -| `getDefinition(string $name)` | la definición con el nombre dado (lanza una excepción si falta) -| `hasDefinition(string $name)` | si existe una definición o un alias con ese nombre -| `getDefinitions()` | todas las definiciones -| `removeDefinition(string $name)` | elimina una definición -| `getByType(string $type)` | el nombre del servicio autowired de ese tipo, o `null` -| `getDefinitionByType(string $type)` | la definición autowired de ese tipo -| `findByType(string $type)` | todas las definiciones de ese tipo como pares `nombre => definición` -| `findByTag(string $tag)` | los servicios que llevan la etiqueta como pares `nombre => valor de la etiqueta` -| `addExcludedClasses(array $types)` | excluye clases e interfaces del autowiring - -Un modismo práctico es usar `getByType()` para averiguar si un servicio existe siquiera, por ejemplo para engancharse a un logger solo cuando la aplicación tiene uno: - -```php -if ($builder->getByType(Psr\Log\LoggerInterface::class)) { - $builder->getDefinition($this->prefix('articles')) - ->addSetup('setLogger'); -} -``` - - -Tipos de definición -------------------- - -Cada método `add*Definition()` devuelve un tipo distinto de definición. Todos ellos extienden el antecesor común `Nette\DI\Definitions\Definition`: - -- **`ServiceDefinition`**: un servicio corriente; se configura con `setType()`, `setFactory()`, `addSetup()`, `addTag()` y `setAutowired()` -- **`FactoryDefinition`**: una [factory generada |factory]: una interfaz cuyo método `create()` devuelve un objeto nuevo en cada llamada -- **`AccessorDefinition`**: un [accessor generado |factory#Accessor]: una interfaz cuyo método `get()` devuelve un servicio existente -- **`LocatorDefinition`**: una [multifactory / locator |factory#Multifactory/Accessor] que combina varias factories o accessors en una sola interfaz -- **`ImportedDefinition`**: un servicio que el contenedor no crea él mismo, sino que recibe desde fuera en tiempo de ejecución - -Tenga presente que `getDefinition()` devuelve el tipo de definición que viva bajo el nombre dado. Si su código puede toparse con una factory generada, compruebe primero el tipo y configure el objeto producido mediante `getResultDefinition()`: - -```php -$def = $builder->getDefinition($name); -if ($def instanceof Nette\DI\Definitions\FactoryDefinition) { - $def = $def->getResultDefinition(); -} -$def->addSetup('setLogger'); -``` - - -Consejos y trampas -================== - - -Tiempo de compilación frente a tiempo de ejecución --------------------------------------------------- - -La fuente de confusión más habitual: el código de la extensión se ejecuta cuando el contenedor se **compila**, no cuando la aplicación atiende peticiones. En la práctica eso significa: - -- Una extensión nunca trabaja con instancias de servicios: todavía no existen. No instancie servicios con `new`; registre una definición y deje que el contenedor los cree. -- Todos los valores de configuración quedan grabados en el código generado. Un valor que puede diferir entre entornos (una ruta, una contraseña de `getenv()`) debe marcarse como [dinámico |application:bootstrapping#Parámetros dinámicos]; de lo contrario queda congelado en tiempo de compilación. -- Las cadenas que se pasan a `$this->initialization->addBody()` no se ejecutan ahora: son código PHP emitido al contenedor que se ejecuta en cada petición. - - -Dependencias de archivos ------------------------- - -El contenedor se recompila cuando cambian los archivos de configuración o las clases de las extensiones. Pero si su extensión lee cualquier otro archivo (una lista de entidades, una configuración XML de una biblioteca), el contenedor no tiene manera de enterarse. Registre esos archivos con: - -```php -$builder->addDependency($file); -``` - -De lo contrario le espera un misterio clásico: edita el archivo, pero la aplicación se sigue comportando como antes, y el cambio solo aparece cuando el contenedor se reconstruye por algún otro motivo. (Los archivos leídos con `loadFromFile()` se registran automáticamente.) - - -Registro condicional --------------------- - -Una extensión puede adaptarse a su entorno. Las integraciones opcionales se protegen normalmente con `class_exists()`: - -```php -if (class_exists(Symfony\Component\Console\Command\Command::class)) { - $builder->addDefinition($this->prefix('command')) - ->setFactory(Blog\Console\SitemapCommand::class); -} -``` - -Y los valores como `%debugMode%` es mejor pasarlos por el constructor de la extensión: - -```neon -extensions: - blog: BlogExtension(%debugMode%) -``` - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function __construct( - private bool $debugMode = false, - ) {} -} -``` - -Un uso típico es registrar un panel de Tracy solo en modo de desarrollo. - - -Argumentos complejos --------------------- - -A veces un argumento para una factory o para una llamada del setup no es un valor simple, un nombre de clase o una referencia `@servicio`. Para esos casos existen: - -- `new Nette\DI\Definitions\Statement(Blog\Panel::class, [$args])`: un objeto creado en el sitio, un "servicio anónimo" usado como argumento -- `new Nette\DI\Definitions\Reference('blog.articles')`: una referencia a un servicio, la contrapartida como objeto de la cadena `@nombre` -- `$builder::literal('PHP_SAPI')`: un trozo de código PHP en bruto insertado tal cual en el contenedor generado - -Ejemplo: registrar un panel de Tracy: - -```php -$builder->getDefinition($this->prefix('articles')) - ->addSetup('@Tracy\Bar::addPanel', [ - new Nette\DI\Definitions\Statement(Blog\ArticlesPanel::class), - ]); -``` - - -Etiquetas y tipos exportados ----------------------------- - -La [exportación de metadatos |configuration#Exportación de metadatos] se puede restringir en la configuración para que el contenedor compilado conserve solo las etiquetas y los tipos de autowiring que la aplicación realmente usa. Si su extensión obtiene servicios en tiempo de ejecución con `$container->findByTag()` o `$container->getByType()`, una restricción así podría eliminar justamente los metadatos de los que depende. - -Para evitarlo, dígale al compilador qué etiquetas y tipos deben exportarse siempre: - -```php -public function loadConfiguration(): void -{ - // esta etiqueta se exportará siempre, aunque la exportación esté restringida - $this->compiler->addExportedTag('event.subscriber'); - - // este tipo estará siempre disponible para getByType() - $this->compiler->addExportedType(Nette\Database\Connection::class); -} -``` - -Ambos métodos solo añaden a los metadatos exportados; nunca sobrescriben la configuración `di › export` de la aplicación. Así que, cuando la aplicación restringe la exportación a una lista, las etiquetas y los tipos que su extensión necesita siguen incluidos; solo desactivar por completo la exportación de etiquetas (`tags: false`) las descarta junto con todo lo demás. diff --git a/dependency-injection/es/factory.texy b/dependency-injection/es/factory.texy deleted file mode 100644 index 271383f43f..0000000000 --- a/dependency-injection/es/factory.texy +++ /dev/null @@ -1,233 +0,0 @@ -Factories generadas -******************* - -.[perex] -Nette DI puede generar automáticamente el código de las factories a partir de interfaces, lo que le ahorra escribir código. - -Una factory es una clase que se encarga de crear objetos y de pasarles sus dependencias. No lo confunda, por favor, con el patrón de diseño *factory method*, que describe una forma concreta de usar las factories y no está relacionado con este tema. - -Ya hemos mostrado qué aspecto tiene una factory así en el [capítulo introductorio |introduction#Factory]: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Nette DI puede generar automáticamente el código de la factory. Todo lo que tiene que hacer es crear una interfaz y Nette DI generará la implementación. La interfaz debe tener exactamente un método llamado `create` y declarar un tipo de retorno: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Es decir, la factory `ArticleFactory` tiene un método `create` que crea objetos `Article`. La clase `Article` podría tener, por ejemplo, este aspecto: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } -} -``` - -Añada la factory al archivo de configuración: - -```neon -services: - - ArticleFactory -``` - -Nette DI generará la implementación correspondiente de la factory. - -En el código que usa la factory, pida el objeto por su interfaz y Nette DI le proporcionará la implementación generada: - -```php -class UserController -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function foo() - { - // dejamos que la factory cree el objeto - $article = $this->articleFactory->create(); - } -} -``` - - -Factory parametrizada -===================== - -El método `create` de la factory puede aceptar parámetros, que después pasa al constructor. Añadamos, por ejemplo, a la clase `Article` el ID del autor del artículo: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - private int $authorId, - ) { - } -} -``` - -Añadiremos el parámetro también a la factory: - -```php -interface ArticleFactory -{ - function create(int $authorId): Article; -} -``` - -Como el nombre del parámetro en el constructor (`$authorId`) coincide con el nombre del parámetro del método de la factory, Nette DI lo pasa automáticamente. - - -Definición avanzada -=================== - -La definición también se puede escribir en forma de varias líneas con la clave `implement`: - -```neon -services: - articleFactory: - implement: ArticleFactory -``` - -Usar este formato más largo permite indicar argumentos adicionales para el constructor con la clave `arguments` y configurar más cosas con `setup`, de forma parecida a las definiciones de servicios corrientes. - -Ejemplo: si el método `create()` no aceptara el parámetro `$authorId`, podríamos indicar en la configuración un valor fijo que se pasaría al constructor de `Article`: - -```neon -services: - articleFactory: - implement: ArticleFactory - arguments: - authorId: 123 -``` - -Y al revés: si `create()` aceptara `$authorId`, pero este no formara parte del constructor y se pasara mediante un método como `Article::setAuthorId()`, nos referiríamos al parámetro en la sección `setup`: - -```neon -services: - articleFactory: - implement: ArticleFactory - setup: - - setAuthorId($authorId) -``` - - -Accessor -======== - -Además de las factories, Nette también puede generar los llamados accessors. Son objetos con un método `get()` que devuelve un servicio concreto del contenedor DI. Las llamadas repetidas a `get()` devuelven siempre la misma instancia. - -Los accessors ofrecen carga diferida de las dependencias. Imagine una clase que registra errores en una base de datos dedicada. Si esa clase recibiera la conexión a la base de datos por inyección en el constructor, la conexión se establecería siempre, aunque los errores se produzcan rara vez y la conexión quede sin usar la mayor parte del tiempo. En su lugar, la clase puede recibir un accessor. El objeto de la base de datos (la conexión) se crea solo cuando se llama por primera vez al método `get()` del accessor. - -¿Cómo se crea un accessor? Basta con escribir una interfaz y Nette DI generará la implementación. La interfaz debe tener exactamente un método llamado `get` que no acepte parámetros y declare el tipo de retorno: - -```php -interface PDOAccessor -{ - function get(): PDO; -} -``` - -Añada el accessor al archivo de configuración junto con la definición del servicio que debe devolver: - -```neon -services: - - PDOAccessor - - PDO(%dsn%, %user%, %password%) -``` - -Como el accessor devuelve un servicio `PDO` y en la configuración solo hay un servicio así definido, el accessor devolverá justamente ese servicio. Si existieran varios servicios de ese tipo, indique por su nombre cuál debe devolver el accessor, p. ej. `- PDOAccessor(@db1)`. - - -Multifactory/Accessor -===================== - -Hasta ahora, nuestras factories y accessors solo podían crear o devolver un único tipo de objeto. Pero puede crear con facilidad multifactories, que combinan las características de las factories y de los accessors. La interfaz de un componente así puede contener varios métodos llamados `create<Name>()` y `get<Name>()`, por ejemplo: - -```php -interface MultiFactory -{ - function createArticle(): Article; - function getDb(): PDO; -} -``` - -Así, en lugar de inyectar varias factories y accessors individuales, puede inyectar un único componente más completo. - -Alternativamente, en lugar de varios métodos se puede usar `get()` con un parámetro: - -```php -interface MultiFactoryAlt -{ - function get($name): PDO; -} -``` - -Entonces, `MultiFactory::getDb()` hace lo mismo que `MultiFactoryAlt::get('db')`. Esta notación alternativa tiene, sin embargo, la desventaja de que los valores admitidos para `$name` no se ven claramente en la firma de la interfaz. Además, no puede definir tipos de retorno distintos para distintos valores de `$name` dentro de la interfaz. - -En lugar de `get($name)`, la interfaz puede declarar `create($name)`, que devuelve una instancia nueva en cada llamada (mientras que `get()` devuelve una compartida). La interfaz solo puede contener un método parametrizado de este tipo. Si el tipo de retorno del método admite null (p. ej. `?PDO`), devuelve `null` para un `$name` desconocido en lugar de lanzar una excepción. - - -Definición con una lista ------------------------- -Puede definir una multifactory en la configuración con una lista, escribiendo los servicios en línea: .{data-version:3.2.0} - -```neon -services: - - MultiFactory( - article: Article() # define createArticle() - db: PDO(%dsn%, %user%, %password%) # define getDb() - ) -``` - -Alternativamente puede referirse a servicios existentes en la definición de la multifactory mediante referencias: - -```neon -services: - article: Article - - PDO(%dsn%, %user%, %password%) - - MultiFactory( - article: @article # define createArticle() - db: @\PDO # define getDb() - ) -``` - - -Definición con etiquetas ------------------------- - -Otra forma de definir una multifactory es usar [etiquetas |services#Etiquetas]. El valor de la etiqueta determina el nombre del método correspondiente: - -```neon -services: - article: - create: Article - tags: {multi: article} # define createArticle() - db: - create: PDO(%dsn%, %user%, %password%) - tags: {multi: db} # define getDb() - - - MultiFactory(tagged: multi) -``` diff --git a/dependency-injection/es/faq.texy b/dependency-injection/es/faq.texy deleted file mode 100644 index c5d3ecb7eb..0000000000 --- a/dependency-injection/es/faq.texy +++ /dev/null @@ -1,123 +0,0 @@ -Preguntas frecuentes sobre DI (FAQ) -*********************************** - - -¿Es DI otro nombre para IoC? ----------------------------- - -*Inversion of Control* (IoC) es un principio que describe el flujo de control de un programa: ¿es su código el que llama a código externo o es el código externo (como un framework) el que llama al suyo? IoC es un concepto amplio que incluye los [eventos |nette:glossary#Eventos], el llamado [principio de Hollywood |application:components#Estilo Hollywood] y otros aspectos. Este concepto abarca también las factories, de las que habla la [Rule #3: Let the Factory Handle It |introduction#Regla n.º 3: déjelo en manos de la factory], y que representan una inversión del operador `new`. - -*Dependency Injection* (DI) se centra en cómo obtienen los objetos sus dependencias (es decir, los otros objetos con los que necesitan trabajar). Es un patrón de diseño que aboga por pasar las dependencias explícitamente a los objetos en lugar de que estos las creen o las busquen. - -Por tanto, la DI se puede considerar una forma concreta de IoC. Pero no todas las formas de IoC fomentan un código limpio. Son antipatrones, por ejemplo, las técnicas basadas en el [estado global|global-state] o en el patrón [Service Locator |#¿Qué es un Service Locator?]. - - -¿Qué es un Service Locator? ---------------------------- - -Es un enfoque alternativo a la inyección de dependencias. Consiste en un objeto central (el locator) en el que se registran todos los servicios (dependencias) disponibles. Cuando un objeto necesita una dependencia, se la pide al Service Locator. - -Comparado con la DI, sin embargo, le falta transparencia. Las dependencias quedan escondidas dentro del código del objeto (las llamadas al locator) en lugar de ser explícitas en su API (constructor o métodos), lo que obliga a inspeccionar el código para entender las conexiones. Las pruebas también son más complicadas, porque no puede simplemente pasar dependencias simuladas al crear el objeto; a menudo hay que manipular el propio Service Locator. Además, el Service Locator introduce una dependencia innecesaria: los objetos quedan acoplados al locator, a diferencia de lo que ocurre con la DI, donde lo ideal es que los objetos ni siquiera sepan del contenedor. - - -¿Cuándo es mejor no usar DI? ----------------------------- - -No se conocen inconvenientes significativos de usar correctamente el patrón de diseño de la inyección de dependencias. Al contrario, obtener las dependencias de lugares accesibles globalmente (como propiedades estáticas o singletons) trae [numerosas complicaciones|global-state], igual que usar un Service Locator. Por eso usar DI es en general siempre recomendable. No es un dogma; simplemente no se ha impuesto ninguna alternativa mejor para gestionar las dependencias de forma limpia. - -Hay, sin embargo, situaciones concretas y limitadas en las que acceder a los objetos globalmente puede ser aceptable. Por ejemplo, durante la depuración, cuando necesita volcar el valor de una variable, medir el tiempo de ejecución o registrar un mensaje en un punto concreto. En esos casos, que implican acciones temporales que después se eliminarán del código, usar un dumper, un cronómetro o un logger accesible globalmente puede ser legítimo. Esas herramientas no forman parte del diseño esencial de la aplicación. - - -¿Tiene inconvenientes usar DI? ------------------------------- - -¿Usar la inyección de dependencias trae desventajas, como más esfuerzo al escribir código o peor rendimiento? ¿Qué perdemos cuando empezamos a escribir código conforme a la DI? - -La DI en sí tiene un impacto insignificante en el rendimiento en tiempo de ejecución o en el consumo de memoria. El rendimiento del contenedor DI sí puede influir, pero [Nette DI |nette-container] compila el contenedor a código PHP puro, con lo que la sobrecarga durante la ejecución de la aplicación es prácticamente nula. - -Al escribir código siguiendo los principios de la DI, a menudo hay que crear constructores que aceptan dependencias. Aunque en el pasado eso pudiera parecer tedioso, los IDE modernos y características como la [promoción de propiedades del constructor |https://blog.nette.org/es/php-8-0-complete-overview-of-news#toc-constructor-property-promotion] de PHP 8 lo hacen muy rápido. Las factories las puede generar a menudo Nette DI automáticamente, lo que reduce aún más el código repetitivo. Por otro lado, desaparece la necesidad de escribir singletons y puntos de acceso estáticos. - -En conjunto, una aplicación bien diseñada que usa DI no suele ser ni notablemente más corta ni más larga que una que se apoya en singletons o en el acceso global. El código relacionado con la creación y la conexión de las dependencias simplemente se traslada de las clases individuales a lugares dedicados: la configuración del contenedor DI y las factories. - - -¿Cómo reescribir una aplicación heredada para que use DI? ---------------------------------------------------------- - -Migrar una aplicación heredada a la inyección de dependencias puede ser un proceso exigente, sobre todo en aplicaciones grandes y complejas. Es importante abordar el proceso de forma sistemática. - -- Al pasar a la inyección de dependencias es importante que todos los miembros del equipo entiendan los principios y las prácticas que se van a usar. -- Primero, analice la aplicación existente para identificar los componentes clave y sus dependencias. Cree un plan sobre qué partes se refactorizarán y en qué orden. -- Implemente un contenedor DI o, mejor aún, use una biblioteca existente como Nette DI. -- Refactorice poco a poco las partes de la aplicación para que usen la inyección de dependencias. Eso puede implicar modificar constructores o métodos para que acepten las dependencias como parámetros. -- Actualice el código donde se crean los objetos para obtenerlos del contenedor o usar las factories que este proporciona. - -Recuerde que pasar a la inyección de dependencias es una inversión en la calidad del código y en la mantenibilidad a largo plazo de la aplicación. Aunque hacer estos cambios pueda resultar exigente, el resultado debería ser un código más limpio, más modular y fácilmente testeable, preparado para futuras ampliaciones y para su mantenimiento. - - -¿Por qué se prefiere la composición a la herencia? --------------------------------------------------- -Usar la [composición |nette:introduction-to-object-oriented-programming#Composición] se prefiere en general a la [herencia |nette:introduction-to-object-oriented-programming#Herencia] para reutilizar código, porque lleva a un acoplamiento más laxo. Con la composición es menos probable que se encuentre con problemas en los que cambiar una clase base rompe las subclases dependientes. Un ejemplo típico es la situación conocida como [infierno del constructor |passing-dependencies#Infierno del constructor]. - - -¿Se puede usar el contenedor DI de Nette fuera de Nette? --------------------------------------------------------- - -Por supuesto. El contenedor DI de Nette forma parte de Nette, pero está diseñado como una biblioteca independiente que se puede usar al margen de las demás partes del framework. Basta con instalarlo con Composer, crear un archivo de configuración que defina sus servicios y después usar unas pocas líneas de código PHP para crear el contenedor DI. Y puede empezar de inmediato a aprovechar la inyección de dependencias en sus proyectos. - -El capítulo sobre el [contenedor DI de Nette |nette-container] describe un caso de uso concreto con ejemplos de código. - - -¿Por qué la configuración está en archivos NEON? ------------------------------------------------- - -NEON es un lenguaje de configuración sencillo y fácil de leer, desarrollado dentro de Nette para configurar aplicaciones, servicios y sus dependencias. Comparado con JSON o YAML ofrece para ese fin opciones mucho más intuitivas y flexibles. En NEON puede describir con naturalidad definiciones de servicios y relaciones que en JSON o YAML sería difícil o imposible expresar con la misma claridad. - - -¿Ralentiza la aplicación el análisis de los archivos NEON? ----------------------------------------------------------- - -Aunque los archivos NEON se analizan muy rápido, su velocidad de análisis es en gran medida irrelevante en producción. Y es que los archivos de configuración se analizan una sola vez, la primera vez que la aplicación se ejecuta (o cuando cambian). Tras el análisis se genera el código del contenedor DI, se guarda en caché (en disco) y ese código PHP compilado se ejecuta en cada petición posterior, con lo que no hace falta ningún análisis más. - -Así funciona en un entorno de producción. Durante el desarrollo, los archivos NEON se analizan cada vez que cambia su contenido, lo que garantiza que el desarrollador tenga siempre un contenedor DI actualizado. Como ya se ha dicho, el análisis en sí es muy rápido. - - -¿Cómo accedo desde mi clase a los parámetros del archivo de configuración? --------------------------------------------------------------------------- - -Tenga presente la [Rule #1: Let It Be Passed to You |introduction#Regla n.º 1: deje que se lo pasen]. Si una clase necesita información del archivo de configuración, no intente averiguar cómo puede la clase *conseguirla*. Simplemente pídala, por ejemplo mediante el constructor de la clase. Y después proporcione ese valor en el archivo de configuración. - -En este ejemplo, `%myParameter%` es un marcador del valor del parámetro `myParameter`, que se pasará al constructor de `MyClass`: - -```neon -# config.neon -parameters: - myParameter: Some value - -services: - - MyClass(%myParameter%) -``` - -Si quiere pasar varios parámetros o usar autowiring, conviene [envolver los parámetros en un objeto |best-practices:passing-settings-to-presenters]. - - -¿Soporta Nette la interfaz Container de PSR-11? ------------------------------------------------ - -El [contenedor DI de Nette |api:Nette\DI\Container] no soporta PSR-11 directamente. Pero si necesita interoperabilidad entre el contenedor DI de Nette y bibliotecas o frameworks que esperan la interfaz Container de PSR-11, puede crear un [adaptador sencillo |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f] que sirva de puente entre el contenedor DI de Nette y PSR-11. - - -¿Qué significan los términos container, compiler, definition, etc.? -------------------------------------------------------------------- - -Un breve vocabulario de las palabras que aparecen una y otra vez alrededor de Nette DI, la mayoría al [escribir extensiones |extensions]: - -- **Container** (contenedor): el objeto compilado (`Nette\DI\Container`) que crea los servicios cuando se piden y los mantiene en tiempo de ejecución. Se genera una vez como código PHP optimizado. -- **Compiler** (compilador): la maquinaria que convierte los archivos de configuración y las extensiones en esa clase del contenedor. -- **ContainerBuilder**: el modelo mutable del contenedor usado durante la compilación; contiene las definiciones de los servicios antes de que exista ningún servicio real. Véase [Creación de extensiones |extensions#ContainerBuilder]. -- **Service** (servicio): un objeto gestionado por el contenedor, normalmente creado una vez y compartido (un singleton): una conexión a la base de datos, un mailer, un logger. -- **Definition** (definición): la receta de un servicio: su tipo, cómo crearlo y qué hacer después. Nette convierte las definiciones en los métodos factory del contenedor; existen varios tipos (véase [tipos de definición |extensions#Tipos de definición]). -- **Type** (tipo): la clase o interfaz de un servicio, que el autowiring usa para casar los servicios con los lugares que los requieren. -- **Autowiring**: pasar automáticamente los servicios a los constructores y métodos según su tipo, para que no tenga que conectar las dependencias a mano. -- **Tag** (etiqueta): una marca adjunta a una definición (opcionalmente con un valor); una extensión puede después encontrar con `findByTag()` todos los servicios que la llevan. -- **Setup**: llamadas adicionales que se realizan sobre un servicio justo después de crearlo: llamadas a métodos o asignaciones a propiedades, añadidas con `addSetup()`. -- **Alias**: un nombre alternativo para un servicio existente. diff --git a/dependency-injection/es/global-state.texy b/dependency-injection/es/global-state.texy deleted file mode 100644 index df0d5d6e03..0000000000 --- a/dependency-injection/es/global-state.texy +++ /dev/null @@ -1,294 +0,0 @@ -Estado global y singletons -************************** - -.[perex] -Advertencia: las siguientes construcciones son síntomas de un código mal diseñado: - -- `Foo::getInstance()` -- `DB::insert(...)` -- `Article::setDb($db)` -- `ClassName::$var` o `static::$var` - -¿Aparece alguna de estas construcciones en su código? Si es así, tiene una oportunidad de mejora. Quizá piense que son construcciones habituales, que quizá ha visto en soluciones de ejemplo de distintas bibliotecas y frameworks. Si es así, el diseño de su código es defectuoso. - -No estamos hablando aquí de una pureza académica. Todas estas construcciones comparten un rasgo común: usan estado global. Y el estado global tiene un efecto pernicioso sobre la calidad del código. Las clases pasan a mentir sobre sus dependencias. El código se vuelve impredecible. Confunde a los desarrolladores y reduce su eficiencia. - -En este capítulo explicaremos por qué es así y cómo evitar el estado global. - - -Interconexión global --------------------- - -En un mundo ideal, un objeto solo debería comunicarse con los objetos que se le [pasaron directamente |passing-dependencies]. Si creo dos objetos `A` y `B` y nunca les paso una referencia el uno del otro, ni `A` ni `B` pueden acceder al estado del otro ni modificarlo. Es una propiedad muy deseable del código. Es como tener una pila y una bombilla: la bombilla no se encenderá hasta que la conecte a la pila con un cable. - -Eso, sin embargo, no vale para las variables globales (estáticas) ni para los singletons. El objeto `A` podría acceder *de forma inalámbrica* al objeto `C` y modificarlo sin que se le pase ninguna referencia, llamando a `C::changeSomething()`. Y si el objeto `B` se conecta también al `C` global, `A` y `B` pueden influirse mutuamente a través de `C`. - -Usar variables globales introduce una nueva forma de acoplamiento *inalámbrico*, invisible desde fuera. Crea una cortina de humo que hace el código más difícil de entender y de usar. Para captar de verdad las dependencias, los desarrolladores tienen que leer cada línea del código fuente en lugar de basarse en las interfaces de las clases. Y además ese acoplamiento es del todo innecesario. El estado global se usa porque es fácilmente accesible desde cualquier sitio y permite, por ejemplo, escribir en la base de datos mediante un método global (estático) `DB::insert()`. Pero, como demostraremos, la comodidad aparente es mínima frente a las graves complicaciones que introduce. - -.[note] -En cuanto al comportamiento no hay diferencia entre una variable global y una estática. Son igual de dañinas. - - -La fantasmagórica acción a distancia ------------------------------------- - -"Fantasmagórica acción a distancia" es como llamó célebremente Albert Einstein a un fenómeno de la física cuántica que le producía escalofríos. -Se refiere al entrelazamiento cuántico, en el que medir una propiedad de una partícula afecta instantáneamente a otra partícula entrelazada, sin importar la distancia que las separe, aunque sea de millones de años luz, lo que en apariencia viola la ley fundamental del universo de que nada puede viajar más rápido que la luz. - -En el mundo del software, la "fantasmagórica acción a distancia" describe una situación en la que ejecutamos un proceso que creemos aislado (porque no se le pasó explícitamente ninguna dependencia) y, sin embargo, se producen interacciones y cambios de estado inesperados en partes lejanas del sistema, sin que lo sepamos. Esto solo puede ocurrir mediante el estado global. - -Imagine que se incorpora a un equipo de desarrollo en un proyecto con una base de código grande y madura. Su nuevo jefe le pide que implemente una nueva funcionalidad y usted, como buen desarrollador, empieza escribiendo un test. Pero, como es nuevo en el proyecto, hace un montón de pruebas exploratorias del tipo "qué pasa si llamo a este método". Y prueba a escribir el siguiente test: - -```php -function testCreditCardCharge() -{ - $cc = new CreditCard('1234567890123456', 5, 2028); // el número de su tarjeta - $cc->charge(100); -} -``` - -Ejecuta el código, quizá varias veces, y al cabo de un rato ve en el móvil notificaciones del banco: ¡se han cargado 100 $ a su tarjeta de crédito en cada ejecución! 🤦‍♂️ - -¿Cómo demonios pudo el test provocar un cargo real? Operar con una tarjeta de crédito no es sencillo. Hay que comunicarse con un servicio web de terceros, conocer su URL, autenticarse, etc. Nada de esa información está en el test. Peor aún: no sabe dónde reside esa información, lo que hace imposible simular las dependencias externas para evitar el cargo de 100 $ en cada ejecución del test. Y, como desarrollador nuevo, ¿cómo iba a saber que lo que estaba a punto de hacer le dejaría 100 $ más pobre? - -¡Eso es una fantasmagórica acción a distancia! - -Se ve obligado a rebuscar en un código fuente extenso y a consultar a compañeros veteranos para entender las interconexiones del proyecto. Esta dificultad surge porque la interfaz de la clase `CreditCard` no revela la necesaria inicialización del estado global. Ni siquiera examinar el código fuente de la clase revela a qué método de inicialización hay que llamar. En el mejor de los casos encontrará la variable global a la que se accede e intentará deducir cómo inicializarla. - -Las clases de un proyecto así son mentirosas patológicas. La clase `CreditCard` finge que basta con instanciarla y llamar a su método `charge()`. Pero en secreto se comunica con otra clase, `PaymentGateway`, que representa la pasarela de pago. Incluso la interfaz de `PaymentGateway` puede sugerir una inicialización independiente cuando en realidad quizá saque las credenciales de un archivo de configuración, etc. Los desarrolladores originales saben que `CreditCard` necesita `PaymentGateway`. Escribieron el código así. Pero para los recién llegados es un misterio absoluto que les impide aprender y contribuir con eficacia. - -¿Cómo arreglar la situación? Fácil. **Deje que la API declare las dependencias.** - -```php -function testCreditCardCharge() -{ - $gateway = new PaymentGateway(/* ... */); - $cc = new CreditCard('1234567890123456', 5, 2028); - $cc->charge($gateway, 100); -} -``` - -Fíjese en cómo las interdependencias del código se vuelven evidentes de inmediato. Como el método `charge()` declara que necesita un `PaymentGateway`, ya no tiene que adivinar ni preguntar por esa dependencia. Sabe que tiene que crear una instancia y, al hacerlo, descubrirá los parámetros de acceso necesarios. Sin ellos el código ni siquiera funcionaría. - -Y, lo más importante, ahora puede simular la pasarela de pago para que no le cobren 100 $ cada vez que ejecuta un test. - -El estado global permite a los objetos acceder en secreto a dependencias no declaradas en sus API, lo que convierte sus API en mentirosas patológicas. - -Puede que no lo hubiera pensado así antes, pero siempre que usa estado global está creando canales secretos de comunicación inalámbrica. Esa fantasmagórica acción a distancia obliga a los desarrolladores a leer cada línea de código para entender las posibles interacciones, lo que reduce la productividad y confunde a los nuevos miembros del equipo. Si usted es quien creó el código, conoce las dependencias reales; pero cualquiera que venga después no tiene ni idea. - -Evite escribir código que dependa del estado global; prefiera pasar las dependencias explícitamente. Adopte la inyección de dependencias. - - -Fragilidad del estado global ----------------------------- - -En un código que usa estado global y singletons nunca puede estar seguro de cuándo ni por quién se modificó el estado. Ese riesgo se manifiesta ya en la inicialización. El siguiente código pretende crear una conexión a la base de datos e inicializar una pasarela de pago, pero lanza excepciones una y otra vez, y depurar la causa es extremadamente tedioso: - -```php -PaymentGateway::init(); -DB::init('mysql:', 'user', 'password'); -``` - -Tiene que rastrear el código meticulosamente para descubrir que el objeto `PaymentGateway` accede de forma inalámbrica a otros objetos, algunos de los cuales necesitan una conexión a la base de datos. Por tanto, la base de datos debe inicializarse antes que `PaymentGateway`. Pero la cortina de humo del estado global se lo oculta. ¿Cuánto tiempo se ahorraría si las API de esas clases fueran honestas y declararan sus dependencias? - -```php -$db = new DB('mysql:', 'user', 'password'); -$gateway = new PaymentGateway($db, /* ... */); -``` - -Un problema parecido surge al usar un acceso global a la conexión de la base de datos: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public function save(): void - { - DB::insert(/* ... */); - } -} -``` - -Al llamar al método `save()` no se sabe si se ha establecido una conexión a la base de datos ni quién es responsable de establecerla. Si necesitamos cambiar la conexión dinámicamente (p. ej. para las pruebas), quizá acabemos añadiendo métodos como `DB::reconnect(...)` o `DB::reconnectForTest()`. - -Considere un ejemplo: - -```php -$article = new Article; -// ... -DB::reconnectForTest(); -Foo::doSomething(); -$article->save(); -``` - -¿Cómo podemos estar seguros de que al llamar a `$article->save()` se usa realmente la base de datos de pruebas? ¿Y si el método `Foo::doSomething()` cambió la conexión global a la base de datos? Para averiguarlo tendríamos que inspeccionar el código fuente de `Foo` y quizá de muchas otras clases. Y esa investigación solo daría una respuesta temporal, porque la situación podría cambiar más adelante. - -¿Y si movemos la conexión a la base de datos a una variable estática dentro de la clase `Article`? - -```php -class Article -{ - private static DB $db; - - public static function setDb(DB $db): void - { - self::$db = $db; - } - - public function save(): void - { - self::$db->insert(/* ... */); - } -} -``` - -Eso no cambia nada en absoluto. El problema es el estado global en sí, sin importar dentro de qué clase se esconda. En este escenario, igual que en el anterior, al llamar a `$article->save()` no tenemos ninguna certeza sobre en qué base de datos se escribirán los datos. Cualquiera, en cualquier lugar de la aplicación, pudo cambiar la base de datos en cualquier momento con `Article::setDb()`. Sin que lo supiéramos. - -El estado global hace nuestra aplicación **extremadamente frágil**. - -Pero hay una forma sencilla de tratar este problema. Basta con que la API declare las dependencias necesarias para funcionar correctamente. - -```php -class Article -{ - public function __construct( - private DB $db, - ) { - } - - public function save(): void - { - $this->db->insert(/* ... */); - } -} - -$article = new Article($db); -// ... -Foo::doSomething(); -$article->save(); -``` - -Este enfoque elimina la preocupación por cambios ocultos o inesperados en la conexión a la base de datos. Ahora tenemos la certeza de dónde se guarda el artículo, y las modificaciones en clases no relacionadas ya no pueden afectarlo. El código deja de ser frágil y pasa a ser estable. - -Evite escribir código que dependa del estado global; prefiera pasar las dependencias explícitamente. Adopte la inyección de dependencias. - - -Singleton ---------- - -El singleton es un patrón de diseño que, según la [definición |https://es.wikipedia.org/wiki/Singleton] de la famosa publicación de la Banda de los Cuatro, limita una clase a una única instancia y ofrece acceso global a ella. La implementación de este patrón se parece normalmente al siguiente código: - -```php -class Singleton -{ - private static self $instance; - - public static function getInstance(): self - { - self::$instance ??= new self; - return self::$instance; - } - - // y otros métodos que realizan las funciones de la clase -} -``` - -Por desgracia, el singleton introduce estado global en la aplicación. Y, como hemos mostrado arriba, el estado global es indeseable. Por eso el singleton se considera un antipatrón. - -No use singletons en su código y sustitúyalos por otros mecanismos. Realmente no necesita singletons. Ahora bien, si necesita asegurarse de que en toda la aplicación exista una única instancia de una clase, delegue esa responsabilidad en el [contenedor DI |container]. Así se crea un singleton con el alcance de la aplicación, al que normalmente se llama servicio. La clase misma queda entonces liberada de gestionar su unicidad (es decir, no tendrá un método `getInstance()` ni una propiedad estática con la instancia) y puede centrarse solo en sus responsabilidades. Así dejará de violar el principio de responsabilidad única. - - -El estado global frente a los tests ------------------------------------ - -Al escribir tests, lo ideal es suponer que cada test es una unidad aislada, sin que entre ni salga de ella ningún estado externo. Cuando un test termina, todo el estado asociado a él debería limpiarlo automáticamente el recolector de basura. Eso hace que los tests estén aislados. Por eso podemos ejecutarlos en cualquier orden. - -Pero cuando hay estado global o singletons, esas suposiciones tan útiles se desmoronan. El estado puede filtrarse hacia dentro y hacia fuera de los tests. De pronto, el orden de los tests puede importar. - -Para poder siquiera testear código con singletons, los desarrolladores tienen a menudo que comprometer su integridad, por ejemplo permitiendo sustituir la instancia del singleton. Esas soluciones son, en el mejor de los casos, apaños que llevan a un código difícil de mantener y de entender. Cualquier test (o su método `tearDown()`) que modifique el estado global debe deshacer meticulosamente esos cambios. - -¡El estado global es el mayor quebradero de cabeza de las pruebas unitarias! - -¿Cómo arreglarlo? Sencillo. Evite escribir código que use singletons; prefiera pasar las dependencias explícitamente. Adopte la inyección de dependencias. - - -Constantes globales -------------------- - -El estado global no se limita al uso de singletons y variables estáticas: también puede afectar a las constantes globales. - -Las constantes cuyos valores representan verdades universales (`M_PI`) o aportan información autocontenida (`PREG_BACKTRACK_LIMIT_ERROR`) son en general aceptables. En cambio, las constantes usadas como forma de inyectar información en el código *de forma inalámbrica* son en la práctica dependencias ocultas. Como `LOG_FILE` en el siguiente ejemplo. El uso de la constante `FILE_APPEND` es perfectamente correcto. - -```php -const LOG_FILE = '...'; - -class Foo -{ - public function doSomething() - { - // ... - file_put_contents(LOG_FILE, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -En su lugar deberíamos declarar la ruta del archivo de registro como parámetro del constructor de la clase `Foo`, convirtiéndola en una parte explícita de su API: - -```php -class Foo -{ - public function __construct( - private string $logFile, - ) { - } - - public function doSomething() - { - // ... - file_put_contents($this->logFile, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Ahora pasamos explícitamente la ruta al archivo de registro. Podemos cambiarla con facilidad cuando haga falta, lo que simplifica las pruebas y el mantenimiento del código. - - -Funciones globales y métodos estáticos --------------------------------------- - -Queremos subrayar que usar métodos estáticos y funciones globales no es de por sí problemático. Hemos explicado los problemas de métodos como `DB::insert()`, pero el problema de fondo era siempre el estado global subyacente, guardado normalmente en una variable estática. El método `DB::insert()` depende de una variable estática que contiene la conexión a la base de datos. Sin esa variable sería imposible implementar el método. - -Usar métodos y funciones estáticos deterministas como `Closure::fromCallable()`, `strlen()` y muchos otros es perfectamente compatible con la inyección de dependencias. Esas funciones son predecibles porque devuelven siempre el mismo resultado para los mismos parámetros de entrada. No usan ningún estado global. - -Hay, sin embargo, funciones en PHP que no son deterministas. Entre ellas está, por ejemplo, la función `htmlspecialchars()`. Su tercer parámetro, `$encoding`, si se omite, toma por defecto el valor de la opción de configuración `default_charset` (`ini_get('default_charset')`). Por eso se recomienda indicar siempre este parámetro para evitar un posible comportamiento impredecible. Nette lo hace de forma sistemática. - -Algunas funciones, como `strtolower()` y `strtoupper()`, mostraban en el pasado reciente un comportamiento no determinista que dependía de la configuración del locale (`setlocale()`). Eso causó muchas complicaciones, sobre todo al trabajar con el turco. El motivo es que el turco distingue entre la "I" con punto y sin punto tanto en minúscula como en mayúscula. En consecuencia, `strtolower('I')` devolvía `ı` (i minúscula sin punto) y `strtoupper('i')` devolvía `İ` (I mayúscula con punto), lo que provocaba numerosos errores misteriosos en las aplicaciones. Este problema, sin embargo, se corrigió en la versión 8.2 de PHP y las funciones ya no dependen del locale. - -Es un buen ejemplo de cómo el estado global (la configuración del locale) trajo de cabeza a miles de desarrolladores de todo el mundo. La solución definitiva consistió en hacer las funciones independientes del locale, es decir, en eliminar la dependencia oculta. - - -¿Cuándo es posible usar estado global? --------------------------------------- - -Hay situaciones concretas y limitadas en las que usar estado global puede ser aceptable. Por ejemplo, durante la depuración, cuando necesita volcar el valor de una variable o medir el tiempo de ejecución de un fragmento concreto de código. En esos casos, que implican acciones temporales que después se eliminarán del código, usar un dumper o un cronómetro accesible globalmente puede ser legítimo. Esas herramientas no forman parte del diseño esencial de la aplicación. - -Otro ejemplo son las funciones de expresiones regulares de PHP (`preg_*`), que internamente guardan en memoria estática una caché de las expresiones regulares compiladas. Cuando llama varias veces a esas funciones con la misma expresión regular a lo largo de su código, la expresión se compila una sola vez. Esa caché mejora el rendimiento y es completamente invisible para el usuario, lo que hace que ese uso de estado estático interno sea en general aceptable. - - -Resumen -------- - -Hemos hablado de por qué tiene sentido: - -1) eliminar de su código todas las propiedades estáticas mutables (el estado global) -2) declarar explícitamente las dependencias -3) y usar la inyección de dependencias - -Al diseñar su código, recuerde que cada `static $foo` mutable es una fuente potencial de problemas. Para crear un entorno favorable a la DI es crucial eliminar por completo el estado global y sustituirlo por la inyección de dependencias. - -Durante ese proceso quizá descubra la necesidad de dividir clases que tienen varias responsabilidades. No lo dude; aspire al principio de responsabilidad única. - -*Quiero dar las gracias a Miško Hevery, cuyos artículos, como [Flaw: Brittle Global State & Singletons |https://web.archive.org/web/20230321084133/http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/], son la base de este capítulo.* diff --git a/dependency-injection/es/introduction.texy b/dependency-injection/es/introduction.texy deleted file mode 100644 index e74833dd90..0000000000 --- a/dependency-injection/es/introduction.texy +++ /dev/null @@ -1,525 +0,0 @@ -¿Qué es la inyección de dependencias? -************************************* - -.[perex] -Este capítulo presenta las prácticas básicas de programación que debería seguir al escribir cualquier aplicación. Son los fundamentos necesarios para escribir código limpio, comprensible y mantenible. - -Si adopta y sigue estas reglas, Nette le apoyará a cada paso. Se ocupará por usted de las tareas rutinarias y le dará la máxima comodidad para que pueda concentrarse en la lógica misma. - -Los principios que mostraremos aquí son bastante sencillos. No hay nada que temer. - - -¿Recuerda su primer programa? ------------------------------ - -No sabemos en qué lenguaje lo escribió, pero si fue en PHP, probablemente tenía este aspecto: - -```php -function addition(float $a, float $b): float -{ - return $a + $b; -} - -echo addition(23, 1); // imprime 24 -``` - -Unas pocas líneas de código triviales y, sin embargo, esconden un montón de conceptos clave. Que existen las variables. Que el código se divide en unidades más pequeñas, como las funciones. Que les pasamos argumentos de entrada y devuelven resultados. Solo faltan las condiciones y los bucles. - -Que a una función le pasemos datos de entrada y nos devuelva un resultado es un concepto perfectamente comprensible, usado también en otros campos, como las matemáticas. - -Una función tiene su firma, formada por su nombre, la lista de parámetros y sus tipos y, por último, el tipo del valor devuelto. Como usuarios nos interesa la firma; de la implementación interna no solemos necesitar saber nada. - -Ahora imagine que la firma de la función tuviera este aspecto: - -```php -function addition(float $x): float -``` - -¿Una suma con un solo parámetro? Qué raro… ¿Y qué tal esta? - -```php -function addition(): float -``` - -Ahora sí que es raro, ¿verdad? ¿Cómo se usa esta función? - -```php -echo addition(); // ¿qué imprimirá? -``` - -Al ver un código así nos quedaríamos desconcertados. No solo no lo entendería un principiante: tampoco lo entendería un programador experto. - -¿Se pregunta qué aspecto tendría por dentro una función así? ¿De dónde sacaría los números que hay que sumar? Probablemente se los procuraría *de alguna manera* ella misma, quizá así: - -```php -function addition(): float -{ - $a = Input::get('a'); - $b = Input::get('b'); - return $a + $b; -} -``` - -En el cuerpo de la función hemos descubierto dependencias ocultas de otras funciones globales o métodos estáticos. Para averiguar de dónde salen realmente los números hay que seguir investigando. - - -¡Por ahí no! ------------- - -El diseño que acabamos de mostrar es la esencia de muchas propiedades negativas: - -- La firma de la función fingía que no necesitaba los números que sumar, lo que nos desconcertó. -- No tenemos ni idea de cómo hacer que la función sume dos números distintos. -- Hemos tenido que mirar el código para averiguar de dónde saca los números. -- Hemos descubierto dependencias ocultas. -- Para entenderlo del todo hay que examinar también esas dependencias. - -¿Y es acaso tarea de la función de suma obtener las entradas? Por supuesto que no. Su responsabilidad es solo la suma misma. - - -No queremos toparnos con un código así, y desde luego no queremos escribirlo. La solución es sencilla: volver a lo básico y usar simplemente parámetros: - - -```php -function addition(float $a, float $b): float -{ - return $a + $b; -} -``` - - -Regla n.º 1: deje que se lo pasen ---------------------------------- - -La regla más importante es: **todos los datos que las funciones o las clases necesiten deben serles proporcionados**. - -En lugar de inventar formas ocultas de que ellas mismas obtengan los datos, pase simplemente los parámetros. Ahorrará el tiempo dedicado a inventar caminos ocultos que desde luego no mejorarán su código. - -Si sigue esta regla siempre y en todas partes, va camino de un código sin dependencias ocultas. Camino de un código comprensible no solo para su autor, sino también para cualquiera que lo lea después. Donde todo se entiende a partir de las firmas de las funciones y las clases, y no hace falta buscar detalles ocultos en la implementación. - -A esta técnica se la llama profesionalmente **inyección de dependencias**. Y a los datos se los llama **dependencias**. Es simplemente pasar parámetros, nada más. - -.[note] -No confunda, por favor, la inyección de dependencias, que es un patrón de diseño, con un "contenedor de inyección de dependencias", que es una herramienta, algo esencialmente distinto. De los contenedores hablaremos más adelante. - - -De las funciones a las clases ------------------------------ - -¿Y cómo se aplica esto a las clases? Una clase es una entidad más compleja que una simple función, pero la regla n.º 1 vale aquí igualmente. Solo que hay [más maneras de pasar los argumentos |passing-dependencies]. Por ejemplo, de forma bastante parecida al caso de la función: - -```php -class Math -{ - public function sum(float $a, float $b): float - { - return $a + $b; - } -} - -$math = new Math; -echo $math->sum(23, 1); // 24 -``` - -O mediante otros métodos, o directamente el constructor: - -```php -class Sum -{ - public function __construct( - private float $a, - private float $b, - ) { - } - - public function calculate(): float - { - return $this->a + $this->b; - } -} - -$sum = new Sum(23, 1); -echo $sum->calculate(); // 24 -``` - -Ambos ejemplos cumplen plenamente con la inyección de dependencias. - - -Ejemplos de la vida real ------------------------- - -En el mundo real no escribirá clases para sumar números. Pasemos a ejemplos prácticos. - -Tengamos una clase `Article` que representa un artículo de un blog: - -```php -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - // guarda el artículo en la base de datos - } -} -``` - -y su uso será el siguiente: - -```php -$article = new Article; -$article->title = '10 Things You Need to Know About Losing Weight'; -$article->content = 'Every year millions of people in ...'; -$article->save(); -``` - -El método `save()` guardará el artículo en una tabla de la base de datos. Implementarlo con [Nette Database |database:] sería sencillo, si no fuera por una pega: ¿de dónde saca `Article` la conexión a la base de datos, es decir, un objeto de la clase `Nette\Database\Connection`? - -Parece que tenemos muchas opciones. Podría tomarlo de una variable estática. O heredando de una clase que proporcione la conexión a la base de datos. O usar un [singleton |global-state#Singleton]. O las llamadas fachadas, como se usan en Laravel: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - DB::insert( - 'INSERT INTO articles (title, content) VALUES (?, ?)', - [$this->title, $this->content], - ); - } -} -``` - -Genial, hemos resuelto el problema. - -¿O no? - -Recordemos la [#Regla n.º 1: deje que se lo pasen]: todas las dependencias que la clase necesita deben serle pasadas. Porque si rompemos la regla, hemos tomado el camino hacia un código sucio, lleno de dependencias ocultas y falta de claridad, y el resultado será una aplicación que costará un mundo mantener y desarrollar. - -El usuario de la clase `Article` no tiene ni idea de dónde guarda el artículo el método `save()`. ¿En una tabla de la base de datos? ¿En cuál, en la de producción o en la de pruebas? ¿Y cómo se puede cambiar? - -El usuario tiene que mirar cómo está implementado el método `save()` y encuentra el uso del método `DB::insert()`. Así que tiene que seguir investigando cómo obtiene ese método la conexión a la base de datos. Y las dependencias ocultas pueden formar una cadena bastante larga. - -En un código limpio y bien diseñado nunca hay dependencias ocultas, fachadas de Laravel ni variables estáticas. En un código limpio y bien diseñado se pasan argumentos: - -```php -class Article -{ - public function save(Nette\Database\Connection $db): void - { - $db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -Aún más práctico, como veremos más adelante, es usar el constructor: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function save(): void - { - $this->db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -.[note] -Si es usted un programador con experiencia, quizá piense que `Article` no debería tener el método `save()` en absoluto; que debería representar puramente una estructura de datos y que del guardado debería ocuparse un repositorio aparte. Tiene sentido. Pero eso nos llevaría mucho más allá del alcance del tema, que es la inyección de dependencias, y del objetivo de dar ejemplos sencillos. - -Si escribe una clase que necesita para funcionar, por ejemplo, una base de datos, no invente de dónde sacarla: haga que se la pasen. Quizá como parámetro del constructor o de otro método. Reconozca las dependencias. Reconózcalas en la API de su clase. Obtendrá un código comprensible y previsible. - -¿Y qué tal esta clase, que registra mensajes de error? - -```php -class Logger -{ - public function log(string $message): void - { - $file = LOG_DIR . '/log.txt'; - file_put_contents($file, $message . "\n", FILE_APPEND); - } -} -``` - -¿Qué opina, hemos seguido la [#Regla n.º 1: deje que se lo pasen]? - -No la hemos seguido. - -La información clave, el directorio con el archivo de registro, *se la procura la propia clase* a partir de una constante. - -Mire el ejemplo de uso: - -```php -$logger = new Logger; -$logger->log('Temperature is 23 °C'); -$logger->log('Temperature is 10 °C'); -``` - -Sin conocer la implementación, ¿sabría decir dónde se escriben los mensajes? ¿Se le habría ocurrido que para su funcionamiento hace falta que exista la constante `LOG_DIR`? ¿Y podría crear una segunda instancia que escribiera en otro sitio? Desde luego que no. - -Arreglemos la clase: - -```php -class Logger -{ - public function __construct( - private string $file, - ) { - } - - public function log(string $message): void - { - file_put_contents($this->file, $message . "\n", FILE_APPEND); - } -} -``` - -La clase es ahora mucho más comprensible, configurable y, por tanto, más útil. - -```php -$logger = new Logger('/path/to/log.txt'); -$logger->log('Temperature is 15 °C'); -``` - - -¡Pero a mí eso me da igual! ---------------------------- - -*"Cuando creo un objeto Article y llamo a save(), no quiero ocuparme de la base de datos; solo quiero que se guarde en la que tengo configurada."* - -*"Cuando uso Logger, solo quiero que se escriba el mensaje y no quiero ocuparme de dónde. Que se use la configuración global."* - -Son observaciones válidas. - -Como ejemplo, mostremos una clase que distribuye boletines y registra el resultado: - -```php -class NewsletterDistributor -{ - public function distribute(): void - { - $logger = new Logger(/* ... */); - try { - $this->sendEmails(); - $logger->log('Emails have been sent out'); - - } catch (Exception $e) { - $logger->log('An error occurred during sending'); - throw $e; - } - } -} -``` - -El `Logger` mejorado, que ya no usa la constante `LOG_DIR`, requiere la ruta al archivo en el constructor. ¿Cómo se resuelve esto? A la clase `NewsletterDistributor` no le interesa dónde se escriben los mensajes; simplemente quiere registrarlos. - -La solución es de nuevo la [#Regla n.º 1: deje que se lo pasen]: le pasamos todos los datos que la clase necesita. - -¿Significa eso que pasamos la ruta del registro por el constructor y la usamos después al crear el objeto `Logger`? - -```php -class NewsletterDistributor -{ - public function __construct( - private string $file, // ⛔ ¡ASÍ NO! - ) { - } - - public function distribute(): void - { - $logger = new Logger($this->file); -``` - -¡Así no! Porque la ruta **no** es un dato que necesite la clase `NewsletterDistributor`; lo necesita el `Logger`. ¿Percibe la diferencia? La clase `NewsletterDistributor` necesita el logger en sí. Así que le pasaremos el logger: - -```php -class NewsletterDistributor -{ - public function __construct( - private Logger $logger, // ✅ - ) { - } - - public function distribute(): void - { - try { - $this->sendEmails(); - $this->logger->log('Emails have been sent out'); - - } catch (Exception $e) { - $this->logger->log('An error occurred during sending'); - throw $e; - } - } -} -``` - -Ahora queda claro por la firma de la clase `NewsletterDistributor` que el registro forma parte de su funcionamiento. Y la tarea de sustituir el logger por otro, quizá para las pruebas, es del todo directa. Además, si cambiara el constructor de la clase `Logger`, no tendría ningún impacto en nuestra clase. - - -Regla n.º 2: tome lo que es suyo --------------------------------- - -No se líe y no acepte las dependencias de sus dependencias. Acepte solo las suyas propias. - -Gracias a eso, el código que usa otros objetos será completamente independiente de los cambios en sus constructores. Su API será más precisa. Y, sobre todo, será directo sustituir esas dependencias por otras. - - -Un nuevo miembro de la familia ------------------------------- - -El equipo de desarrollo ha decidido crear un segundo logger, uno que escriba en la base de datos. Así que creamos la clase `DatabaseLogger`. Ahora tenemos dos clases, `Logger` y `DatabaseLogger`; una escribe en un archivo, la otra en la base de datos... ¿no le parecen los nombres un poco raros? ¿No sería mejor renombrar `Logger` a `FileLogger`? Desde luego. - -Pero hagámoslo con cabeza. Creamos una interfaz con el nombre original: - -```php -interface Logger -{ - function log(string $message): void; -} -``` - -… que ambos loggers implementarán: - -```php -class FileLogger implements Logger -// ... - -class DatabaseLogger implements Logger -// ... -``` - -Y gracias a eso no hará falta modificar nada en el resto del código donde se usa el logger. Por ejemplo, el constructor de la clase `NewsletterDistributor` seguirá contentándose con exigir `Logger` como parámetro. Y de nosotros dependerá qué instancia le proporcionamos. - -**Por eso nunca añadimos el sufijo `Interface` ni el prefijo `I` a los nombres de las interfaces.** De lo contrario no sería posible ampliar el código de forma tan elegante. - - -Houston, tenemos un problema ----------------------------- - -Mientras que en toda la aplicación nos las apañamos con una única instancia del logger, sea de archivo o de base de datos, y basta con pasarla allí donde se registra algo, la situación es bien distinta con la clase `Article`. Sus instancias las creamos según haga falta, incluso muchas veces. ¿Cómo gestionamos la dependencia de la base de datos en su constructor? - -Un ejemplo podría ser un controlador que debe guardar un artículo en la base de datos tras enviar un formulario: - -```php -class EditController extends Controller -{ - public function formSubmitted($data) - { - $article = new Article(/* ... */); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Una posible solución parece obvia: pasemos el objeto de la base de datos por el constructor a `EditController` y usemos `$article = new Article($this->db)`. - -Igual que en el caso anterior con `Logger` y la ruta al archivo, este no es el enfoque correcto. La base de datos no es una dependencia de `EditController`, sino de `Article`. Pasar la base de datos infringe, por tanto, la [Rule #2: Take What's Yours |#Regla n.º 2: tome lo que es suyo]. Si cambia el constructor de la clase `Article` (se añade un nuevo parámetro), tendrá que modificar el código en todos los lugares donde se crean instancias. Uf. - -Houston, ¿qué propone? - - -Regla n.º 3: déjelo en manos de la factory ------------------------------------------- - -Al eliminar las dependencias ocultas y pasar todas las dependencias como argumentos, hemos ganado clases más configurables y flexibles. Por eso necesitamos algo más que cree y configure por nosotros esas clases más flexibles. Lo llamaremos factories. - -La regla es: si una clase tiene dependencias, delegue la creación de sus instancias en una factory. - -Las factories son una alternativa más inteligente al operador `new` en el mundo de la inyección de dependencias. - -.[note] -No lo confunda, por favor, con el patrón de diseño *factory method*, que describe una forma concreta de usar las factories y no está relacionado con este tema. - - -Factory -------- - -Una factory es un método o una clase que crea y configura objetos. A la clase que produce `Article` la llamaremos `ArticleFactory` y podría tener este aspecto: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Su uso en el controlador será el siguiente: - -```php -class EditController extends Controller -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function formSubmitted($data) - { - // dejamos que la factory cree el objeto - $article = $this->articleFactory->create(); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Llegados a este punto, si cambia la firma del constructor de la clase `Article`, la única parte del código que tiene que reaccionar es la propia `ArticleFactory`. Todo el demás código que trabaja con objetos `Article`, como `EditController`, quedará intacto. - -Puede que ahora se esté rascando la cabeza preguntándose si de verdad hemos mejorado la situación. La cantidad de código ha crecido y todo el asunto empieza a parecer sospechosamente complejo. - -Tranquilo, enseguida llegaremos al contenedor DI de Nette. Y tiene varios ases en la manga que simplificarán enormemente la construcción de aplicaciones con inyección de dependencias. Por ejemplo, en lugar de la clase `ArticleFactory` bastará con [escribir solo una interfaz |factory]: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Pero nos estamos adelantando, siga atento :-) - - -Resumen -------- - -Al principio de este capítulo prometimos mostrar un procedimiento para diseñar código limpio. Basta con asegurarse de que las clases: - -1) [reciban las dependencias que necesitan |#Regla n.º 1: deje que se lo pasen] -2) [y, por el contrario, no reciban lo que no necesitan directamente |#Regla n.º 2: tome lo que es suyo] -3) [y que los objetos con dependencias se creen mejor en factories |#Regla n.º 3: déjelo en manos de la factory] - -A primera vista puede no parecerlo, pero estas tres reglas tienen consecuencias de largo alcance. Llevan a una perspectiva radicalmente distinta sobre el diseño del código. ¿Merece la pena? Los programadores que han abandonado los viejos hábitos y han empezado a usar la inyección de dependencias de forma consecuente consideran ese paso un momento decisivo en su carrera profesional. Les abrió un mundo de aplicaciones claras y mantenibles. - -¿Y qué pasa si el código no usa la inyección de dependencias de forma consecuente? ¿Si está construido sobre métodos estáticos o singletons? ¿Trae eso problemas? [Sí, y muy grandes |global-state]. diff --git a/dependency-injection/es/nette-container.texy b/dependency-injection/es/nette-container.texy deleted file mode 100644 index 51843f43d1..0000000000 --- a/dependency-injection/es/nette-container.texy +++ /dev/null @@ -1,117 +0,0 @@ -Contenedor DI de Nette -********************** - -.[perex] -Nette DI es una de las bibliotecas más interesantes de Nette. Sabe generar y actualizar automáticamente contenedores DI compilados que son extremadamente rápidos y muy fáciles de configurar. - -La forma de los servicios que el contenedor DI debe crear se define normalmente con archivos de configuración en [formato NEON|neon:format]. El contenedor que creamos a mano en el [capítulo anterior|container] se escribiría así: - -```neon -parameters: - db: - dsn: 'mysql:' - user: root - password: '***' - -services: - - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - - ArticleFactory - - EditController -``` - -La sintaxis es muy concisa. - -Todas las dependencias declaradas en los constructores de las clases `ArticleFactory` y `EditController` las descubre y las pasa automáticamente Nette DI gracias al llamado [autowiring|autowiring], así que no hace falta indicar nada en el archivo de configuración. Por tanto, aunque cambien los parámetros, no tiene que cambiar nada en la configuración. Durante el desarrollo, Nette regenera el contenedor automáticamente. Puede concentrarse puramente en desarrollar la aplicación. - -Si queremos pasar las dependencias mediante setters, usamos para ello la sección [setup |services#Setup]. - -Nette DI genera directamente el código PHP del contenedor. El resultado es, por tanto, un archivo `.php` que puede abrir y examinar. Eso le permite ver exactamente cómo funciona el contenedor. También puede depurarlo en su IDE y recorrer su ejecución paso a paso. Y, sobre todo: el código PHP generado es extremadamente rápido. - -Nette DI también puede generar el código de una [factory|factory] a partir de una interfaz dada. Por eso, en lugar de la clase `ArticleFactory`, en la aplicación solo tenemos que crear una interfaz: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Encontrará el ejemplo completo [en GitHub|https://github.com/nette-examples/di-example-doc]. - - -Uso independiente ------------------ - -Integrar la biblioteca Nette DI en una aplicación es muy fácil. Primero la instalamos con Composer (porque descargar archivos zip está muy pasado de moda): - -```shell -composer require nette/di -``` - -El siguiente código usa el [Compiler |api:Nette\DI\Compiler] para crear una instancia del contenedor DI según la configuración guardada en el archivo `config.neon`: - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp'); -$class = $loader->load(function ($compiler) { - $compiler->loadConfig(__DIR__ . '/config.neon'); -}); -$container = new $class; -``` - -El contenedor se genera una sola vez, su código se escribe en la caché (el directorio `__DIR__ . '/temp'`) y en las peticiones siguientes solo se carga desde ahí. - -Por sí solo, el `Compiler` habilita en la configuración únicamente las secciones `services` y `parameters`. Para usar las demás (como `search`, `decorator`, `di` o `inject`) hay que registrar antes sus extensiones. Y para poder registrar extensiones desde la sección `extensions` de la configuración, añada la `ExtensionsExtension`: - -```php -$compiler->addExtension('search', new Nette\DI\Extensions\SearchExtension($tempDir)); -$compiler->addExtension('extensions', new Nette\DI\Extensions\ExtensionsExtension); -``` - -El [Configurator |application:bootstrapping] que se usa en las aplicaciones Nette completas las registra todas automáticamente. - -Si guarda varios contenedores distintos en el mismo directorio de caché, distíngalos con una clave pasada como segundo argumento a `load()`; pasa a formar parte del nombre de la clase generada: - -```php -$class = $loader->load( - fn($compiler) => $compiler->loadConfig(__DIR__ . '/config.neon'), - 'my-key', -); -``` - -Para crear y obtener los servicios se usan los métodos `getService()` o `getByType()`. Así creamos el objeto `EditController`: - -```php -$controller = $container->getByType(EditController::class); -$controller->someMethod(); -``` - -Durante el desarrollo conviene activar el modo de refresco automático, en el que el contenedor se regenera solo si se modifica alguna clase o algún archivo de configuración. Basta con pasar `true` como segundo argumento en el constructor de [ContainerLoader |api:Nette\DI\ContainerLoader]. - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp', true); -``` - - -Trabajar con el contenedor --------------------------- - -Además de `getService()` y `getByType()`, el objeto del contenedor ofrece otros métodos útiles: - -- `getByType(string $type, bool $throw = true): ?object` devuelve el servicio del tipo dado. Si pasa `false` como segundo argumento, devuelve `null` en lugar de lanzar una excepción cuando no existe tal servicio. -- `hasService(string $name): bool` e `isCreated(string $name): bool` le dicen si un servicio está definido y si ya se ha creado su instancia. -- `getParameters(): array` devuelve todos los parámetros del contenedor, `getParameter($key)` devuelve uno solo. -- `createInstance(string $class, array $args = []): object` crea una nueva instancia de la clase dada y le pasa las dependencias del constructor mediante autowiring. -- `callMethod(callable $function, array $args = []): mixed` llama al callable dado y le pasa sus argumentos mediante autowiring. -- `callInjects(object $service): void` llama a todos los métodos `inject*()` del objeto dado y les pasa las dependencias. - -El constructor del contenedor acepta además un array de parámetros que complementan los definidos en la configuración: - -```php -$container = new $class(['host' => 'localhost']); -``` - - -Uso con Nette Framework ------------------------ - -Como hemos mostrado, el uso de Nette DI no se limita a las aplicaciones construidas con Nette Framework; puede integrarlo en cualquier sitio con solo tres líneas de código. Ahora bien, si desarrolla aplicaciones con Nette Framework, de la configuración y la creación del contenedor se encarga [Bootstrap |application:bootstrapping#Configuración del contenedor DI]. diff --git a/dependency-injection/es/passing-dependencies.texy b/dependency-injection/es/passing-dependencies.texy deleted file mode 100644 index 2b969a2217..0000000000 --- a/dependency-injection/es/passing-dependencies.texy +++ /dev/null @@ -1,215 +0,0 @@ -Paso de dependencias -******************** - -<div class=perex> - -Los argumentos, o "dependencias" en la terminología de la DI, se pueden pasar a las clases de las siguientes maneras principales: - -* inyección por constructor -* inyección por método (la llamada inyección por setter) -* inyección en propiedades -* mediante el método `inject*()` o el atributo `#[Inject]` - -</div> - -Mostremos cada variante con ejemplos concretos. - - -Inyección por constructor -========================= - -Las dependencias se proporcionan como argumentos del constructor en el momento de crear el objeto: - -```php -class MyClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -$obj = new MyClass($cache); -``` - -Este enfoque es adecuado para las dependencias obligatorias que la clase necesita imprescindiblemente para funcionar, porque sin ellas no se puede crear la instancia. - -Desde PHP 8.0 podemos usar una notación más corta ([promoción de propiedades del constructor |https://blog.nette.org/es/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), que es funcionalmente equivalente: - -```php -// PHP 8.0 -class MyClass -{ - public function __construct( - private Cache $cache, - ) { - } -} -``` - -Desde PHP 8.1, una propiedad se puede marcar con la bandera `readonly`, que declara que el valor de la propiedad no cambiará después de la inicialización: - -```php -// PHP 8.1 -class MyClass -{ - public function __construct( - private readonly Cache $cache, - ) { - } -} -``` - -El contenedor DI pasa las dependencias al constructor automáticamente mediante [autowiring |autowiring]. Los argumentos que no se pueden proporcionar así (p. ej. cadenas, números, booleanos) [se indican en la configuración |services#Argumentos]. - - -Infierno del constructor ------------------------- - -El término *infierno del constructor* describe una situación en la que una clase hija hereda de una clase padre cuyo constructor requiere dependencias, y la clase hija requiere además las suyas propias. Entonces tiene que aceptar y pasar también las dependencias del padre: - -```php -abstract class BaseClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass extends BaseClass -{ - private Database $db; - - // ⛔ INFIERNO DEL CONSTRUCTOR - public function __construct(Cache $cache, Database $db) - { - parent::__construct($cache); - $this->db = $db; - } -} -``` - -El problema surge cuando queremos cambiar el constructor de `BaseClass`, por ejemplo al añadir una nueva dependencia. Entonces hay que modificar también todos los constructores de las clases hijas. Lo que convierte esa modificación en un infierno. - -¿Cómo se puede evitar? La solución es **preferir la [composición a la herencia |faq#¿Por qué se prefiere la composición a la herencia?]**. - -Así que diseñamos el código de otra manera. Evitaremos las clases [abstractas |nette:introduction-to-object-oriented-programming#Clases abstractas] `Base*`. En lugar de que `MyClass` obtenga cierta funcionalidad heredando de `BaseClass`, se le pasará esa funcionalidad como dependencia: - -```php -final class SomeFunctionality -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass -{ - private SomeFunctionality $sf; - private Database $db; - - public function __construct(SomeFunctionality $sf, Database $db) // ✅ - { - $this->sf = $sf; - $this->db = $db; - } -} -``` - - -Inyección por setter -==================== - -Las dependencias se proporcionan llamando a un método que las guarda en una propiedad privada. La convención de nombres habitual para estos métodos es el patrón `set*()`, de ahí que se les llame setters, aunque naturalmente pueden llamarse de otra manera. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - $this->cache = $cache; - } -} - -$obj = new MyClass; -$obj->setCache($cache); -``` - -Este enfoque es adecuado para las dependencias opcionales, que no son imprescindibles para el funcionamiento de la clase, porque no está garantizado que el objeto reciba realmente la dependencia (es decir, que quien lo usa llame al método). - -Al mismo tiempo, este método permite llamar al setter repetidamente para cambiar la dependencia. Si eso no es deseable, añada una comprobación dentro del método o, desde PHP 8.1, marque la propiedad `$cache` con la bandera `readonly`. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - if (isset($this->cache)) { - throw new RuntimeException('The dependency has already been set'); - } - $this->cache = $cache; - } -} -``` - -La llamada al setter se define en la configuración del contenedor DI en la [clave setup |services#Setup]. También aquí se usa el suministro automático de dependencias mediante autowiring: - -```neon -services: - - create: MyClass - setup: - - setCache -``` - - -Inyección en propiedades -======================== - -Las dependencias se proporcionan escribiendo directamente en una propiedad miembro: - -```php -class MyClass -{ - public Cache $cache; -} - -$obj = new MyClass; -$obj->cache = $cache; -``` - -Este método se considera inadecuado, porque la propiedad miembro debe declararse `public`. En consecuencia perdemos el control sobre que la dependencia pasada sea realmente del tipo requerido (esto valía sobre todo antes de que PHP 7.4 introdujera los type hints en las propiedades) y perdemos la posibilidad de reaccionar con lógica propia ante una dependencia recién asignada, por ejemplo para impedir su modificación posterior. Al mismo tiempo, la propiedad pasa a formar parte de la API pública de la clase, lo que puede no ser lo deseado. - -La asignación a la propiedad se define en la configuración del contenedor DI en la [sección setup |services#Setup]: - -```neon -services: - - create: MyClass - setup: - - $cache = @\Cache -``` - - -Inject -====== - -Mientras que los tres enfoques anteriores valen en general en todos los lenguajes orientados a objetos, la inyección mediante los métodos `inject*()` o el atributo `#[Inject]` se usa normalmente con los presenters de Nette, donde está activada de forma predeterminada; cualquier otro servicio puede activarla con [`inject: true` |services#Modo inject]. Se tratan en un [capítulo aparte |best-practices:inject-method-attribute]. - - -¿Qué método elegir? -=================== - -- El constructor es adecuado para las dependencias obligatorias que la clase necesita imprescindiblemente para funcionar. -- El setter, en cambio, es adecuado para las dependencias opcionales o para las que quizá haya que cambiar más adelante. -- Las propiedades públicas no se recomiendan en general. diff --git a/dependency-injection/es/services.texy b/dependency-injection/es/services.texy deleted file mode 100644 index 37760a0faa..0000000000 --- a/dependency-injection/es/services.texy +++ /dev/null @@ -1,480 +0,0 @@ -Definición de servicios -*********************** - -.[perex] -En la configuración le indicamos al contenedor DI cómo debe crear cada servicio y cómo conectarlo con sus dependencias. Nette ofrece para ello una forma muy clara y elegante. - -La sección `services` del archivo de configuración NEON es donde definimos nuestros propios servicios y su configuración. Veamos un ejemplo sencillo que define un servicio llamado `database`, que representa una instancia de la clase `PDO`: - -```neon -services: - database: PDO('sqlite::memory:') -``` - -La configuración anterior da como resultado el siguiente método factory en el [contenedor DI|container]: - -```php -public function createServiceDatabase(): PDO -{ - return new PDO('sqlite::memory:'); -} -``` - -Los nombres de los servicios permiten referirse a ellos en otras partes del archivo de configuración con el formato `@nombreDelServicio`. Si no hace falta darle nombre al servicio, podemos usar simplemente un guion (`-`): - -```neon -services: - - PDO('sqlite::memory:') -``` - -Para obtener un servicio del contenedor DI podemos usar el método `getService()` con el nombre del servicio como parámetro, o el método `getByType()` con el tipo del servicio: - -```php -$database = $container->getService('database'); -$database = $container->getByType(PDO::class); -``` - - -Creación de servicios -===================== - -Normalmente creamos un servicio simplemente instanciando una clase concreta. Por ejemplo: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Si necesitamos ampliar la configuración con más claves, la definición se puede repartir en varias líneas: - -```neon -services: - database: - create: PDO('sqlite::memory:') - setup: ... -``` - -La clave `create` tiene el alias `factory`; ambas variantes se usan habitualmente. Recomendamos, sin embargo, usar `create`. - -Los argumentos del constructor o del método factory se pueden indicar alternativamente con la clave `arguments`: - -```neon -services: - database: - create: PDO - arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] -``` - -Los servicios no tienen por qué crearse necesariamente instanciando una clase; también pueden ser el resultado de llamar a métodos estáticos o a métodos de otros servicios: - -```neon -services: - database: DatabaseFactory::create() - router: @routerFactory::create() -``` - -Fíjese en que, por simplicidad, se usa `::` en lugar de `->`, véase [#Lenguaje de expresiones]. Se generarán estos métodos factory: - -```php -public function createServiceDatabase(): PDO -{ - return DatabaseFactory::create(); -} - -public function createServiceRouter(): RouteList -{ - return $this->getService('routerFactory')->create(); -} -``` - -El contenedor DI necesita conocer el tipo del servicio que se crea. Si creamos un servicio con un método que no tiene indicado el tipo de retorno, debemos declarar ese tipo explícitamente en la configuración: - -```neon -services: - database: - create: DatabaseFactory::create() - type: PDO -``` - - -Argumentos -========== - -Los argumentos se pasan a los constructores y a los métodos de forma muy parecida a como se hace en el propio PHP: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Para mejorar la legibilidad podemos poner los argumentos en líneas separadas. En ese caso, las comas son opcionales: - -```neon -services: - database: PDO( - 'mysql:host=127.0.0.1;dbname=test' - root - secret - ) -``` - -También puede dar nombre a los argumentos, con lo que no tendrá que preocuparse por su orden: - -```neon -services: - database: PDO( - username: root - password: secret - dsn: 'mysql:host=127.0.0.1;dbname=test' - ) -``` - -Si quiere omitir algunos argumentos y usar sus valores por defecto, o que se le inyecte un servicio mediante [autowiring|autowiring], use un guion bajo (`_`): - -```neon -services: - foo: Foo(_, %appDir%) -``` - -Los argumentos pueden incluir servicios, parámetros y mucho más, véase [#Lenguaje de expresiones]. - - -Setup -===== - -En la sección `setup` definimos los métodos que deben llamarse al crear el servicio. - -```neon -services: - database: - create: PDO(%dsn%, %user%, %password%) - setup: - - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) -``` - -En PHP esto tendría este aspecto: - -```php -public function createServiceDatabase(): PDO -{ - $service = new PDO('...', '...', '...'); - $service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - return $service; -} -``` - -Además de llamar a métodos, también se pueden asignar valores a propiedades. También se admite añadir elementos a arrays, para lo que hay que encerrar el acceso al array entre comillas para evitar conflictos con la sintaxis de NEON: - -```neon -services: - foo: - create: Foo - setup: - - $value = 123 - - '$onClick[]' = [@bar, clickHandler] -``` - -Lo que en código PHP tendría este aspecto: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - $service->value = 123; - $service->onClick[] = [$this->getService('bar'), 'clickHandler']; - return $service; -} -``` - -En el setup puede, sin embargo, llamar también a métodos estáticos o a métodos de otros servicios. Si necesita pasar como argumento el propio servicio actual, refiérase a él con `@self`: - -```neon -services: - foo: - create: Foo - setup: - - My\Helpers::initializeFoo(@self) - - @anotherService::setFoo(@self) -``` - -Fíjese en que, por simplicidad, se usa `::` en lugar de `->`, véase [#Lenguaje de expresiones]. Se generará este método factory: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - My\Helpers::initializeFoo($service); - $this->getService('anotherService')->setFoo($service); - return $service; -} -``` - - -Lenguaje de expresiones -======================= - -Nette DI ofrece un lenguaje de expresiones excepcionalmente rico con el que podemos definir casi cualquier cosa. En los archivos de configuración podemos usar así [parámetros |configuration#Parámetros]: - -```neon -# parámetro -%wwwDir% - -# valor de un parámetro bajo una clave -%mailer.user% - -# parámetro dentro de una cadena -'%wwwDir%/images' -``` - -Además, crear objetos y llamar a métodos y funciones: - -```neon -# crear un objeto -DateTime() - -# llamar a un método estático -Collator::create(%locale%) - -# llamar a una función de PHP -::getenv(DB_USER) -``` - -Referirse a los servicios por su nombre o por su tipo: - -```neon -# servicio por nombre -@database - -# servicio por tipo -@Nette\Database\Connection -``` - -Usar la sintaxis first-class callable: .{data-version:3.2.0} - -```neon -# crear un callback, equivalente a [@user, logout] -@user::logout(...) -``` - -Usar constantes: - -```neon -# constante de clase -FilesystemIterator::SKIP_DOTS - -# obtener una constante global con la función constant() de PHP -::constant(\PHP_VERSION) -``` - -Acceder a las propiedades públicas y a las constantes de un servicio con `@servicio::miembro`. Si el nombre se resuelve como propiedad o como constante lo decide su primera letra: una inicial minúscula significa propiedad pública, una mayúscula significa constante: - -```neon -# propiedad pública de un servicio (empieza por minúscula) -@settings::apiUrl - -# constante de clase de un servicio (empieza por mayúscula) -@settings::Version -``` - -Las llamadas a métodos se pueden encadenar igual que en PHP. Por simplicidad se usa `::` en lugar de `->`: - -```neon -DateTime()::format('Y-m-d') -# PHP: (new DateTime())->format('Y-m-d') - -@http.request::getUrl()::getHost() -# PHP: $this->getService('http.request')->getUrl()->getHost() -``` - -Puede usar estas expresiones en cualquier sitio: al [crear servicios |#Creación de servicios], en los [#Argumentos], en la sección [#Setup] o en los [parámetros |configuration#Parámetros]: - -```neon -parameters: - ipAddress: @http.request::getRemoteAddress() - -services: - database: - create: DatabaseFactory::create( @anotherService::getDsn() ) - setup: - - initialize( ::getenv('DB_USER') ) -``` - - -Funciones especiales --------------------- - -En los archivos de configuración puede usar las siguientes funciones especiales: - -- `not()` niega un valor -- `bool()`, `int()`, `float()`, `string()` conversión sin pérdida al tipo indicado .{data-version:3.0.5} -- `typed()` crea un array de todos los servicios del tipo indicado -- `tagged()` crea un array de todos los servicios con la etiqueta dada - -```neon -services: - - Foo( - id: int(::getenv('ProjectId')) - productionMode: not(%debugMode%) - ) -``` - -A diferencia de la conversión estándar de PHP, como `(int)`, la conversión sin pérdida lanza una excepción para valores no numéricos. - -La función `typed()` crea un array de todos los servicios del tipo indicado (clase o interfaz). Excluye los servicios que tienen el autowiring desactivado. También se pueden indicar varios tipos separados por comas. - -```neon -services: - - BarsDependent( typed(Bar) ) -``` - -Un array de servicios de un determinado tipo también se puede pasar como argumento automáticamente mediante [autowiring |autowiring#Colección de servicios]. - -La función `tagged()` crea un array de todos los servicios con una etiqueta concreta. También aquí puede indicar varias etiquetas separadas por comas. - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - - -Autowiring -========== - -La clave `autowired` le permite influir en el comportamiento del autowiring para un servicio concreto. Los detalles están en el [capítulo sobre autowiring|autowiring]. - -```neon -services: - foo: - create: Foo - autowired: false # el servicio foo queda excluido del autowiring -``` - - -Servicios lazy .{data-version:3.2.4} -==================================== - -La carga lazy es una técnica que aplaza la creación de un servicio hasta que realmente se necesita. En la configuración global puede [activar la creación lazy |configuration#Servicios lazy] para todos los servicios de una vez. Para los servicios concretos puede después modificar ese comportamiento: - -```neon -services: - foo: - create: Foo - lazy: false -``` - -Cuando un servicio se define como lazy, al pedirlo al contenedor DI recibimos un objeto proxy especial. Ese proxy tiene el mismo aspecto y se comporta igual que el servicio real, pero la inicialización real (la llamada al constructor y las llamadas del setup) ocurre solo en el primer acceso a alguno de sus métodos o propiedades. - -Tenga en cuenta que, como el servicio se crea más tarde, los errores de su configuración también se manifiestan más tarde. Por ejemplo, unas credenciales de base de datos incorrectas no se revelarán al arrancar la aplicación, sino en la primera consulta. - -La creación lazy también mitiga las dependencias circulares, es decir, la situación en la que el servicio A necesita el servicio B y B necesita a la vez A. Sin ella, el contenedor informa del error `Circular reference detected`. Con un proxy lazy, el servicio A recibe solo un proxy del servicio B, que se inicializa cuando realmente se usa, en un momento en el que A ya existe. Aun así, una dependencia circular señala un diseño defectuoso y es mejor deshacerse de ella. - -.[note] -La carga lazy requiere PHP 8.4 o superior y funciona solo para servicios creados instanciando directamente una clase (p. ej. `create: Foo`), no para los creados por un método factory. Tampoco se puede usar para clases que en última instancia extienden una clase interna de PHP. Cuando la carga lazy no se puede aplicar, la bandera `lazy: true` se ignora en silencio. - - -Etiquetas -========= - -Las etiquetas sirven para añadir información complementaria a los servicios. Puede asignar una o varias etiquetas a un servicio: - -```neon -services: - foo: - create: Foo - tags: - - cached -``` - -Las etiquetas también pueden llevar valores: - -```neon -services: - foo: - create: Foo - tags: - logger: monolog.logger.event -``` - -Para obtener todos los servicios asociados a determinadas etiquetas puede usar la función `tagged()`: - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - -Dentro del contenedor DI puede obtener los nombres de todos los servicios con una etiqueta concreta con el método `findByTag()`: - -```php -$names = $container->findByTag('logger'); -// $names es un array con los nombres de los servicios como claves y los valores de la etiqueta como valores -// p. ej. ['foo' => 'monolog.logger.event', ...] -``` - - -Modo inject -=========== - -La bandera `inject: true` activa la inyección de dependencias mediante propiedades públicas con el atributo [Inject |best-practices:inject-method-attribute#Atributos Inject] y mediante los métodos [inject*() |best-practices:inject-method-attribute#Métodos inject*()]. - -```neon -services: - articles: - create: App\Model\Articles - inject: true -``` - -De forma predeterminada, el modo `inject` está activado solo para los presenters. - - -Modificación de servicios -========================= - -El contenedor DI contiene numerosos servicios añadidos por las extensiones integradas o [propias|extensions]. Puede modificar las definiciones de esos servicios existentes directamente en la configuración. Por ejemplo, puede cambiar la clase del servicio `application.application`, que por defecto es `Nette\Application\Application`, por otra: - -```neon -services: - application.application: - create: MyApplication - alteration: true -``` - -La bandera `alteration` indica que solo estamos modificando un servicio existente. También actúa como salvaguarda: si el servicio que se modifica no existe, la compilación falla con una excepción. - -También podemos completar el setup: - -```neon -services: - application.application: - create: MyApplication - alteration: true - setup: - - '$onStartup[]' = [@resource, init] -``` - -No tiene que identificar el servicio por su nombre interno: puede referirse a él por su tipo. El ejemplo anterior también se puede escribir así: - -```neon -services: - @Nette\Application\Application: - create: MyApplication -``` - -Al modificar un servicio quizá queramos eliminar los argumentos, los elementos del setup o las etiquetas originales, con la clave `reset`: - -```neon -services: - application.application: - create: MyApplication - alteration: true - reset: - arguments: true - setup: true - tags: true -``` - -Si quiere eliminar un servicio añadido por una extensión, puede hacerlo así: - -```neon -services: - cache.journal: false -``` diff --git a/dependency-injection/es/upgrading.texy b/dependency-injection/es/upgrading.texy deleted file mode 100644 index 995c047db3..0000000000 --- a/dependency-injection/es/upgrading.texy +++ /dev/null @@ -1,49 +0,0 @@ -Actualización -************* - - -Actualización a la versión 3.1 -============================== - -- el autowiring ya no pasa `null` a un parámetro que admite null y no tiene valor por defecto; pase el argumento explícitamente o dele al parámetro un valor por defecto -- se ha eliminado el soporte de la anotación `@return`; use un tipo de retorno o indique el tipo en la definición del servicio con `type:` -- la clave `dynamic` se ha renombrado a `imported` y `class` a `type` -- el símbolo para un argumento omitido ha cambiado de `...` a `_`, p. ej. `MyService(_, 123)` -- en los archivos NEON ya no hace falta escapar el carácter `@` al principio de una cadena -- la clave `parameters` dentro de las definiciones de factories generadas está obsoleta -- el método `Nette\DI\Config\Loader::save()` está obsoleto; exporte la configuración con `Nette\DI\Config\Adapters\NeonAdapter::dump()` - -La versión 3.1 es una versión de transición: no trae funcionalidades nuevas, pero avisa con notices de todo lo que funcionará de otra manera más adelante. Véase el artículo [Nette DI 3.1: transition release |https://blog.nette.org/en/nette-di-3-1-transition-release]. - - -Actualización a la versión 3.0 -============================== - -- se ha eliminado el soporte de los archivos INI -- se ha eliminado la escritura directa de código PHP en la configuración con signos de interrogación (p. ej. `"$service->onError[] = ?"(...)`); use en su lugar la sintaxis de array `'$onError[]' = [...]` -- en los archivos de configuración, use `factory: PDO(...)` en lugar de `class: PDO(...)` -- la etiqueta `nette.presenter` ya no se usa para los presenters - - -Para los autores de extensiones del compilador ----------------------------------------------- - -Mientras que Nette 2.4 describía internamente cada servicio como `Nette\DI\ServiceDefinition`, ahora hay varios tipos de definición: `Nette\DI\Definitions\ImportedDefinition` para los servicios importados (dinámicos), `Nette\DI\Definitions\FactoryDefinition` para las factories generadas a partir de interfaces, `Nette\DI\Definitions\AccessorDefinition` para los accessors generados y `Nette\DI\Definitions\ServiceDefinition` para los servicios corrientes. - -Por eso, además de `ContainerBuilder::addDefinition()` hay otros métodos para crear una nueva definición: `addFactoryDefinition()`, `addAccessorDefinition()` y `addImportedDefinition()`. - - -Actualización a la versión 2.4 -============================== - -- las secciones de configuración (p. ej. production, development) en un único archivo de configuración están obsoletas; use un par de archivos `config.neon` y `config.local.neon` -- la herencia de definiciones de servicios está obsoleta -- `Statement::setEntity()` está obsoleto - - -Actualización a la versión 2.3 -============================== - -- se ha eliminado el soporte para colocar servicios dentro de la sección de la extensión en el archivo de configuración -- se ha eliminado el soporte de las extensiones añadidas dinámicamente -- al sustituir un servicio dinámicamente (con `removeService()`, `addService()`), el nuevo servicio debe ser una instancia de la misma interfaz o clase que el original diff --git a/dependency-injection/fr/@home.texy b/dependency-injection/fr/@home.texy deleted file mode 100644 index 882a530082..0000000000 --- a/dependency-injection/fr/@home.texy +++ /dev/null @@ -1,22 +0,0 @@ -Nette DI -******** - -.[perex] -L'injection de dépendances est un patron de conception qui changera fondamentalement votre regard sur le code et le développement. Elle ouvre la voie vers un monde d'applications proprement conçues et durables. - -- [Qu'est-ce que l'injection de dépendances ? |introduction] -- [État global & singletons |global-state] -- [Passage des dépendances |passing-dependencies] -- [Qu'est-ce qu'un conteneur DI ? |container] -- [Questions fréquentes |faq] - - -Le paquet `nette/di` fournit un conteneur DI compilé extrêmement avancé pour PHP. - -- [Nette DI Container |nette-container] -- [Configuration |configuration] -- [Définition des services |services] -- [Autowiring |autowiring] -- [Factories générées |factory] -- [Créer des extensions pour Nette DI |extensions] -- [La compilation du conteneur en détail |compilation-internals] diff --git a/dependency-injection/fr/@left-menu.texy b/dependency-injection/fr/@left-menu.texy deleted file mode 100644 index 91e718b494..0000000000 --- a/dependency-injection/fr/@left-menu.texy +++ /dev/null @@ -1,28 +0,0 @@ -Dependency Injection -******************** -- [Qu'est-ce que la DI ? |introduction] -- [État global & singletons |global-state] -- [Passage des dépendances |passing-dependencies] -- [Qu'est-ce qu'un conteneur DI ? |container] -- [Questions fréquentes |faq] - - -Nette DI --------- -- [Nette DI Container |nette-container] -- [Configuration |configuration] -- [Définition des services |services] -- [Autowiring |autowiring] -- [Factories générées |factory] -- [Créer des extensions pour Nette DI |extensions] -- [La compilation en détail |compilation-internals] -- [Mise à niveau|upgrading] - - -Pour aller plus loin -******************** -- [Documentation Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Bonnes pratiques |best-practices:] -- [Résolution de problèmes |nette:troubleshooting] diff --git a/dependency-injection/fr/@meta.texy b/dependency-injection/fr/@meta.texy deleted file mode 100644 index 72ae4b8db8..0000000000 --- a/dependency-injection/fr/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentation Nette}} diff --git a/dependency-injection/fr/autowiring.texy b/dependency-injection/fr/autowiring.texy deleted file mode 100644 index f421d02c91..0000000000 --- a/dependency-injection/fr/autowiring.texy +++ /dev/null @@ -1,279 +0,0 @@ -Autowiring -********** - -.[perex] -L'autowiring est une fonctionnalité formidable qui passe automatiquement les services requis au constructeur et aux autres méthodes, si bien que nous n'avons pas à les indiquer explicitement. Il vous fait gagner énormément de temps. - -Grâce à lui, nous pouvons omettre la grande majorité des arguments lors de l'écriture des définitions de services. Au lieu de : - -```neon -services: - articles: Model\ArticleRepository(@database, @cache.storage) -``` - -Il suffit d'écrire : - -```neon -services: - articles: Model\ArticleRepository -``` - -L'autowiring se guide sur les types ; pour qu'il fonctionne, la classe `ArticleRepository` doit donc être définie à peu près ainsi : - -```php -namespace Model; - -class ArticleRepository -{ - public function __construct(\PDO $db, \Nette\Caching\Storage $storage) - {} -} -``` - -L'autowiring n'utilise jamais les noms des services. Il se guide uniquement sur le système de types de PHP, il sait donc aussi qu'une classe satisfait les interfaces qu'elle implémente et les classes dont elle hérite. De ce fait, le nom d'un service n'est qu'un identifiant auxiliaire et le renommer ne cassera rien dans l'application. - -Pour pouvoir utiliser l'autowiring, il doit y avoir dans le conteneur **exactement un service** de chaque type. S'il y en avait plusieurs, l'autowiring ne saurait pas lequel passer et lèverait une exception : - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - tempDb: PDO('sqlite::memory:') - articles: Model\ArticleRepository # LÈVE UNE EXCEPTION, mainDb et tempDb correspondent -``` - -Une solution consiste à contourner l'autowiring et à indiquer explicitement le nom du service (par exemple `articles: Model\ArticleRepository(@mainDb)`). Il est cependant plus commode soit de [désactiver |#Désactiver l'autowiring] l'autowiring pour l'un des services, soit de [préférer |#Préférence d'autowiring] un service aux autres. - - -Désactiver l'autowiring ------------------------ - -Nous pouvons désactiver l'autowiring d'un service à l'aide de l'option `autowired: false` : - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - - tempDb: - create: PDO('sqlite::memory:') - autowired: false # le service tempDb est exclu de l'autowiring - - articles: Model\ArticleRepository # passe donc mainDb au constructeur -``` - -Le service `articles` ne lèvera pas d'exception au sujet de deux services `PDO` correspondants (`mainDb` et `tempDb`) disponibles pour le constructeur, car il ne prend en compte que le service `mainDb`. - -L'autowiring peut aussi être désactivé globalement pour des types entiers à l'aide de l'option de configuration [`di › excluded` |configuration#DI], qui énumère les types (et leurs descendants) qui ne doivent jamais être autowirés. - -.[note] -La configuration de l'autowiring dans Nette diffère de celle de Symfony. Dans Symfony, `autowire: false` signifie que l'autowiring ne doit pas être utilisé pour les arguments du constructeur du service. Dans Nette, l'autowiring s'applique aux arguments du constructeur et à toute autre méthode appelée par le conteneur (comme l'injection par setter). L'option `autowired: false` empêche le conteneur de passer automatiquement cette instance de service comme dépendance à d'autres services. - - -Préférence d'autowiring ------------------------ - -Si nous avons plusieurs services du même type et que nous indiquons l'option `autowired` pour l'un d'eux, ce service devient le service préféré : - -```neon -services: - mainDb: - create: PDO(%dsn%, %user%, %password%) - autowired: PDO # devient le préféré - - tempDb: - create: PDO('sqlite::memory:') - - articles: Model\ArticleRepository -``` - -Le service `articles` ne lèvera pas d'exception au sujet de plusieurs services `PDO` correspondants (`mainDb` et `tempDb`), mais utilisera le service préféré, à savoir `mainDb`. - - -Collection de services ----------------------- - -L'autowiring sait aussi passer des tableaux de services d'un type donné. Comme PHP ne permet pas nativement d'indiquer le type des éléments d'un tableau dans les déclarations de type, vous devez compléter la déclaration `array` par un commentaire phpDoc précisant le type des éléments, par exemple `ClassName[]` : - -```php -namespace Model; - -class ShipManager -{ - /** - * @param Shipper[] $shippers - */ - public function __construct(array $shippers) - {} -} -``` - -Le conteneur DI passe alors automatiquement un tableau des services correspondant au type donné. Il omet les services dont l'[autowiring est désactivé |#Désactiver l'autowiring] et n'inclut jamais dans sa propre collection le service en cours de création. Contrairement au passage d'un service isolé, [restreindre |#Restreindre l'autowiring] l'autowiring à un type donné ou marquer un service comme [préféré |#Préférence d'autowiring] n'a ici aucun effet - le tableau contient toujours tous les services du type donné. - -Le type indiqué dans le commentaire peut aussi prendre la forme `array<int, Class>` ou `list<Class>`. Si vous ne maîtrisez pas la forme du commentaire phpDoc, vous pouvez passer un tableau de services directement dans la configuration à l'aide de [`typed()` |services#Fonctions spéciales]. - - -Arguments scalaires -------------------- - -L'autowiring ne fonctionne que pour les objets et les tableaux d'objets. Les arguments scalaires (par exemple les chaînes, les nombres, les booléens) doivent être [indiqués dans la configuration |services#Arguments]. Une alternative consiste à créer un [objet de configuration|best-practices:passing-settings-to-presenters] qui encapsule la valeur scalaire (ou plusieurs valeurs). Cet objet peut ensuite être passé par autowiring. - -```php -class MySettings -{ - public function __construct( - // readonly peut être utilisé depuis PHP 8.1 - public readonly bool $value, - ) - {} -} -``` - -Vous l'enregistrez comme service en l'ajoutant à la configuration : - -```neon -services: - - MySettings('any value') -``` - -Les autres classes peuvent ensuite le demander par autowiring. - - -Dépendances facultatives ------------------------- - -Si un paramètre de constructeur ou de méthode a une valeur par défaut et qu'aucun service du type requis n'existe dans le conteneur, l'autowiring ne lève pas d'exception - il saute simplement l'argument, si bien que la valeur par défaut est utilisée. C'est ainsi que vous déclarez des dépendances facultatives : - -```php -class Foo -{ - public function __construct( - private ?Logger $logger = null, - ) {} -} -``` - -En revanche, pour un paramètre sans valeur par défaut, un service manquant provoque toujours une exception. - - -Restreindre l'autowiring ------------------------- - -Pour chaque service, l'autowiring peut être restreint à certaines classes ou interfaces. - -Normalement, l'autowiring passe un service à tout paramètre de méthode dont le type correspond au service. Restreindre signifie que nous posons des conditions que les types indiqués pour les paramètres des méthodes doivent remplir pour que le service leur soit passé. - -Prenons un exemple : - -```php -class ParentClass -{} - -class ChildClass extends ParentClass -{} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Si nous les enregistrions tous comme services, l'autowiring échouerait : - -```neon -services: - parent: ParentClass - child: ChildClass - parentDep: ParentDependent # LÈVE UNE EXCEPTION, parent et child correspondent - childDep: ChildDependent # l'autowiring passe le service child au constructeur -``` - -Le service `parentDep` lève l'exception `Multiple services of type ParentClass found: child, parent`, car les services `parent` et `child` conviennent tous deux à son constructeur et l'autowiring ne peut pas décider lequel choisir. - -Pour le service `child`, nous pouvons donc restreindre son autowiring au type `ChildClass` : - -```neon -services: - parent: ParentClass - child: - create: ChildClass - autowired: ChildClass # peut aussi s'écrire 'autowired: self' - - parentDep: ParentDependent # l'autowiring passe le service parent au constructeur - childDep: ChildDependent # l'autowiring passe le service child au constructeur -``` - -Désormais, c'est le service `parent` qui est passé au constructeur du service `parentDep`, car il est maintenant le seul objet correspondant. Le service `child` n'y est plus passé par l'autowiring. Oui, le service `child` est toujours du type `ParentClass`, mais la condition de restriction `autowired: ChildClass` fait qu'il ne sera passé qu'aux paramètres explicitement typés `ChildClass` (ou ses sous-types). Comme `ParentDependent` exige `ParentClass`, le service `child` n'y est plus considéré comme candidat à l'autowiring. - -Pour le service `child`, `autowired: ChildClass` pourrait aussi s'écrire `autowired: self`, car `self` est un placeholder pour la classe du service courant. - -Dans la clé `autowired`, il est également possible d'indiquer plusieurs classes ou interfaces sous forme de tableau : - -```neon -autowired: [ParentClass, FooInterface] -``` - -Essayons d'ajouter des interfaces à l'exemple : - -```php -interface FooInterface -{} - -interface BarInterface -{} - -class ParentClass implements FooInterface -{} - -class ChildClass extends ParentClass implements BarInterface -{} - -class FooDependent -{ - function __construct(FooInterface $obj) - {} -} - -class BarDependent -{ - function __construct(BarInterface $obj) - {} -} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Si nous ne restreignons en rien le service `child`, il conviendra aux constructeurs de toutes les classes `FooDependent`, `BarDependent`, `ParentDependent` et `ChildDependent`, et l'autowiring l'y passera. - -En revanche, si nous restreignons son autowiring à `ChildClass` avec `autowired: ChildClass` (ou `self`), l'autowiring ne le passera qu'au constructeur de `ChildDependent`, car celui-ci exige un argument de type `ChildClass` et il est vrai que `ChildClass` *est du type* `ChildClass`. Aucun des types requis par les autres paramètres n'est `ChildClass` ni un de ses sous-types, le service ne leur est donc pas passé. - -Si nous le restreignons à `ParentClass` avec `autowired: ParentClass`, l'autowiring le passera de nouveau au constructeur de `ChildDependent` (car le `ChildClass` requis est un sous-type de `ParentClass`) et désormais aussi au constructeur de `ParentDependent`, car le type requis `ParentClass` convient également. - -Si nous le restreignons à `FooInterface`, il sera toujours autowiré dans `ParentDependent` (le `ParentClass` requis est un sous-type de `FooInterface`) et dans `ChildDependent`, et en plus dans le constructeur de `FooDependent`, mais pas dans `BarDependent`, car `BarInterface` n'est pas un sous-type de `FooInterface`. - -```neon -services: - child: - create: ChildClass - autowired: FooInterface - - fooDep: FooDependent # l'autowiring passe le service child au constructeur - barDep: BarDependent # LÈVE UNE EXCEPTION, aucun service ne correspond - parentDep: ParentDependent # l'autowiring passe le service child au constructeur - childDep: ChildDependent # l'autowiring passe le service child au constructeur -``` diff --git a/dependency-injection/fr/compilation-internals.texy b/dependency-injection/fr/compilation-internals.texy deleted file mode 100644 index a2b595c699..0000000000 --- a/dependency-injection/fr/compilation-internals.texy +++ /dev/null @@ -1,222 +0,0 @@ -La compilation du conteneur en détail -************************************* - -.[perex] -Cette page ouvre le capot de la compilation du conteneur : les phases qu'elle traverse, le moment où les paramètres de configuration sont développés, celui où les chaînes `@service` deviennent de vraies références et - la question que les auteurs d'extensions posent le plus souvent - la phase dans laquelle vous pouvez rechercher sans risque les services par type. C'est le complément approfondi de [Créer des extensions |extensions]. - -Vous n'avez besoin de rien de tout cela pour écrire une application normale, ni même une extension normale. Mais dès que votre extension se met à inspecter ou à remodeler le graphe des services, le timing devient déterminant : le même appel à `getByType()` donne une réponse fiable dans une phase et une réponse trompeuse dans une autre. Cette page explique pourquoi, afin que vous sachiez toujours où placer votre code. - - -Deux mondes : compilation et exécution -====================================== - -La chose la plus importante à comprendre est qu'un conteneur Nette **n'est pas assemblé à chaque requête**. Il est construit une seule fois sous forme de classe PHP optimisée, cette classe est stockée sur le disque et chaque requête suivante se contente d'`include` le fichier terminé. Toute la machinerie décrite ci-dessous - extensions, resolvers, générateur de code - ne tourne **que pendant la (re)compilation**. - -Cela sépare le monde en deux représentations qui ne coexistent jamais : - -| | pendant la compilation | à l'exécution -|---|---|--- -| Ce qui existe | **définitions** (recettes) dans `ContainerBuilder` | **instances** des services dans `Container` -| Classes clés | `Compiler`, `ContainerBuilder`, `Resolver`, `PhpGenerator` | `Container` (parent de la classe générée) -| `%param%`, `@service` | marqueurs textuels encore en cours de traduction | déjà traduits / figés dans le code - -La classe générée étend `Nette\DI\Container` et possède une méthode `createServiceXxx()` pour chaque service. Ses paramètres et les métadonnées de l'autowiring sont précalculés, si bien qu'à l'exécution il n'y a plus rien à résoudre - seulement des services à instancier à la demande. - -.[note] -En mode développement, le conteneur est reconstruit automatiquement dès qu'un fichier de configuration ou une classe d'extension change ; les deux sont suivis comme dépendances. En production, il est compilé une fois et jamais revérifié, et c'est de là que vient la vitesse. - - -Les phases en un coup d'œil -=========================== - -La compilation est orchestrée par `Compiler::compile()` et se ramène à trois étapes : - -```php -public function compile(): string -{ - $this->processExtensions(); // PHASE A : schémas + loadConfiguration() - $this->processBeforeCompile(); // PHASE B : resolve + beforeCompile() + complete - return $this->generateCode(); // PHASE C : génération du code + afterCompile() -} -``` - -Tout le modèle mental tient en une seule idée : **chaque phase en sait plus que la précédente.** - -- La **phase A** remplit le graphe de définitions. Les **types des services ne sont pas encore connus de façon fiable**, car un type peut provenir de la valeur de retour d'une factory que personne n'a encore examinée. -- La **phase B** résout d'abord tous les types (`resolve`), laisse ensuite les extensions remodeler le graphe (`beforeCompile`) et enfin [autowire |autowiring] les arguments (`complete`). -- La **phase C** transforme le graphe terminé en PHP et laisse les extensions retoucher le code généré. - -C'est précisément cette connaissance croissante qui fait que la même opération est sûre dans une phase et peu fiable dans une autre. Le reste de cette page parcourt les phases en gardant cette idée à l'esprit. - - -Phase A : enregistrement des définitions -======================================== - -Dans cette phase, Nette appelle trois méthodes sur chaque extension - `getConfigSchema()`, puis `setConfig()`, puis `loadConfiguration()` - mais dans un **ordre soigneusement contrôlé**, car ici l'ordre compte vraiment. - - -Pourquoi l'ordre compte ------------------------ - -- **`ParametersExtension` et `ExtensionsExtension` passent en premier.** La première doit s'exécuter avant tout le reste afin de pouvoir développer `%param%` dans toute la configuration - chaque autre extension reçoit ensuite sa propre section avec les valeurs déjà remplies. La seconde enregistre les extensions supplémentaires listées dans la section `extensions:`, elle doit donc elle aussi exister avant que les autres soient traitées. -- **`ServicesExtension` passe en dernier.** La section `services:` de l'utilisateur a donc toujours le dernier mot et peut redéfinir tout ce que les extensions ont mis en place. -- **`InjectExtension` est déplacée tout à la fin** pour que son travail tienne compte des setups ajoutés par toutes les autres extensions. - -Ce qu'il faut en retenir : au moment où le `loadConfiguration()` de votre extension s'exécute, les paramètres sont déjà développés, mais les services de l'utilisateur ne sont pas encore là. Ce simple fait explique la plupart des règles de timing qui suivent. - - -De services: aux définitions ----------------------------- - -La section `services:` de l'utilisateur est transformée en [objets de définition |extensions#Types de définitions] ici, à la dernière étape de la phase A. Chaque entrée NEON est normalisée (les notations abrégées sont unifiées), son genre est détecté (service ordinaire, factory, accesseur, ...) et une définition correspondante est créée dans le builder. C'est aussi le premier moment où les arguments simples `@name` / `@Type` deviennent des références - voir [ci-dessous |#Références : quand @service devient une référence]. - -À la fin de la phase A, toutes les définitions sont présentes - chaque extension et l'utilisateur ont enregistré ce qu'ils voulaient - mais l'image n'est pas encore nette : - -- **les types ne sont pas résolus** pour les définitions dont le type provient de la valeur de retour d'une factory, -- **les arguments ne sont pas autowirés**, -- certaines références `@service` sont encore de simples chaînes. - -C'est exactement pour cela que la recherche par type n'est pas fiable ici - voir [ci-dessous |#Inspecter ContainerBuilder : quand est-ce sûr]. - - -Paramètres : quand %param% est développé -======================================== - -L'une des deux grandes questions. La réponse est courte : **une seule fois, tout au début de la phase A, dans tout l'arbre de configuration.** - -`ParametersExtension` s'exécute en premier et l'une des premières choses qu'elle fait est de développer les placeholders `%param%` - d'abord à l'intérieur des paramètres eux-mêmes (un paramètre peut en référencer un autre), puis dans tout le reste de la configuration. Ainsi, au moment où n'importe quelle autre extension, y compris `ServicesExtension`, reçoit sa section, les placeholders ont déjà disparu. Les extensions travaillent avec des valeurs concrètes, jamais avec des `%...%`. - -Quand un placeholder constitue toute la chaîne, sa valeur est renvoyée *telle quelle* - y compris les tableaux et les objets - si bien que `%mailer%` peut se développer en un tableau entier. Partout ailleurs, il est concaténé dans une chaîne, et la notation pointée `%foo.bar%` atteint les tableaux imbriqués. - - -Paramètres statiques et dynamiques ----------------------------------- - -Toutes les valeurs ne peuvent pas être figées dans le code. Un paramètre dont la valeur diffère selon l'environnement - une variable d'environnement, la `baseUrl` déduite de la requête - doit rester **dynamique**. Vous déclarez de tels paramètres avec `setDynamicParameterNames()` ou `Expect::...->dynamic()` dans un schéma ; plus d'informations dans [Paramètres dynamiques |application:bootstrapping#Paramètres Dynamiques]. - -Un paramètre dynamique n'est pas remplacé par une valeur, mais par une expression qui la lit *à l'exécution*. `%env.DB_HOST%` ne se fige donc pas en une chaîne ; il devient une lecture effectuée à l'exécution dans le conteneur généré. Tout le reste est statique et se fige au moment de la compilation - c'est la source habituelle de la surprise "ma valeur `getenv()` est la même dans tous les environnements" : le paramètre était tout simplement statique. - -L'opération inverse est l'**échappement** : pour éviter qu'un `%` ou un `@` littéral soit interprété, on le double (`%%`, `@@`). Nette le fait automatiquement pour les paramètres qu'il injecte à votre place, si bien que leurs valeurs ne sont jamais prises pour des placeholders ou des références. - - -Références : quand @service devient une référence -================================================= - -La deuxième grande question. La traduction de `@service` se fait **en plusieurs étapes réparties sur différentes phases**, selon la complexité de la chaîne. Vous avez rarement besoin de suivre cela à la main, mais connaître les étapes explique pourquoi certaines références se résolvent plus tôt que d'autres. - -- **Analyse (chargement de la configuration).** Un `@service` utilisé *comme entité* - ce qui crée un service, comme dans `Foo(@bar)` - devient immédiatement une référence. Un `@service` utilisé *comme argument* reste pour l'instant une simple chaîne. Un `@` entre guillemets est échappé en `@@` et compte donc comme du texte littéral, pas comme une référence. -- **Phase A (`loadConfiguration`).** Lors du traitement des définitions, un argument `@name` ou `@Type` propre est transformé en objet `Reference`. Cela ne capte que les formes simples ; `@service::CONST` ou un `@` au sein d'une expression plus large est laissé pour plus tard. -- **Phase B (`complete`).** La véritable traduction "intelligente" a lieu ici : `@service` -> référence, `@service::CONSTANT` -> une constante de classe littérale, `@service::property` -> la lecture de cette propriété, `@@x` -> le texte littéral `@x`. - -Une seconde traduction se cache dans le mot *référence* lui-même. Une `Reference` peut pointer soit par **nom**, soit par **type** (`@Namespace\Type`). Une référence par type **n'est pas encore un nom de service** : elle est résolue en un nom concret par l'autowiring, et cela ne se produit qu'à l'étape **complete**, une fois l'index de l'autowiring construit. C'est le pont vers la section suivante : les recherches d'autowiring sont délibérément repoussées jusqu'à ce que l'index soit prêt. - -| Forme | Devient une référence/expression en | Résolue en un service concret en -|---|---|--- -| entité (`@foo` comme factory) | analyse | complete -| argument `@foo`, `@Type` | phase A | complete -| `@foo::CONST`, `@foo::prop` | phase B | complete -| référence par type `@Type` | phase A/B | complete (autowiring) - - -Inspecter ContainerBuilder : quand est-ce sûr -============================================= - -Voici maintenant la question que les auteurs d'extensions posent le plus souvent : **dans quelle méthode puis-je rechercher les services par type ?** La réponse découle d'une règle simple sur la façon dont le builder suit son propre état. - -La recherche **par type** (`getByType()`, `getDefinitionByType()`, `findByType()`) exige que le graphe des services soit *résolu* : chaque type connu, l'index de l'autowiring construit. Aussi, dès que vous appelez l'une de ces méthodes alors que le graphe a changé depuis le dernier resolve, le builder **résout sur-le-champ tout le graphe connu**. Pendant le resolve lui-même, toute recherche par type est interdite et lève `NotAllowedDuringResolvingException`. - -La recherche **par tag** (`findByTag()`) n'a pas cette exigence - les tags ne dépendent pas des types, cela fonctionne donc dans **toutes les phases**. - -Phase par phase : - -- **`loadConfiguration()` (phase A) - la recherche par type n'est pas fiable.** Le graphe est incomplet : les extensions qui s'exécutent plus tard n'ont pas encore enregistré leurs services et, surtout, la section `services:` de l'utilisateur (qui passe en dernier) n'est pas là. Un appel à `getByType()` fonctionne bien - il déclenche un resolve prématuré d'un graphe partiel - mais la réponse provient d'une image incomplète, et ce resolve prématuré gaspille du travail. Règle empirique : **dans `loadConfiguration()`, contentez-vous d'enregistrer des définitions ; ne cherchez pas par type.** `findByTag()` ne pose pas de problème. -- **`beforeCompile()` (phase B) - le bon endroit pour l'introspection.** À ce stade, **toutes** les définitions existent (y compris celles de l'utilisateur), les **types sont résolus** et l'**index de l'autowiring est construit** ; `getByType()`, `findByType()` et `findByTag()` renvoient donc des réponses **fiables**. Les arguments ne sont *pas* encore autowirés - c'est justement l'étape suivante (`complete`), après tous les appels à `beforeCompile()`. Quand vous modifiez une définition ici, le `getByType()` suivant re-résout le graphe de façon transparente, vous pouvez donc alterner librement modifications et requêtes. -- **`afterCompile()` (phase C) - le code seulement.** Elle travaille sur la classe générée, pas sur le builder. Le graphe est terminé ; ici vous façonnez le PHP obtenu. - -| Je veux... | Phase -|---|--- -| enregistrer un service | `loadConfiguration()` -| chercher par **tag** et modifier des définitions | `loadConfiguration()` ou `beforeCompile()` -| chercher par **type** (`getByType`/`findByType`) | **`beforeCompile()`** -| dépendre des services que l'autowiring a choisis pour les arguments | pas à la compilation - inspectez-le à l'exécution -| toucher au code généré | `afterCompile()` -| exécuter du code après le démarrage du conteneur | [code d'initialisation |extensions#Code d'initialisation] - - -Dans la phase B : resolve et complete -===================================== - -La phase B se compose de deux passes, avec les appels à `beforeCompile()` intercalés entre elles : - -```php -$this->builder->resolve(); // types résolus, index de l'autowiring construit -foreach ($this->extensions as $extension) { - $extension->beforeCompile(); -} -$this->builder->complete(); // SEULEMENT MAINTENANT les arguments sont autowirés -``` - -**`resolve()`** détermine le type de chaque service - repris de son `type` déclaré, ou déduit de sa factory : le type de retour d'une méthode fabrique, la classe qu'elle instancie, ou le service vers lequel pointe une référence - puis construit l'index de l'autowiring, qui associe chaque type (la classe ainsi que ses parents et interfaces) à un nom de service. Un service marqué `autowired: false` est laissé hors de l'index ; `autowired: [A, B]` restreint les types sous lesquels il est visible. Point crucial : resolve fixe les *types*, pas les *arguments* - autowirer les arguments demanderait l'index terminé, qui n'existe qu'après cette passe. - -**`complete()`** est l'endroit où l'autowiring des arguments a réellement lieu. Pour chaque définition, il complète les arguments manquants du constructeur et du setup en cherchant leurs types dans l'index désormais complet. Voilà pourquoi les références par type sont restées non résolues pendant resolve : la recherche a sa place ici, une fois qu'il existe un index fiable où chercher. - - -Phase C : génération du code -============================ - -`generateCode()` confie le graphe terminé à `PhpGenerator`, qui produit une classe étendant `Container` avec une méthode `createServiceXxx()` par service, ainsi que les métadonnées précalculées `aliases`, `tags` et `wiring`. Chaque `Statement` devient du texte PHP (`new Foo(...)`, appels de méthodes, accès aux propriétés) et chaque `Reference` devient un appel `$this->getService(...)`. - -Les extensions bénéficient ensuite d'une dernière passe `afterCompile()` sur la classe générée - c'est là que sont émis, par exemple, les getters des paramètres statiques et dynamiques - ainsi que de la possibilité d'ajouter du [code d'initialisation |extensions#Code d'initialisation] exécuté à chaque requête. - - -Le déroulement en une image -=========================== - -``` -COMPILATION (une fois, dans le cache) -│ -├─ charger les fichiers de config NEON -> Statement/tableau ; fusion des fichiers -│ @ entre guillemets -> @@ ; entités -> Statement -│ -▼ Compiler::compile() -│ -├─ PHASE A processExtensions() -│ ├─ ParametersExtension (1re) ── %param% DÉVELOPPÉ dans toute la configuration -│ │ les dynamiques -> expression à l'exécution -│ ├─ ExtensionsExtension (1re) ── enregistre d'autres extensions -│ ├─ ...autres extensions... ── loadConfiguration() : enregistrer seulement des définitions -│ └─ ServicesExtension (DERNIÈRE) ── services: -> objets Definition -│ @name/@Type -> Reference -│ [graphe complet en nombre ; TYPES et ARGUMENTS pas encore ; recherche par type non fiable] -│ -├─ PHASE B processBeforeCompile() -│ ├─ builder.resolve() ── résoudre tous les types ; construire l'index d'autowiring -│ │ [types prêts ; index prêt] -│ ├─ beforeCompile() extensions ── getByType/findByType/findByTag SÛRS ici -│ │ (arguments pas encore autowirés) -│ └─ builder.complete() ── autowirer les ARGUMENTS ; finir la traduction des références -│ références par type -> noms de services -│ -└─ PHASE C generateCode() - ├─ PhpGenerator.generate() ── Statement -> PHP ; méthodes createServiceXxx() - ├─ afterCompile() extensions ── retoucher le code ; émettre les getters de paramètres - └─ toString() ── code PHP final -> cache - -──────────────────────────────────────────────────────────── - -EXÉCUTION (chaque requête) -│ -├─ new Container($dynamicParams) -├─ initialize() ── code de démarrage des extensions (session, en-têtes, validation) -└─ getService()/getByType() ── instances paresseuses depuis les métadonnées précalculées -``` - - -Idées fausses courantes -======================= - -- "Dans `loadConfiguration()`, je vais chercher les services par type." Non - le graphe est incomplet (la section `services:` de l'utilisateur s'exécute après vous) et `getByType()` déclenche un resolve prématuré d'un graphe partiel. Déplacez cela dans `beforeCompile()`. `findByTag()` ne pose pas de problème, même ici. -- "Une valeur `getenv()` dans un paramètre sera différente dans chaque environnement." Seulement si le paramètre est dynamique. Sinon, elle est figée à la compilation et reste la même partout. -- "Une référence `@Type` est déjà un nom de service." Non - c'est une référence par type, résolue en un nom concret par l'autowiring, et seulement à l'étape complete. -- "Mon extension lit un fichier auxiliaire, mais les changements n'apparaissent pas." Enregistrez-le avec `$builder->addDependency($file)`, sinon le cache n'est pas au courant et ne se reconstruira pas. -- "Pendant `resolve()`, je peux appeler `getByType()`." Non - cela lève `NotAllowedDuringResolvingException`. La recherche par type appartient à `beforeCompile()` ou plus tard, jamais au milieu du resolve. diff --git a/dependency-injection/fr/configuration.texy b/dependency-injection/fr/configuration.texy deleted file mode 100644 index 85dd8902fd..0000000000 --- a/dependency-injection/fr/configuration.texy +++ /dev/null @@ -1,335 +0,0 @@ -Configuration du conteneur DI -***************************** - -.[perex] -Aperçu des options de configuration du conteneur DI de Nette. - - -Fichier de configuration -======================== - -Le conteneur DI de Nette se pilote facilement à l'aide de fichiers de configuration. Ceux-ci s'écrivent habituellement au [format NEON|neon:format]. Nous vous recommandons d'utiliser des [éditeurs qui prennent en charge |tools:ide] ce format. - -<pre> -"decorator .[prism-token prism-atrule]":[#Decorator]: "Decorator .[prism-token prism-comment]"<br> -"di .[prism-token prism-atrule]":[#DI]: "Conteneur DI .[prism-token prism-comment]"<br> -"extensions .[prism-token prism-atrule]":[#Extensions]: "Installer d'autres extensions DI .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#Inclure des fichiers]: "Inclure des fichiers .[prism-token prism-comment]"<br> -"parameters .[prism-token prism-atrule]":[#Paramètres]: "Paramètres .[prism-token prism-comment]"<br> -"search .[prism-token prism-atrule]":[#Search]: "Enregistrement automatique des services .[prism-token prism-comment]"<br> -"services .[prism-token prism-atrule]":[services]: "Services .[prism-token prism-comment]" -</pre> - -.[note] -Pour écrire une chaîne contenant le caractère `%`, vous devez l'échapper en le doublant en `%%`. - - -Paramètres -========== - -Dans la configuration, vous pouvez définir des paramètres qui pourront ensuite servir dans les définitions de services. Cela vous permet de rendre la configuration plus lisible ou de centraliser les valeurs susceptibles de changer. - -```neon -parameters: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: secret -``` - -Nous faisons référence au paramètre `dsn` n'importe où dans la configuration par la notation `%dsn%`. Les paramètres peuvent aussi être utilisés à l'intérieur de chaînes, comme `'%wwwDir%/images'`. - -Les paramètres ne sont pas obligatoirement des chaînes ou des nombres, ils peuvent aussi contenir des tableaux : - -```neon -parameters: - mailer: - host: smtp.example.com - secure: ssl - user: franta@gmail.com - languages: [cs, en, de] -``` - -Nous faisons référence à une clé précise par `%mailer.user%`. - -Si votre code (par exemple une classe) a besoin de la valeur d'un paramètre, passez-la-lui. Par exemple dans le constructeur. Il n'existe pas d'objet de configuration global que les classes pourraient interroger pour connaître la valeur d'un paramètre. Ce serait une violation du principe de l'injection de dépendances. - - -Services -======== - -Voir le [chapitre distinct|services]. - - -Decorator -========= - -Comment modifier d'un coup plusieurs services d'un certain type ? Par exemple, comment appeler une méthode donnée sur tous les presenters qui héritent d'une classe de base précise ? C'est à cela que sert le decorator. - -```neon -decorator: - # pour tous les services qui sont des instances de cette classe ou interface - App\Presentation\BasePresenter: - setup: - - setProjectId(10) # appeler cette méthode - - $absoluteUrls = true # et définir la variable -``` - -Le decorator peut aussi servir à poser des [tags |services#Tags] ou à activer le [mode inject |services#Mode inject]. - -```neon -decorator: - InjectableInterface: - tags: [mytag: 1] - inject: true -``` - - -DI -=== - -Réglages techniques du conteneur DI. - -```neon -di: - # afficher le DIC dans la Tracy Bar ? - debugger: ... # (bool) détection automatique par défaut (activé si Tracy est présent) - - # types de paramètres que vous n'autowirez jamais - excluded: ... # (string[]) - - # activer la création lazy des services ? - lazy: ... # (bool) false par défaut - - # la classe dont le conteneur DI hérite - parentClass: ... # (string) Nette\DI\Container par défaut -``` - - -Services lazy .{data-version:3.2.4} ------------------------------------ - -Le réglage `lazy: true` active la création lazy (différée) des services. Cela signifie que les services ne sont pas réellement créés au moment où on les demande au conteneur DI, mais seulement lors de leur première utilisation. Cela peut accélérer le démarrage de l'application et réduire la consommation mémoire, car seuls les services réellement nécessaires à une requête donnée sont créés. - -Pour un service précis, la création lazy peut être [ajustée |services#Services lazy]. - -.[note] -Les objets lazy ne peuvent être utilisés que pour les classes définies par l'utilisateur, pas pour les classes internes de PHP. Nécessite PHP 8.4 ou plus récent. - - -Export des métadonnées ----------------------- - -La classe du conteneur DI contient aussi beaucoup de métadonnées. Vous pouvez réduire sa taille en réduisant l'export des métadonnées. - -```neon -di: - export: - # exporter les paramètres ? - parameters: false # (bool) true par défaut - - # exporter les tags, et lesquels ? - tags: # (string[]|bool) tous par défaut - - event.subscriber - - # exporter les données pour l'autowiring, et lesquelles ? - types: # (string[]|bool) toutes par défaut - - Nette\Database\Connection - - Symfony\Component\Console\Application -``` - -Si vous n'utilisez pas `$container->getParameters()`, vous pouvez désactiver l'export des paramètres. Vous pouvez en outre n'exporter que les tags dont vous vous servez réellement pour récupérer des services via `$container->findByTag(...)`. Si vous n'appelez pas du tout cette méthode, vous pouvez désactiver complètement l'export des tags avec `false`. - -Vous pouvez réduire nettement les métadonnées de l'[autowiring|autowiring] en n'indiquant que les classes que vous demandez réellement avec `$container->getByType()`. Là encore, si vous n'appelez pas cette méthode (ou seulement dans le fichier [bootstrap|application:bootstrapping], par exemple pour obtenir `Nette\Application\Application`), vous pouvez désactiver complètement l'export des types avec `false`. - - -Extensions -========== - -Enregistrement d'extensions DI supplémentaires. C'est ainsi que vous ajoutez, par exemple, l'extension DI `Dibi\Bridges\Nette\DibiExtension3` sous le nom `dibi` : - -```neon -extensions: - dibi: Dibi\Bridges\Nette\DibiExtension3 -``` - -Vous la configurez ensuite dans la section `dibi` : - -```neon -dibi: - host: localhost -``` - -Vous pouvez aussi ajouter comme extension une classe avec des paramètres : - -```neon -extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, [%appDir%], %tempDir%/cache) -``` - - -Inclure des fichiers -==================== - -D'autres fichiers de configuration peuvent être inclus dans la section `includes` : - -```neon -includes: - - parameters.php - - services.neon - - presenters.neon -``` - -Le nom `parameters.php` n'est pas une faute de frappe : la configuration peut aussi être écrite dans un fichier PHP qui la renvoie sous forme de tableau : - -```php -<?php -return [ - 'database' => [ - 'main' => [ - 'dsn' => 'sqlite::memory:', - ], - ], -]; -``` - -Si des éléments portant les mêmes clés apparaissent dans plusieurs fichiers de configuration, ils seront écrasés ou, dans le cas des tableaux, [fusionnés |#Fusion]. Un fichier inclus plus tard a une priorité plus élevée que le précédent. Le fichier dans lequel figure la section `includes` a une priorité plus élevée que les fichiers qui y sont inclus. - - -Search -====== - -L'enregistrement automatique des services dans le conteneur DI simplifie considérablement le développement. Nette ajoute automatiquement les presenters au conteneur, mais vous pouvez tout aussi facilement y ajouter n'importe quelles autres classes. - -Il suffit d'indiquer dans quels répertoires (et sous-répertoires) les classes doivent être recherchées : - -```neon -search: - - in: %appDir%/Forms - - in: %appDir%/Model -``` - -Si vous n'avez besoin que d'une seule règle de recherche, vous pouvez omettre la liste et écrire ses clés directement sous `search` : - -```neon -search: - in: %appDir% -``` - -Habituellement, nous ne voulons cependant pas ajouter absolument toutes les classes et interfaces, nous pouvons donc les filtrer : - -```neon -search: - - in: %appDir%/Forms - - # filtrage par nom de fichier (string|string[]) - files: - - *Factory.php - - # filtrage par nom de classe (string|string[]) - classes: - - *Factory -``` - -Ou nous pouvons sélectionner les classes qui héritent d'au moins une des classes listées ou qui en implémentent au moins une : - - -```neon -search: - - in: %appDir% - extends: - - App\*Form - implements: - - App\*FormInterface -``` - -Vous pouvez également définir des règles d'exclusion à l'aide de masques de noms de classes ou d'ancêtres. Si une classe correspond à une règle d'exclusion, elle ne sera pas ajoutée au conteneur DI : - -```neon -search: - - in: %appDir% - exclude: - files: ... - classes: ... - extends: ... - implements: ... -``` - -Des tags peuvent être attribués à tous les services enregistrés automatiquement : - -```neon -search: - - in: %appDir% - tags: ... -``` - -Outre les classes, la recherche enregistre aussi les interfaces qui ont une unique méthode `create()` ou `get()` - en tant que [factories ou accesseurs générés |factory]. Les classes pour lesquelles un service du même type est déjà enregistré dans le conteneur sont ignorées, aucun doublon n'est donc créé. - - -Fusion -====== - -Si des éléments portant les mêmes clés apparaissent dans plusieurs fichiers de configuration, ils seront écrasés ou, dans le cas des tableaux, fusionnés. Le fichier inclus plus tard a une priorité plus élevée que le précédent. - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>résultat</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> - <td> -```neon -items: - - 1 - - 2 - - 3 -``` - </td> -</tr> -</table> - -Pour les tableaux, la fusion peut être empêchée en ajoutant un point d'exclamation après le nom de la clé : - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>résultat</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items!: - - 3 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> -</tr> -</table> - -{{maintitle: Configuration de l'injection de dépendances}} diff --git a/dependency-injection/fr/container.texy b/dependency-injection/fr/container.texy deleted file mode 100644 index f8fb2dcd09..0000000000 --- a/dependency-injection/fr/container.texy +++ /dev/null @@ -1,142 +0,0 @@ -Qu'est-ce qu'un conteneur DI ? -****************************** - -.[perex] -Un conteneur d'injection de dépendances (DIC ou conteneur DI) est un objet chargé d'instancier et de configurer d'autres objets (appelés services). - -Cela va peut-être vous surprendre, mais dans bien des cas, vous n'avez pas besoin d'un conteneur d'injection de dépendances pour profiter des avantages de l'injection de dépendances (DI en abrégé). Après tout, même dans le [chapitre d'introduction|introduction], nous avons montré des exemples concrets de DI, et aucun conteneur n'était nécessaire. - -En revanche, dès qu'il faut gérer un grand nombre d'objets aux dépendances complexes, un conteneur DI devient très utile. C'est souvent le cas des applications web construites sur un framework. - -Dans le chapitre précédent, nous avons présenté les classes `Article` et `EditController`. Toutes deux ont des dépendances, à savoir la base de données et la factory `ArticleFactory`. Et c'est pour ces classes que nous allons maintenant créer un conteneur. Bien sûr, créer un conteneur pour un exemple aussi simple est exagéré. Mais nous allons en créer un pour montrer à quoi il ressemble et comment il fonctionne. - -Voici un conteneur simple, écrit en dur, pour l'exemple ci-dessus : - -```php -class Container -{ - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection('mysql:', 'root', '***'); - } - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->createDatabase()); - } - - public function createEditController(): EditController - { - return new EditController($this->createArticleFactory()); - } -} -``` - -L'utilisation ressemblerait à ceci : - -```php -$container = new Container; -$controller = $container->createEditController(); -``` - -Nous demandons simplement l'objet au conteneur, sans avoir besoin de savoir comment le créer ni quelles sont ses dépendances ; le conteneur s'occupe de tout. Les dépendances sont injectées automatiquement par le conteneur. C'est là sa force. - -Pour l'instant, le conteneur a toutes les informations écrites en dur. Passons donc à l'étape suivante et ajoutons des paramètres pour rendre le conteneur vraiment utile : - -```php -class Container -{ - public function __construct( - private array $parameters, - ) { - } - - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection( - $this->parameters['db.dsn'], - $this->parameters['db.user'], - $this->parameters['db.password'], - ); - } - - // ... -} - -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); -``` - -Les lecteurs attentifs remarqueront un problème. Chaque fois que nous récupérons un objet `EditController`, de nouvelles instances de `ArticleFactory` et de la connexion à la base de données sont également créées. Ce n'est vraiment pas ce que nous voulons. - -Nous allons donc ajouter une méthode `getService()` qui renverra toujours les mêmes instances : - -```php -class Container -{ - private array $services = []; - - public function __construct( - private array $parameters, - ) { - } - - public function getService(string $name): object - { - if (!isset($this->services[$name])) { - // getService('Database') appellera createDatabase() - $method = 'create' . $name; - $this->services[$name] = $this->$method(); - } - return $this->services[$name]; - } - - // ... -} -``` - -Au premier appel, par exemple `$container->getService('Database')`, elle appelle `createDatabase()` pour créer l'objet de base de données, le stocke dans le tableau `$services` et le renvoie. Lors des appels suivants, elle renvoie directement l'instance déjà stockée. - -Nous modifions aussi le reste du conteneur pour qu'il utilise `getService()` : - -```php -class Container -{ - // ... - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->getService('Database')); - } - - public function createEditController(): EditController - { - return new EditController($this->getService('ArticleFactory')); - } -} -``` - -Au passage, le terme service désigne n'importe quel objet géré par le conteneur. D'où le nom de la méthode `getService()`. - -C'est fait. Nous avons un conteneur DI pleinement fonctionnel ! Et nous pouvons l'utiliser : - -```php -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); - -$controller = $container->getService('EditController'); -$database = $container->getService('Database'); -``` - -Comme vous le voyez, écrire un DIC n'a rien de difficile. Il est bon de noter que les objets eux-mêmes ignorent qu'un conteneur les crée. Il est donc possible de créer ainsi n'importe quel objet PHP sans modifier son code source. - -Créer et maintenir à la main la classe du conteneur peut vite tourner au cauchemar. C'est pourquoi, dans le chapitre suivant, nous parlerons du [Nette DI Container|nette-container], qui sait se générer et se mettre à jour presque automatiquement. - - -{{maintitle: Qu'est-ce qu'un conteneur d'injection de dépendances ?}} diff --git a/dependency-injection/fr/extensions.texy b/dependency-injection/fr/extensions.texy deleted file mode 100644 index e6549a7b30..0000000000 --- a/dependency-injection/fr/extensions.texy +++ /dev/null @@ -1,443 +0,0 @@ -Créer des extensions pour Nette DI -********************************** - -.[perex] -Une extension est une classe qui s'accroche à la compilation du conteneur DI. Elle sait enregistrer des services par programme, valider sa propre section de configuration, modifier les services définis par d'autres et même retoucher le code généré du conteneur. Cette page vous apprend à en écrire une, ce qui se passe et quand, et à quoi faire attention. - -Les extensions sont la manière native dont les paquets s'intègrent à Nette : tous les paquets `nette/*` en utilisent, et les vôtres le peuvent aussi. Une extension typique fait une ou plusieurs de ces choses : - -- **elle intègre une bibliothèque** - elle enregistre ses services dans le conteneur et expose une section de configuration agréable et validée (c'est de là que viennent les sections `mail:` ou `database:`) -- **elle automatise l'enregistrement** - elle enregistre en boucle, ou selon une règle, de nombreux services similaires qu'il serait fastidieux d'énumérer dans `services:` -- **elle apporte des changements transversaux** - elle retrouve les services enregistrés par d'autres et les complète, par exemple en attachant un logger à chaque service portant un certain tag - -Pour le travail quotidien sur une application, vous en avez rarement besoin : la section [services |services] de la configuration couvre l'enregistrement et le câblage de vos classes. Tournez-vous vers une extension lorsque la configuration seule ne suffit plus. - -Une extension s'active dans la section `extensions`. C'est ainsi que vous ajoutez une extension représentée par la classe `BlogExtension` sous le nom `blog` : - -```neon -extensions: - blog: BlogExtension -``` - -Si son constructeur prend des arguments, passez-les directement ici : - -```neon -extensions: - blog: BlogExtension(%debugMode%) -``` - - -Comment fonctionne la compilation -================================= - -Pour écrire des extensions en confiance, vous devez savoir une chose essentielle : **quand votre code s'exécute.** Nette ne câble pas les services pendant le traitement des requêtes. Il *compile* le conteneur à l'avance : il lit tous les fichiers de configuration, laisse les extensions faire leur travail et génère une classe PHP optimisée qu'il stocke sur le disque. Chaque requête suivante ne fait plus que charger cette classe terminée. Le code de votre extension ne s'exécute donc que lorsque le conteneur est (re)construit, et non à chaque requête. - -Cela a une conséquence importante : pendant la compilation, aucun service n'existe encore. Ce qui existe, ce sont des **définitions** - des recettes qui décrivent la classe que sera chaque service, la façon de le créer et ce qu'il faudra lui appeler ensuite. Les définitions vivent dans l'objet [ContainerBuilder |#ContainerBuilder]. Une extension est essentiellement une *configuration scriptable* : tout ce que vous pouvez déclarer dans la section `services:`, vous pouvez aussi le construire en PHP - conditionnellement, dans des boucles ou en réaction à ce que d'autres ont enregistré. - -La compilation se déroule en phases, et une extension peut intervenir dans chacune d'elles : - -1) les sections de configuration de toutes les extensions sont validées (`getConfigSchema()`) -2) chaque extension enregistre ses services (`loadConfiguration()`) ; la section `services:` de l'utilisateur est traitée en dernier, l'application a donc toujours le dernier mot -3) une fois toutes les définitions en place et les types des services résolus, les extensions peuvent les modifier (`beforeCompile()`) -4) la classe du conteneur est générée ; les extensions peuvent encore en ajuster le code (`afterCompile()`) et émettre du code qui s'exécutera au démarrage de l'application ([initialisation |#Code d'initialisation]) - -.[note] -En mode développement, le conteneur est recompilé automatiquement dès que vous modifiez un fichier de configuration ou la classe de l'extension elle-même - les deux sont suivis comme dépendances. Vous pouvez donc développer des extensions sans jamais vider de cache. - -.[tip] -Pour un examen plus approfondi de ce qui se passe dans chaque phase - quand les paramètres sont développés, quand `@service` devient une référence et à quel moment exactement il est sûr de chercher les services par type - voir [La compilation du conteneur en détail |compilation-internals]. - - -Première extension -================== - -Voici une extension petite mais complète. Nous l'activons et la configurons dans le même fichier : - -```neon -extensions: - blog: BlogExtension - -blog: - postsPerPage: 5 -``` - -Et voici la classe entière : - -```php -use Nette\Schema\Expect; - -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function getConfigSchema(): Nette\Schema\Schema - { - return Expect::structure([ - 'postsPerPage' => Expect::int(10), - 'allowComments' => Expect::bool(true), - ]); - } - - - public function loadConfiguration(): void - { - $builder = $this->getContainerBuilder(); - - $builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class, ['postsPerPage' => $this->config->postsPerPage]); - - if ($this->config->allowComments) { - $builder->addDefinition($this->prefix('comments')) - ->setFactory(Blog\Comments::class); - } - } -} -``` - -`getConfigSchema()` décrit ce que la section `blog:` (nommée d'après la clé sous laquelle nous avons enregistré l'extension) peut contenir, types et valeurs par défaut compris - les valeurs validées sont ensuite disponibles dans `$this->config`. Dans `loadConfiguration()`, nous enregistrons les services. Notez les noms : `$this->prefix('articles')` produit `blog.articles`, les services de différentes extensions ne peuvent donc pas entrer en collision. - -Et les dernières lignes montrent pourquoi les extensions existent : le service `comments` n'est enregistré que si les commentaires sont activés. Un simple fichier de configuration ne peut pas prendre de telles décisions. - -Les services enregistrés de cette façon se comportent exactement comme s'ils étaient écrits dans `services:` - ils sont créés paresseusement à la demande, et l'autowiring les passe partout où `Blog\Articles` est déclaré comme type. - -Les chapitres suivants décrivent en détail le cycle de vie d'une extension, puis l'API du [ContainerBuilder |#ContainerBuilder] que vous utiliserez à l'intérieur de l'extension, et enfin les [pièges |#Conseils et pièges] qu'il vaut mieux connaître. - - -Cycle de vie d'une extension -============================ - -Une extension hérite de [api:Nette\DI\CompilerExtension] et redéfinit certaines des quatre méthodes `getConfigSchema()`, `loadConfiguration()`, `beforeCompile()` et `afterCompile()`, que le compilateur appelle dans cet ordre pendant la compilation. - - -getConfigSchema(): Nette\Schema\Schema .[method] ------------------------------------------------- - -Définit le schéma de la section de configuration de l'extension. Grâce à lui, les utilisateurs obtiennent gratuitement la validation et des messages d'erreur clairs : une faute de frappe ou un mauvais type dans la section `blog:` est signalé par un message compréhensible, sans que vous écriviez la moindre vérification. - -Le schéma se décrit à l'aide de la bibliothèque [Schema |schema:] et peut exprimer les types, les valeurs par défaut, les valeurs autorisées et bien plus encore : - -```php -public function getConfigSchema(): Nette\Schema\Schema -{ - return Expect::structure([ - 'postsPerPage' => Expect::int(10), - 'storage' => Expect::anyOf('files', 'database')->firstIsDefault(), - ]); -} -``` - -La configuration validée est disponible dans `$this->config` sous forme d'objet `stdClass` (ou de tableau, si vous ajoutez `castTo('array')` au schéma). - -Si la valeur d'une option ne peut pas être connue à la compilation - parce qu'elle vient par exemple d'une variable d'environnement - marquez-la avec `dynamic()`, par exemple `Expect::int()->dynamic()`. Plus d'informations dans [paramètres dynamiques |application:bootstrapping#Paramètres Dynamiques]. - - -loadConfiguration() .[method] ------------------------------ - -L'endroit où l'extension enregistre ses services, à l'aide du [ContainerBuilder |#ContainerBuilder] : - -```php -public function loadConfiguration(): void -{ - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class); -} -``` - -Si un service doit également être disponible sous un nom court, ajoutez un alias. Par convention, on ne le fait que lorsque l'extension est enregistrée sous son nom habituel, afin que plusieurs instances de l'extension ne se le disputent pas : - -```php -if ($this->name === 'blog') { - $builder->addAlias('articles', $this->prefix('articles')); -} -``` - -Quand les services sont nombreux, il peut être plus commode de les définir dans un fichier NEON séparé avec la syntaxe familière des [services |services]. Le préfixe `@extension` fait référence à l'extension courante : - -```neon -services: - articles: - create: MyBlog\ArticlesModel(@connection) - - comments: - create: MyBlog\CommentsModel(@connection, @extension.articles) -``` - -Nous chargeons ces définitions avec `loadDefinitionsFromConfig()` ; les noms reçoivent automatiquement le préfixe et le fichier est suivi comme dépendance, si bien que le modifier déclenche une recompilation : - -```php -public function loadConfiguration(): void -{ - $this->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/services.neon')['services'], - ); -} -``` - - -beforeCompile() .[method] -------------------------- - -Lorsque cette méthode est appelée, le builder contient déjà **toutes** les définitions : les vôtres, celles des autres extensions et celles des fichiers de configuration de l'utilisateur. Les types des services ont eux aussi été résolus, la recherche par type est donc fiable. Cette phase est de ce fait idéale pour inspecter et compléter le graphe final des services. - -Typiquement, vous cherchez les services par tag ou par type et complétez les définitions trouvées : - -```php -public function beforeCompile(): void -{ - $builder = $this->getContainerBuilder(); - - foreach ($builder->findByTag('logaware') as $name => $attrs) { - $builder->getDefinition($name)->addSetup('setLogger'); - } -} -``` - -L'appel `setLogger()` n'a pas d'arguments explicites - l'autowiring les fournira, comme il le fait dans les factories. - -Vous pouvez aussi coopérer avec les autres extensions enregistrées, obtenues via `$this->compiler->getExtensions()`, éventuellement filtrées par classe ou interface : - -```php -foreach ($this->compiler->getExtensions(FooExtension::class) as $extension) { - // ... -} -``` - - -afterCompile(Nette\PhpGenerator\ClassType $class) .[method] ------------------------------------------------------------ - -Dans la dernière phase, la classe du conteneur est générée sous forme d'objet [ClassType |php-generator:#Classes] de la bibliothèque [PHP Generator |php-generator:]. Elle contient une méthode fabrique pour chaque service et s'apprête à être écrite dans le cache. Vous pouvez encore en modifier le code : - -```php -public function afterCompile(Nette\PhpGenerator\ClassType $class): void -{ - $method = $class->getMethod('__construct'); - // ... -} -``` - -Vous n'aurez besoin de cette phase que rarement. Pour ajouter du code qui s'exécute au démarrage de l'application, utilisez plutôt l'initialisation : - - -Code d'initialisation ---------------------- - -Toutes les phases précédentes influencent la façon dont le conteneur est *construit*. Une extension peut en outre émettre du code qui s'exécute à l'*exécution*, juste après la création du conteneur - par exemple pour démarrer une session ou lancer des services. Le code s'écrit dans l'objet `$this->initialization` à l'aide de sa méthode [addBody() |php-generator:#Corps des méthodes et des fonctions] : - -```php -public function loadConfiguration(): void -{ - // les services portant le tag 'run' doivent être créés juste après le démarrage du conteneur - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $attrs) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } -} -``` - -Nette utilise lui-même l'initialisation, par exemple pour démarrer automatiquement la session ou envoyer les en-têtes HTTP de sécurité. Et gardez à l'esprit que, contrairement à tout le reste dans une extension, ce code s'exécute à **chaque requête** : gardez-le donc réduit. - - -ContainerBuilder -================ - -[api:Nette\DI\ContainerBuilder] est l'objet par lequel une extension dialogue avec le compilateur. Il contient les [définitions |#Comment fonctionne la compilation] de tous les services et offre des méthodes pour les ajouter, les retrouver et les modifier. Vous l'obtenez dans `loadConfiguration()` et `beforeCompile()` : - -```php -$builder = $this->getContainerBuilder(); -``` - - -Ajouter des services --------------------- - -Enregistrer un service, c'est la même chose que ce que vous faites dans la section `services:` d'un fichier NEON - écrit en PHP. Chaque clé de configuration a sa méthode correspondante sur la définition, ces deux notations sont donc équivalentes : - -```neon -services: - articles: - create: Blog\Articles(@connection) - setup: - - setLogger(@logger) - tags: [logaware] -``` - -```php -$builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class, ['@connection']) - ->addSetup('setLogger', ['@logger']) - ->addTag('logaware'); -``` - -La définition renvoyée par `addDefinition()` est une [ServiceDefinition |#Types de définitions] qui offre les équivalents des clés de configuration : `setType()` (la classe du service), `setFactory()` (comment le créer), `setArguments()`, `addSetup()`, `addTag()` et `setAutowired()`. - -`addSetup()` reflète la liste `setup:` et accepte les mêmes formes : un appel de méthode `addSetup('setLogger', ['@logger'])`, une affectation de propriété `addSetup('$cache', ['@cache'])` ou un appel sur un autre service `addSetup('@Tracy\Bar::addPanel', [$panel])`. - -Outre les services ordinaires, le builder sait aussi enregistrer des [factories |factory] générées, des accesseurs et des locators - chacun avec sa propre méthode qui renvoie le [type de définition |#Types de définitions] correspondant : - -| Méthode | Enregistre -|--------|---------- -| `addDefinition()` | un service ordinaire (renvoie `ServiceDefinition`) -| `addFactoryDefinition()` | une [factory |factory] générée (interface avec une méthode `create()`) -| `addAccessorDefinition()` | un [accesseur |factory#Accessor] généré (interface avec une méthode `get()`) -| `addLocatorDefinition()` | une [multifactory / locator |factory#Multifactory/Accessor] combinant plusieurs factories -| `addImportedDefinition()` | un service passé au conteneur depuis l'extérieur à l'exécution -| `addAlias()` | un second nom pour un service existant - -Avec une factory, vous configurez l'objet qu'elle crée via `getResultDefinition()` ; un accesseur, lui, pointe vers un service existant via `setReference()` : - -```php -$builder->addFactoryDefinition($this->prefix('latteFactory')) - ->setImplement(LatteFactory::class) - ->getResultDefinition() - ->setFactory(Latte\Engine::class) - ->addSetup('setStrictTypes', [true]); -``` - -`addLocatorDefinition()` et `addImportedDefinition()` sont rarement nécessaires - de tels services proviennent généralement des clés `implement:` et des services importés en NEON, plutôt que d'être écrits à la main. - - -Trouver et modifier des services --------------------------------- - -Pour rechercher et parcourir les définitions existantes, le builder fournit : - -| Méthode | Description -|--------|------------ -| `getDefinition(string $name)` | la définition portant le nom donné (lève une exception si elle manque) -| `hasDefinition(string $name)` | indique si une définition ou un alias de ce nom existe -| `getDefinitions()` | toutes les définitions -| `removeDefinition(string $name)` | supprime une définition -| `getByType(string $type)` | le nom du service autowiré de ce type, ou `null` -| `getDefinitionByType(string $type)` | la définition autowirée de ce type -| `findByType(string $type)` | toutes les définitions de ce type sous forme de paires `nom => définition` -| `findByTag(string $tag)` | les services portant le tag sous forme de paires `nom => valeur du tag` -| `addExcludedClasses(array $types)` | exclut des classes et interfaces de l'autowiring - -Un idiome pratique consiste à utiliser `getByType()` pour savoir si un service existe seulement - par exemple pour se raccrocher à un logger uniquement si l'application en a un : - -```php -if ($builder->getByType(Psr\Log\LoggerInterface::class)) { - $builder->getDefinition($this->prefix('articles')) - ->addSetup('setLogger'); -} -``` - - -Types de définitions --------------------- - -Chaque méthode `add*Definition()` renvoie un genre de définition différent. Toutes étendent l'ancêtre commun `Nette\DI\Definitions\Definition` : - -- **`ServiceDefinition`** - un service ordinaire ; se configure avec `setType()`, `setFactory()`, `addSetup()`, `addTag()` et `setAutowired()` -- **`FactoryDefinition`** - une [factory générée |factory] : une interface dont la méthode `create()` renvoie un nouvel objet à chaque appel -- **`AccessorDefinition`** - un [accesseur généré |factory#Accessor] : une interface dont la méthode `get()` renvoie un service existant -- **`LocatorDefinition`** - une [multifactory / locator |factory#Multifactory/Accessor] combinant plusieurs factories ou accesseurs dans une seule interface -- **`ImportedDefinition`** - un service que le conteneur ne crée pas lui-même, mais reçoit de l'extérieur à l'exécution - -Gardez à l'esprit que `getDefinition()` renvoie le genre de définition qui vit sous le nom donné, quel qu'il soit. Si votre code peut tomber sur une factory générée, vérifiez d'abord le type et configurez l'objet produit via `getResultDefinition()` : - -```php -$def = $builder->getDefinition($name); -if ($def instanceof Nette\DI\Definitions\FactoryDefinition) { - $def = $def->getResultDefinition(); -} -$def->addSetup('setLogger'); -``` - - -Conseils et pièges -================== - - -Compilation ou exécution ------------------------- - -La source de confusion la plus fréquente : le code d'une extension s'exécute pendant la **compilation** du conteneur, pas pendant le traitement des requêtes. En pratique, cela signifie que : - -- Une extension ne travaille jamais avec des instances de services - elles n'existent pas encore. N'instanciez pas de services avec `new` ; enregistrez une définition et laissez le conteneur les créer. -- Toutes les valeurs de configuration sont figées dans le code généré. Une valeur qui peut différer d'un environnement à l'autre (un chemin, un mot de passe issu de `getenv()`) doit être marquée comme [dynamique |application:bootstrapping#Paramètres Dynamiques], sinon elle est figée à la compilation. -- Les chaînes passées à `$this->initialization->addBody()` ne sont pas exécutées maintenant - c'est du code PHP émis dans le conteneur, exécuté à chaque requête. - - -Dépendances de fichiers ------------------------ - -Le conteneur est recompilé lorsque les fichiers de configuration ou les classes d'extensions changent. Mais si votre extension lit un autre fichier - une liste d'entités, la configuration XML d'une bibliothèque - le conteneur n'a aucun moyen de le savoir. Enregistrez de tels fichiers avec : - -```php -$builder->addDependency($file); -``` - -Sinon, vous vous exposez à un mystère classique : vous modifiez le fichier, mais l'application continue de se comporter comme avant - le changement n'apparaît qu'une fois le conteneur reconstruit pour une autre raison. (Les fichiers lus via `loadFromFile()` sont suivis automatiquement.) - - -Enregistrement conditionnel ---------------------------- - -Une extension peut s'adapter à son environnement. Les intégrations facultatives sont typiquement protégées par `class_exists()` : - -```php -if (class_exists(Symfony\Component\Console\Command\Command::class)) { - $builder->addDefinition($this->prefix('command')) - ->setFactory(Blog\Console\SitemapCommand::class); -} -``` - -Et il vaut mieux passer les valeurs comme `%debugMode%` par le constructeur de l'extension : - -```neon -extensions: - blog: BlogExtension(%debugMode%) -``` - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function __construct( - private bool $debugMode = false, - ) {} -} -``` - -Un usage typique est d'enregistrer un panneau Tracy uniquement en mode développement. - - -Arguments complexes -------------------- - -Parfois, un argument d'une factory ou d'un appel de setup n'est ni une valeur simple, ni un nom de classe, ni une référence `@service`. Pour ces cas-là, il y a : - -- `new Nette\DI\Definitions\Statement(Blog\Panel::class, [$args])` - un objet créé sur place, un "service anonyme" utilisé comme argument -- `new Nette\DI\Definitions\Reference('blog.articles')` - une référence vers un service, l'équivalent objet de la chaîne `@name` -- `$builder::literal('PHP_SAPI')` - un morceau de code PHP brut inséré tel quel dans le conteneur généré - -Exemple - enregistrement d'un panneau Tracy : - -```php -$builder->getDefinition($this->prefix('articles')) - ->addSetup('@Tracy\Bar::addPanel', [ - new Nette\DI\Definitions\Statement(Blog\ArticlesPanel::class), - ]); -``` - - -Tags et types exportés ----------------------- - -L'[export des métadonnées |configuration#Export des métadonnées] peut être restreint dans la configuration, de sorte que le conteneur compilé ne conserve que les tags et les types d'autowiring que l'application utilise réellement. Si votre extension récupère des services à l'exécution avec `$container->findByTag()` ou `$container->getByType()`, une telle restriction pourrait supprimer précisément les métadonnées sur lesquelles vous comptez. - -Pour l'éviter, indiquez au compilateur quels tags et types doivent toujours être exportés : - -```php -public function loadConfiguration(): void -{ - // ce tag sera toujours exporté, même si l'export est restreint - $this->compiler->addExportedTag('event.subscriber'); - - // ce type sera toujours disponible pour getByType() - $this->compiler->addExportedType(Nette\Database\Connection::class); -} -``` - -Ces deux méthodes ne font qu'ajouter aux métadonnées exportées ; elles ne remplacent jamais la configuration `di › export` de l'application. Ainsi, lorsque l'application restreint l'export à une liste, les tags et types dont votre extension a besoin y restent inclus ; seule la désactivation complète de l'export des tags (`tags: false`) les écarte avec tout le reste. diff --git a/dependency-injection/fr/factory.texy b/dependency-injection/fr/factory.texy deleted file mode 100644 index 236b129092..0000000000 --- a/dependency-injection/fr/factory.texy +++ /dev/null @@ -1,233 +0,0 @@ -Factories générées -****************** - -.[perex] -Nette DI sait générer automatiquement le code des factories à partir d'interfaces, ce qui vous évite d'écrire du code. - -Une factory est une classe chargée de créer des objets et de leur passer leurs dépendances. Ne la confondez pas avec le patron de conception *factory method*, qui décrit une façon particulière d'utiliser les factories et n'a rien à voir avec ce sujet. - -Nous avons montré à quoi ressemble une telle factory dans le [chapitre d'introduction |introduction#Factory] : - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Nette DI sait générer automatiquement le code d'une factory. Il vous suffit de créer une interface et Nette DI en générera l'implémentation. L'interface doit avoir exactement une méthode nommée `create` et déclarer un type de retour : - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Ainsi, la factory `ArticleFactory` possède une méthode `create` qui crée des objets `Article`. La classe `Article` peut par exemple ressembler à ceci : - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } -} -``` - -Ajoutez la factory au fichier de configuration : - -```neon -services: - - ArticleFactory -``` - -Nette DI générera l'implémentation correspondante de la factory. - -Dans le code qui utilise la factory, demandez l'objet par son interface et Nette DI vous fournira l'implémentation générée : - -```php -class UserController -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function foo() - { - // laissons la factory créer un objet - $article = $this->articleFactory->create(); - } -} -``` - - -Factory paramétrée -================== - -La méthode `create` de la factory peut accepter des paramètres, qu'elle passe ensuite au constructeur. Ajoutons par exemple l'identifiant de l'auteur de l'article à la classe `Article` : - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - private int $authorId, - ) { - } -} -``` - -Nous ajoutons également le paramètre à la factory : - -```php -interface ArticleFactory -{ - function create(int $authorId): Article; -} -``` - -Comme le nom du paramètre dans le constructeur (`$authorId`) correspond au nom du paramètre de la méthode de la factory, Nette DI le passe automatiquement. - - -Définition avancée -================== - -La définition peut aussi s'écrire sous forme multiligne à l'aide de la clé `implement` : - -```neon -services: - articleFactory: - implement: ArticleFactory -``` - -Ce format plus long permet d'indiquer des arguments supplémentaires pour le constructeur via la clé `arguments` et de poursuivre la configuration avec `setup`, comme pour les définitions de services ordinaires. - -Exemple : si la méthode `create()` n'acceptait pas le paramètre `$authorId`, nous pourrions fournir dans la configuration une valeur fixe à passer au constructeur d'`Article` : - -```neon -services: - articleFactory: - implement: ArticleFactory - arguments: - authorId: 123 -``` - -À l'inverse, si `create()` acceptait `$authorId` mais que celui-ci ne faisait pas partie du constructeur et était passé par une méthode comme `Article::setAuthorId()`, nous référencerions le paramètre dans la section `setup` : - -```neon -services: - articleFactory: - implement: ArticleFactory - setup: - - setAuthorId($authorId) -``` - - -Accessor -======== - -Outre les factories, Nette sait aussi générer ce qu'on appelle des accesseurs. Ce sont des objets dotés d'une méthode `get()` qui renvoie un service précis du conteneur DI. Les appels répétés à `get()` renvoient toujours la même instance. - -Les accesseurs assurent le chargement paresseux des dépendances. Imaginez une classe qui journalise les erreurs dans une base de données dédiée. Si cette classe recevait la connexion à la base de données par injection dans le constructeur, la connexion serait toujours établie, même si les erreurs sont rares et que la connexion reste inutilisée la plupart du temps. À la place, la classe peut recevoir un accesseur. L'objet de base de données (la connexion) n'est créé qu'au premier appel de la méthode `get()` de l'accesseur. - -Comment créer un accesseur ? Écrivez simplement une interface et Nette DI en générera l'implémentation. L'interface doit avoir exactement une méthode nommée `get`, sans paramètre, et déclarer le type de retour : - -```php -interface PDOAccessor -{ - function get(): PDO; -} -``` - -Ajoutez l'accesseur au fichier de configuration, avec la définition du service qu'il doit renvoyer : - -```neon -services: - - PDOAccessor - - PDO(%dsn%, %user%, %password%) -``` - -Comme l'accesseur renvoie un service `PDO` et qu'un seul service de ce genre est défini dans la configuration, l'accesseur renverra ce service précis. Si plusieurs services de ce type existent, indiquez par son nom celui que l'accesseur doit renvoyer, par exemple `- PDOAccessor(@db1)`. - - -Multifactory/Accessor -===================== - -Jusqu'ici, nos factories et accesseurs ne pouvaient créer ou renvoyer qu'un seul type d'objet. Vous pouvez cependant créer facilement des multifactories, qui combinent les propriétés des factories et des accesseurs. L'interface d'un tel composant peut contenir plusieurs méthodes nommées `create<Nom>()` et `get<Nom>()`, par exemple : - -```php -interface MultiFactory -{ - function createArticle(): Article; - function getDb(): PDO; -} -``` - -Ainsi, au lieu d'injecter plusieurs factories et accesseurs distincts, vous pouvez injecter un seul composant plus complet. - -Autre possibilité : au lieu de plusieurs méthodes, utiliser `get()` avec un paramètre : - -```php -interface MultiFactoryAlt -{ - function get($name): PDO; -} -``` - -Alors `MultiFactory::getDb()` fait la même chose que `MultiFactoryAlt::get('db')`. Cette notation alternative a toutefois l'inconvénient que les valeurs acceptées pour `$name` ne ressortent pas explicitement de la signature de l'interface. De plus, vous ne pouvez pas définir dans l'interface des types de retour différents selon la valeur de `$name`. - -Au lieu de `get($name)`, l'interface peut déclarer `create($name)`, qui renvoie une nouvelle instance à chaque appel (alors que `get()` en renvoie une partagée). L'interface ne peut contenir qu'une seule méthode paramétrée de ce genre. Si le type de retour de la méthode est nullable (par exemple `?PDO`), elle renvoie `null` pour un `$name` inconnu au lieu de lever une exception. - - -Définition à l'aide d'une liste -------------------------------- -Vous pouvez définir une multifactory dans la configuration à l'aide d'une liste, les services étant écrits directement dedans : .{data-version:3.2.0} - -```neon -services: - - MultiFactory( - article: Article() # définit createArticle() - db: PDO(%dsn%, %user%, %password%) # définit getDb() - ) -``` - -Vous pouvez aussi renvoyer, dans la définition de la multifactory, vers des services existants à l'aide de références : - -```neon -services: - article: Article - - PDO(%dsn%, %user%, %password%) - - MultiFactory( - article: @article # définit createArticle() - db: @\PDO # définit getDb() - ) -``` - - -Définition à l'aide de tags ---------------------------- - -Une autre façon de définir une multifactory est d'utiliser les [tags |services#Tags]. La valeur du tag détermine le nom de la méthode correspondante : - -```neon -services: - article: - create: Article - tags: {multi: article} # définit createArticle() - db: - create: PDO(%dsn%, %user%, %password%) - tags: {multi: db} # définit getDb() - - - MultiFactory(tagged: multi) -``` diff --git a/dependency-injection/fr/faq.texy b/dependency-injection/fr/faq.texy deleted file mode 100644 index a49533872d..0000000000 --- a/dependency-injection/fr/faq.texy +++ /dev/null @@ -1,123 +0,0 @@ -Questions fréquentes sur la DI (FAQ) -************************************ - - -La DI est-elle un autre nom pour l'IoC ? ----------------------------------------- - -L'*Inversion of Control* (IoC) est un principe qui décrit le flux de contrôle dans un programme : est-ce votre code qui appelle du code externe, ou est-ce du code externe (comme un framework) qui appelle votre code ? L'IoC est un concept large qui englobe les [événements |nette:glossary#Événements], le fameux [principe d'Hollywood |application:components#Style Hollywood] et d'autres aspects. Ce concept comprend aussi les factories, dont parle la [Règle n° 3 : laissez faire la factory |introduction#Règle n° 3 : laissez faire la factory], qui représentent une inversion de l'opérateur `new`. - -L'*injection de dépendances* (DI) s'intéresse à la façon dont les objets obtiennent leurs dépendances (c'est-à-dire les autres objets dont ils ont besoin pour travailler). C'est un patron de conception qui prône de passer les dépendances aux objets de manière explicite, plutôt que de laisser les objets les créer ou les chercher. - -La DI peut donc être considérée comme une forme particulière d'IoC. Toutes les formes d'IoC ne favorisent cependant pas un code propre. Font par exemple partie des anti-patterns les techniques reposant sur l'[état global|global-state] ou sur le patron [Service Locator |#Qu'est-ce qu'un Service Locator ?]. - - -Qu'est-ce qu'un Service Locator ? ---------------------------------- - -C'est une approche alternative à l'injection de dépendances. Elle repose sur un objet central (le locator) où sont enregistrés tous les services (dépendances) disponibles. Lorsqu'un objet a besoin d'une dépendance, il la demande au Service Locator. - -Comparé à la DI, il manque toutefois de transparence. Les dépendances sont cachées dans le code de l'objet (les appels au locator) au lieu d'être explicites dans son API (constructeur ou méthodes), et il faut donc inspecter le code pour comprendre les liens. Les tests sont eux aussi plus compliqués : vous ne pouvez pas simplement passer des dépendances simulées lors de l'instanciation d'un objet, il faut souvent manipuler le Service Locator lui-même. De plus, le Service Locator introduit une dépendance inutile : les objets deviennent liés au locator, contrairement à la DI, où les objets ignorent idéalement l'existence du conteneur. - - -Quand vaut-il mieux ne pas utiliser la DI ? -------------------------------------------- - -Il n'existe pas d'inconvénient notable connu à utiliser correctement le patron de conception de l'injection de dépendances. Au contraire, obtenir les dépendances depuis des emplacements accessibles globalement (comme des propriétés statiques ou des singletons) entraîne [de nombreuses complications|global-state], tout comme l'utilisation d'un Service Locator. Utiliser la DI est donc pratiquement toujours recommandé. Ce n'est pas un dogme : simplement, aucune meilleure alternative pour gérer proprement les dépendances ne s'est imposée. - -Il existe cependant des situations particulières et limitées où un accès global aux objets peut être acceptable. Par exemple pendant le débogage, quand vous avez besoin de dumper la valeur d'une variable, de mesurer un temps d'exécution ou de journaliser un message à un endroit précis. Dans ces cas, qui concernent des actions temporaires qui seront ensuite retirées du code, l'utilisation d'un dumper, d'un chronomètre ou d'un logger accessible globalement peut être légitime. Ces outils ne font pas partie de la conception même de l'application. - - -L'utilisation de la DI a-t-elle des inconvénients ? ---------------------------------------------------- - -L'injection de dépendances apporte-t-elle des désavantages, comme un effort d'écriture accru ou des performances réduites ? Que perdons-nous en commençant à écrire du code conforme à la DI ? - -La DI elle-même a un impact négligeable sur les performances d'exécution ou sur la consommation mémoire. Les performances du conteneur DI peuvent jouer un rôle, mais [Nette DI |nette-container] compile le conteneur en simple code PHP, si bien que la surcharge pendant l'exécution de l'application est pratiquement nulle. - -Quand vous écrivez du code selon les principes de la DI, vous devez souvent créer des constructeurs qui reçoivent les dépendances. Ce qui pouvait sembler fastidieux autrefois est devenu très rapide grâce aux IDE modernes et à des fonctionnalités comme la [promotion des propriétés du constructeur |https://blog.nette.org/fr/php-8-0-complete-overview-of-news#toc-constructor-property-promotion] de PHP 8. Les factories peuvent souvent être générées automatiquement par Nette DI, ce qui réduit encore le code répétitif. En contrepartie, vous n'avez plus à écrire de singletons ni d'accesseurs statiques. - -Globalement, une application bien conçue utilisant la DI n'est ni sensiblement plus courte ni sensiblement plus longue qu'une application reposant sur des singletons ou sur un accès global. Le code lié à la création et au câblage des dépendances est simplement déplacé des différentes classes vers des endroits dédiés : la configuration du conteneur DI et les factories. - - -Comment réécrire une application legacy en DI ? ------------------------------------------------ - -La migration d'une application legacy vers l'injection de dépendances peut être un processus exigeant, en particulier pour les applications volumineuses et complexes. Il est important d'aborder ce processus de manière systématique. - -- Lors du passage à l'injection de dépendances, il est important que tous les membres de l'équipe comprennent les principes et les pratiques employés. -- Analysez d'abord l'application existante pour identifier les composants clés et leurs dépendances. Établissez un plan indiquant quelles parties seront refactorisées et dans quel ordre. -- Implémentez un conteneur DI ou, mieux encore, utilisez une bibliothèque existante comme Nette DI. -- Refactorisez progressivement les parties de l'application pour qu'elles utilisent l'injection de dépendances. Cela peut impliquer de modifier des constructeurs ou des méthodes afin qu'ils reçoivent les dépendances en paramètres. -- Mettez à jour le code où les objets sont instanciés pour les récupérer depuis le conteneur ou utiliser les factories fournies par celui-ci. - -N'oubliez pas que le passage à l'injection de dépendances est un investissement dans la qualité du code et dans la maintenabilité à long terme de l'application. Même s'il peut être difficile d'opérer ces changements, le résultat devrait être un code plus propre, plus modulaire et facilement testable, prêt pour de futures extensions et pour la maintenance. - - -Pourquoi la composition est-elle préférée à l'héritage ? --------------------------------------------------------- -L'utilisation de la [composition |nette:introduction-to-object-oriented-programming#Composition] est généralement préférée à l'[héritage |nette:introduction-to-object-oriented-programming#Héritage] pour réutiliser du code, car elle conduit à un couplage plus lâche. Avec la composition, vous risquez moins de rencontrer des problèmes où la modification d'une classe de base casse les sous-classes qui en dépendent. Un exemple typique est la situation appelée [constructor hell |passing-dependencies#Constructor hell]. - - -Peut-on utiliser Nette DI Container en dehors de Nette ? --------------------------------------------------------- - -Absolument. Nette DI Container fait partie de Nette, mais il est conçu comme une bibliothèque autonome, utilisable indépendamment des autres parties du framework. Il suffit de l'installer avec Composer, de créer un fichier de configuration définissant vos services, puis d'écrire quelques lignes de code PHP pour créer le conteneur DI. Et vous pouvez immédiatement commencer à profiter de l'injection de dépendances dans vos projets. - -Le chapitre [Nette DI Container |nette-container] décrit un cas d'utilisation concret avec des exemples de code. - - -Pourquoi la configuration est-elle dans des fichiers NEON ? ------------------------------------------------------------ - -NEON est un langage de configuration simple et facilement lisible, développé au sein de Nette pour configurer les applications, les services et leurs dépendances. Comparé à JSON ou YAML, il offre pour cet usage des possibilités bien plus intuitives et souples. En NEON, vous pouvez décrire naturellement des définitions de services et des relations qu'il serait difficile, voire impossible, d'exprimer aussi clairement en JSON ou en YAML. - - -L'analyse des fichiers NEON ralentit-elle l'application ? ---------------------------------------------------------- - -Même si les fichiers NEON s'analysent très rapidement, leur vitesse d'analyse n'a en pratique aucune importance en production. En effet, les fichiers de configuration ne sont analysés qu'une seule fois, au premier lancement de l'application (ou lorsqu'ils changent). Après l'analyse, le code du conteneur DI est généré, mis en cache (stocké sur disque), et c'est ce code PHP compilé qui est exécuté à chaque requête suivante, sans qu'aucune analyse supplémentaire soit nécessaire. - -C'est ainsi que cela se passe en environnement de production. Pendant le développement, les fichiers NEON sont analysés chaque fois que leur contenu change, ce qui garantit au développeur un conteneur DI toujours à jour. Comme nous l'avons dit, l'analyse elle-même est très rapide. - - -Comment accéder dans ma classe aux paramètres du fichier de configuration ? ---------------------------------------------------------------------------- - -Gardez à l'esprit la [Règle n° 1 : laissez-vous les passer |introduction#Règle n° 1 : laissez-vous les passer]. Si une classe a besoin d'une information du fichier de configuration, n'essayez pas de trouver comment la classe pourrait *aller la chercher*. Demandez-la simplement, par exemple via le constructeur de la classe. Puis fournissez cette valeur dans le fichier de configuration. - -Dans cet exemple, `%myParameter%` est un placeholder pour la valeur du paramètre `myParameter`, qui sera passée au constructeur de `MyClass` : - -```neon -# config.neon -parameters: - myParameter: Some value - -services: - - MyClass(%myParameter%) -``` - -Si vous voulez passer plusieurs paramètres ou utiliser l'autowiring, il est utile d'[encapsuler les paramètres dans un objet |best-practices:passing-settings-to-presenters]. - - -Nette prend-il en charge l'interface Container de PSR-11 ? ----------------------------------------------------------- - -[Nette DI Container |api:Nette\DI\Container] ne prend pas en charge PSR-11 directement. Cependant, si vous avez besoin d'interopérabilité entre Nette DI Container et des bibliothèques ou frameworks qui attendent l'interface Container de PSR-11, vous pouvez créer un [adaptateur simple |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f] qui servira de pont entre Nette DI Container et PSR-11. - - -Que signifient les termes conteneur, compiler, définition, etc. ? ------------------------------------------------------------------ - -Un petit lexique des mots qui reviennent sans cesse autour de Nette DI, la plupart lors de l'[écriture d'extensions |extensions] : - -- **Container** (conteneur) - l'objet compilé (`Nette\DI\Container`) qui crée les services à la demande et les conserve à l'exécution. Il est généré une seule fois sous forme de code PHP optimisé. -- **Compiler** - la machinerie qui transforme les fichiers de configuration et les extensions en cette classe de conteneur. -- **ContainerBuilder** - le modèle modifiable du conteneur utilisé pendant la compilation ; il contient les définitions de services avant qu'aucun service réel n'existe. Voir [Créer des extensions |extensions#ContainerBuilder]. -- **Service** - un objet géré par le conteneur, généralement créé une seule fois et partagé (un singleton) - une connexion à la base de données, un mailer, un logger. -- **Definition** (définition) - la recette d'un service : son type, la façon de le créer et ce qu'il faut faire ensuite. Nette transforme les définitions en méthodes fabriques du conteneur ; il en existe plusieurs sortes (voir [types de définitions |extensions#Types de définitions]). -- **Type** - la classe ou l'interface d'un service, utilisée par l'autowiring pour associer les services aux endroits qui les réclament. -- **Autowiring** - le passage automatique des services aux constructeurs et aux méthodes selon leur type, pour que vous n'ayez pas à câbler les dépendances à la main. -- **Tag** - une étiquette attachée à une définition (éventuellement avec une valeur) ; une extension peut ensuite retrouver tous les services qui la portent avec `findByTag()`. -- **Setup** - les appels supplémentaires effectués sur un service juste après sa création - appels de méthodes ou affectations de propriétés, ajoutés avec `addSetup()`. -- **Alias** - un nom alternatif pour un service existant. diff --git a/dependency-injection/fr/global-state.texy b/dependency-injection/fr/global-state.texy deleted file mode 100644 index 454b2f853e..0000000000 --- a/dependency-injection/fr/global-state.texy +++ /dev/null @@ -1,294 +0,0 @@ -État global & singletons -************************ - -.[perex] -Attention : les constructions suivantes sont le symptôme d'un code mal conçu : - -- `Foo::getInstance()` -- `DB::insert(...)` -- `Article::setDb($db)` -- `ClassName::$var` ou `static::$var` - -Est-ce que l'une de ces constructions apparaît dans votre code ? Si oui, vous avez une occasion de l'améliorer. Vous pensez peut-être qu'il s'agit de constructions courantes, vues par exemple dans les solutions proposées par diverses bibliothèques et frameworks. Si c'est le cas, la conception de leur code est défaillante. - -Il ne s'agit pas ici d'une quelconque pureté académique. Toutes ces constructions ont un trait commun : elles utilisent l'état global. Et l'état global a un effet néfaste sur la qualité du code. Les classes deviennent trompeuses quant à leurs dépendances. Le code devient imprévisible. Il désoriente les développeurs et réduit leur efficacité. - -Dans ce chapitre, nous expliquerons pourquoi il en est ainsi et comment éviter l'état global. - - -Interconnexion globale ----------------------- - -Dans un monde idéal, un objet ne devrait communiquer qu'avec les objets qui lui ont été [directement passés |passing-dependencies]. Si je crée deux objets `A` et `B` et que je ne passe jamais de référence de l'un à l'autre, alors ni `A` ni `B` ne peut accéder à l'état de l'autre ni le modifier. C'est une propriété du code hautement souhaitable. C'est comme avoir une pile et une ampoule : l'ampoule ne s'allumera pas tant que vous ne l'aurez pas reliée à la pile par un fil. - -Cela ne vaut cependant pas pour les variables globales (statiques) ni pour les singletons. L'objet `A` peut accéder *sans fil* à l'objet `C` et le modifier sans qu'aucune référence lui ait été passée, en appelant `C::changeSomething()`. Et si l'objet `B` puise lui aussi dans le `C` global, alors `A` et `B` peuvent s'influencer mutuellement à travers `C`. - -L'utilisation de variables globales introduit une nouvelle forme de couplage *sans fil*, invisible de l'extérieur. Elle crée un écran de fumée qui rend le code plus difficile à comprendre et à utiliser. Pour saisir réellement les dépendances, les développeurs doivent lire chaque ligne du code source au lieu de se fier simplement aux interfaces des classes. De plus, ce couplage est totalement inutile. On utilise l'état global parce qu'il est facilement accessible de partout et qu'il permet, par exemple, d'écrire dans une base de données par une méthode globale (statique) `DB::insert()`. Mais comme nous allons le montrer, le confort apparent est minime au regard des graves complications qu'il apporte. - -.[note] -Du point de vue du comportement, il n'y a aucune différence entre une variable globale et une variable statique. Elles sont tout aussi nuisibles. - - -L'action fantomatique à distance --------------------------------- - -"L'action fantomatique à distance" - c'est ainsi qu'Albert Einstein a qualifié, dans une formule restée célèbre, un phénomène de la physique quantique qui lui donnait la chair de poule. -Il s'agit de l'intrication quantique, où la mesure d'une propriété d'une particule affecte instantanément une autre particule intriquée, quelle que soit la distance qui les sépare, fût-elle de millions d'années-lumière, ce qui semble violer la loi fondamentale de l'univers selon laquelle rien ne peut aller plus vite que la lumière. - -Dans le monde du logiciel, 'l'action fantomatique à distance' décrit une situation où nous exécutons un processus que nous croyons isolé (puisque aucune dépendance ne lui a été explicitement passée), alors que des interactions et des changements d'état inattendus se produisent, à notre insu, dans des parties éloignées du système. Cela ne peut arriver que par l'état global. - -Imaginez que vous rejoigniez une équipe de développement sur un projet à la base de code vaste et mature. Votre nouveau chef vous demande d'implémenter une nouvelle fonctionnalité et, en bon développeur, vous commencez par écrire un test. Mais comme vous êtes nouveau sur le projet, vous faites beaucoup de tests exploratoires du genre 'que se passe-t-il si j'appelle cette méthode'. Et vous essayez d'écrire le test suivant : - -```php -function testCreditCardCharge() -{ - $cc = new CreditCard('1234567890123456', 5, 2028); // votre numéro de carte - $cc->charge(100); -} -``` - -Vous exécutez le code, peut-être plusieurs fois, et au bout d'un moment vous remarquez les notifications de la banque sur votre téléphone : 100 $ ont été débités de votre carte de crédit à chaque exécution ! 🤦‍♂️ - -Comment diable le test a-t-il pu provoquer un vrai débit ? Manipuler une carte de crédit n'a rien de simple. Il faut communiquer avec un service web tiers, connaître son URL, s'authentifier, etc. Aucune de ces informations ne figure dans le test. Pire encore, vous ne savez pas où elles se trouvent, ce qui rend impossible de simuler les dépendances externes pour éviter le débit de 100 $ à chaque exécution du test. Et, en tant que nouveau développeur, comment auriez-vous pu savoir que ce que vous vous apprêtiez à faire allait vous appauvrir de 100 $ ? - -Voilà une action fantomatique à distance ! - -Vous êtes contraint d'éplucher un code source considérable et de consulter vos collègues expérimentés pour comprendre les interconnexions du projet. Cette difficulté vient du fait que l'interface de la classe `CreditCard` ne révèle pas l'initialisation nécessaire de l'état global. Même l'examen du code source de la classe ne révélera peut-être pas quelle méthode d'initialisation appeler. Au mieux, vous trouverez la variable globale à laquelle elle accède et tenterez d'en déduire comment l'initialiser. - -Les classes d'un tel projet sont des menteuses pathologiques. La classe `CreditCard` fait semblant de pouvoir être simplement instanciée et de voir sa méthode `charge()` appelée. En réalité, elle communique en secret avec une autre classe, `PaymentGateway`, qui représente la passerelle de paiement. Même l'interface de `PaymentGateway` peut laisser croire à une initialisation indépendante alors qu'en réalité, elle va peut-être chercher les identifiants dans un fichier de configuration, et ainsi de suite. Les développeurs d'origine savent que `CreditCard` a besoin de `PaymentGateway`. Ils ont écrit le code ainsi. Mais pour les nouveaux venus, c'est un mystère complet qui les empêche d'apprendre et de contribuer efficacement. - -Comment corriger la situation ? Facilement. **Que l'API déclare ses dépendances.** - -```php -function testCreditCardCharge() -{ - $gateway = new PaymentGateway(/* ... */); - $cc = new CreditCard('1234567890123456', 5, 2028); - $cc->charge($gateway, 100); -} -``` - -Remarquez comme les interdépendances du code sautent immédiatement aux yeux. Parce que la méthode `charge()` déclare avoir besoin d'une `PaymentGateway`, vous n'avez plus à deviner cette dépendance ni à poser la question. Vous savez que vous devez en créer une instance et, ce faisant, vous découvrirez les paramètres d'accès nécessaires. Sans eux, le code ne s'exécuterait même pas. - -Et surtout, vous pouvez désormais simuler la passerelle de paiement, si bien que 100 $ ne vous seront plus débités à chaque exécution d'un test. - -L'état global permet aux objets d'accéder en secret à des dépendances non déclarées dans leur API, ce qui transforme vos API en menteuses pathologiques. - -Vous ne l'aviez peut-être jamais vu ainsi, mais chaque fois que vous utilisez l'état global, vous créez des canaux de communication sans fil secrets. Cette action fantomatique à distance oblige les développeurs à lire chaque ligne de code pour comprendre les interactions possibles, ce qui réduit la productivité et déroute les nouveaux membres de l'équipe. Si c'est vous qui avez écrit le code, vous connaissez les vraies dépendances, mais celui qui vient après vous n'en a aucune idée. - -Évitez d'écrire du code qui repose sur l'état global ; préférez le passage explicite des dépendances. Adoptez l'injection de dépendances. - - -Fragilité de l'état global --------------------------- - -Dans un code qui utilise l'état global et des singletons, vous ne pouvez jamais être sûr de quand ni par qui l'état a été modifié. Ce risque se manifeste dès l'initialisation. Le code suivant a l'intention de créer une connexion à la base de données et d'initialiser une passerelle de paiement, mais il lève sans cesse des exceptions et en déboguer la cause est extrêmement pénible : - -```php -PaymentGateway::init(); -DB::init('mysql:', 'user', 'password'); -``` - -Vous devez suivre méticuleusement le code pour découvrir que l'objet `PaymentGateway` accède sans fil à d'autres objets, dont certains ont besoin d'une connexion à la base de données. La base de données doit donc être initialisée avant `PaymentGateway`. Mais l'écran de fumée de l'état global vous le cache. Combien de temps serait gagné si l'API de ces classes était honnête et déclarait ses dépendances ? - -```php -$db = new DB('mysql:', 'user', 'password'); -$gateway = new PaymentGateway($db, /* ... */); -``` - -Un problème similaire survient lorsqu'on utilise un accès global à la connexion de base de données : - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public function save(): void - { - DB::insert(/* ... */); - } -} -``` - -Lors de l'appel de la méthode `save()`, on ne sait pas si une connexion à la base de données a été établie ni qui est chargé de l'établir. Si nous avons besoin de changer dynamiquement la connexion (par exemple pour les tests), nous risquons d'en venir à ajouter des méthodes comme `DB::reconnect(...)` ou `DB::reconnectForTest()`. - -Prenons un exemple : - -```php -$article = new Article; -// ... -DB::reconnectForTest(); -Foo::doSomething(); -$article->save(); -``` - -Comment être sûr que la base de données de test est réellement utilisée lors de l'appel à `$article->save()` ? Et si la méthode `Foo::doSomething()` avait changé la connexion globale ? Pour le savoir, il nous faudrait inspecter le code source de `Foo` et peut-être de bien d'autres classes. Et cette enquête ne donnerait qu'une réponse temporaire, car la situation peut changer plus tard. - -Et si nous déplacions la connexion à la base de données dans une variable statique à l'intérieur de la classe `Article` ? - -```php -class Article -{ - private static DB $db; - - public static function setDb(DB $db): void - { - self::$db = $db; - } - - public function save(): void - { - self::$db->insert(/* ... */); - } -} -``` - -Cela ne change absolument rien. Le problème, c'est l'état global lui-même, quelle que soit la classe dans laquelle il est caché. Dans ce scénario, comme dans le précédent, lors de l'appel à `$article->save()` nous n'avons aucune certitude quant à la base de données dans laquelle les données seront écrites. N'importe qui, n'importe où dans l'application, a pu changer la base à n'importe quel moment à l'aide de `Article::setDb()`. À notre insu. - -L'état global rend notre application **extrêmement fragile**. - -Il existe pourtant une façon simple de régler ce problème. Il suffit que l'API déclare ses dépendances pour garantir un fonctionnement correct. - -```php -class Article -{ - public function __construct( - private DB $db, - ) { - } - - public function save(): void - { - $this->db->insert(/* ... */); - } -} - -$article = new Article($db); -// ... -Foo::doSomething(); -$article->save(); -``` - -Cette approche élimine les craintes de modifications cachées ou inattendues de la connexion à la base de données. Nous savons désormais avec certitude où l'article est enregistré, et les modifications apportées à des classes sans rapport ne peuvent plus l'affecter. Le code n'est plus fragile, il est stable. - -Évitez d'écrire du code qui repose sur l'état global ; préférez le passage explicite des dépendances. Adoptez l'injection de dépendances. - - -Singleton ---------- - -Le singleton est un patron de conception qui, selon la [définition |https://fr.wikipedia.org/wiki/Singleton_(patron_de_conception)] de la célèbre publication du Gang of Four, restreint une classe à une instance unique et offre un accès global à celle-ci. L'implémentation de ce patron ressemble habituellement au code suivant : - -```php -class Singleton -{ - private static self $instance; - - public static function getInstance(): self - { - self::$instance ??= new self; - return self::$instance; - } - - // et d'autres méthodes qui assurent les fonctions de la classe -} -``` - -Malheureusement, le singleton introduit l'état global dans l'application. Et comme nous l'avons montré plus haut, l'état global n'est pas souhaitable. C'est pourquoi le singleton est considéré comme un anti-pattern. - -N'utilisez pas de singletons dans votre code et remplacez-les par d'autres mécanismes. Vous n'avez vraiment pas besoin de singletons. En revanche, si vous devez garantir qu'une seule instance d'une classe existe dans toute l'application, déléguez cette responsabilité au [conteneur DI |container]. Vous obtenez ainsi un singleton à l'échelle de l'application, ce qu'on appelle couramment un service. La classe elle-même est alors libérée de la gestion de son unicité (elle n'aura donc ni méthode `getInstance()` ni propriété statique d'instance) et peut se consacrer uniquement à ses responsabilités. Elle cessera ainsi de violer le principe de responsabilité unique. - - -L'état global face aux tests ----------------------------- - -Quand nous écrivons des tests, nous partons idéalement du principe que chaque test est une unité isolée, qu'aucun état extérieur n'y entre et n'en sort. Une fois le test terminé, tout état qui lui est associé devrait être nettoyé automatiquement par le ramasse-miettes. C'est ce qui rend les tests isolés. Nous pouvons donc exécuter les tests dans n'importe quel ordre. - -Mais dès que des états globaux ou des singletons entrent en jeu, ces hypothèses bienvenues s'effondrent. L'état peut entrer dans les tests et en sortir. Soudain, l'ordre des tests peut avoir de l'importance. - -Pour seulement pouvoir tester du code comportant des singletons, les développeurs doivent souvent compromettre leur intégrité, par exemple en permettant de remplacer l'instance du singleton. De telles solutions ne sont, au mieux, que des bricolages qui aboutissent à un code difficile à maintenir et à comprendre. Chaque test (ou sa méthode `tearDown()`) qui modifie l'état global doit méticuleusement annuler ces changements. - -L'état global est le plus gros casse-tête des tests unitaires ! - -Comment y remédier ? Simplement. Évitez d'écrire du code qui utilise des singletons ; préférez le passage explicite des dépendances. Adoptez l'injection de dépendances. - - -Constantes globales -------------------- - -L'état global ne se limite pas à l'usage des singletons et des variables statiques, il peut aussi concerner les constantes globales. - -Les constantes dont les valeurs représentent des vérités universelles (`M_PI`) ou fournissent une information autonome (`PREG_BACKTRACK_LIMIT_ERROR`) sont généralement acceptables. En revanche, les constantes utilisées comme moyen d'injecter *sans fil* de l'information dans le code sont en fait des dépendances cachées. Comme `LOG_FILE` dans l'exemple suivant. L'usage de la constante `FILE_APPEND` est, lui, parfaitement correct. - -```php -const LOG_FILE = '...'; - -class Foo -{ - public function doSomething() - { - // ... - file_put_contents(LOG_FILE, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Nous devrions plutôt déclarer le chemin du fichier de log comme paramètre du constructeur de la classe `Foo`, pour en faire une partie explicite de son API : - -```php -class Foo -{ - public function __construct( - private string $logFile, - ) { - } - - public function doSomething() - { - // ... - file_put_contents($this->logFile, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Nous passons maintenant explicitement le chemin du fichier de log. Nous pouvons le changer facilement selon les besoins, ce qui simplifie les tests et la maintenance du code. - - -Fonctions globales et méthodes statiques ----------------------------------------- - -Nous voulons souligner que l'usage des méthodes statiques et des fonctions globales n'est pas problématique en soi. Nous avons expliqué les problèmes posés par des méthodes comme `DB::insert()`, mais le problème de fond était toujours l'état global sous-jacent, généralement stocké dans une variable statique. La méthode `DB::insert()` repose sur une variable statique qui contient la connexion à la base de données. Sans cette variable, il serait impossible d'implémenter la méthode. - -L'usage de méthodes et de fonctions statiques déterministes comme `Closure::fromCallable()`, `strlen()` et bien d'autres est parfaitement compatible avec l'injection de dépendances. Ces fonctions sont prévisibles, car elles renvoient toujours le même résultat pour les mêmes paramètres d'entrée. Elles n'utilisent aucun état global. - -Il existe cependant en PHP des fonctions qui ne sont pas déterministes. C'est le cas, par exemple, de la fonction `htmlspecialchars()`. Son troisième paramètre, `$encoding`, s'il est omis, prend par défaut la valeur de l'option de configuration `default_charset` (`ini_get('default_charset')`). Il est donc recommandé de toujours indiquer ce paramètre afin d'éviter un comportement potentiellement imprévisible. Nette le fait systématiquement. - -Certaines fonctions, comme `strtolower()` et `strtoupper()`, avaient encore récemment un comportement non déterministe, dépendant du réglage de la locale (`setlocale()`). Cela a causé de nombreuses complications, le plus souvent lors du travail avec la langue turque. Le turc distingue en effet le 'I' avec et sans point, aussi bien en minuscule qu'en majuscule. Ainsi, `strtolower('I')` renvoyait `ı` (i minuscule sans point) et `strtoupper('i')` renvoyait `İ` (I majuscule avec point), d'où quantité d'erreurs mystérieuses dans les applications. Ce problème a toutefois été corrigé dans PHP 8.2 et ces fonctions ne dépendent plus de la locale. - -C'est un bon exemple de la façon dont l'état global (le réglage de la locale) a tourmenté des milliers de développeurs dans le monde entier. La solution a finalement consisté à rendre les fonctions indépendantes de la locale, autrement dit à supprimer la dépendance cachée. - - -Quand peut-on utiliser l'état global ? --------------------------------------- - -Il existe des situations particulières et limitées où l'usage de l'état global peut être acceptable. Par exemple pendant le débogage, quand vous avez besoin de dumper la valeur d'une variable ou de mesurer le temps d'exécution d'un morceau de code précis. Dans ces cas, qui concernent des actions temporaires qui seront ensuite retirées du code, l'utilisation d'un dumper ou d'un chronomètre accessible globalement peut être légitime. Ces outils ne font pas partie de la conception même de l'application. - -Autre exemple : les fonctions de PHP dédiées aux expressions régulières (`preg_*`), qui mettent en cache en interne, dans une mémoire statique, les expressions régulières compilées. Lorsque vous appelez ces fonctions plusieurs fois avec la même expression régulière dans votre code, l'expression n'est compilée qu'une seule fois. Ce cache améliore les performances et est totalement invisible pour l'utilisateur, si bien que cet usage d'un état statique interne est généralement acceptable. - - -Résumé ------- - -Nous avons expliqué pourquoi il est judicieux de : - -1) éliminer de votre code toutes les propriétés statiques modifiables (l'état global), -2) déclarer explicitement les dépendances, -3) et utiliser l'injection de dépendances. - -Quand vous concevez votre code, gardez à l'esprit que chaque `static $foo` modifiable est une source potentielle de problèmes. Pour créer un environnement propice à la DI, il est essentiel d'éliminer complètement l'état global et de le remplacer par l'injection de dépendances. - -Au cours de ce travail, vous découvrirez peut-être la nécessité de scinder des classes qui ont plusieurs responsabilités. N'hésitez pas à le faire ; visez le principe de responsabilité unique. - -*Je tiens à remercier Miško Hevery, dont les articles comme [Flaw: Brittle Global State & Singletons |https://web.archive.org/web/20230321084133/http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/] sont à la base de ce chapitre.* diff --git a/dependency-injection/fr/introduction.texy b/dependency-injection/fr/introduction.texy deleted file mode 100644 index c801c7f8ea..0000000000 --- a/dependency-injection/fr/introduction.texy +++ /dev/null @@ -1,525 +0,0 @@ -Qu'est-ce que l'injection de dépendances ? -****************************************** - -.[perex] -Ce chapitre vous présente les pratiques de programmation de base que vous devriez suivre lors de l'écriture de n'importe quelle application. Ce sont les fondations nécessaires à l'écriture d'un code propre, compréhensible et maintenable. - -Si vous adoptez et suivez ces règles, Nette vous accompagnera à chaque étape. Il fera pour vous le travail de routine et vous offrira un maximum de confort, afin que vous puissiez vous concentrer sur la logique elle-même. - -Les principes que nous allons montrer ici sont assez simples. Il n'y a rien à craindre. - - -Vous vous souvenez de votre premier programme ? ------------------------------------------------ - -Nous ne savons pas dans quel langage vous l'avez écrit, mais si c'était en PHP, il ressemblait probablement à ceci : - -```php -function addition(float $a, float $b): float -{ - return $a + $b; -} - -echo addition(23, 1); // affiche 24 -``` - -Quelques lignes de code triviales, et pourtant elles cachent tant de concepts clés. Que les variables existent. Que le code est divisé en unités plus petites, comme les fonctions. Que nous leur passons des arguments d'entrée et qu'elles renvoient des résultats. Il ne manque que les conditions et les boucles. - -Le fait de passer des données d'entrée à une fonction et qu'elle renvoie un résultat est un concept parfaitement compréhensible, utilisé aussi dans d'autres domaines, comme les mathématiques. - -Une fonction a sa signature, composée de son nom, de la liste de ses paramètres et de leurs types et, enfin, du type de la valeur de retour. En tant qu'utilisateurs, c'est la signature qui nous intéresse ; nous n'avons généralement pas besoin de connaître l'implémentation interne. - -Imaginez maintenant que la signature de la fonction ressemble à ceci : - -```php -function addition(float $x): float -``` - -Une addition à un seul paramètre ? C'est étrange… Et celle-ci ? - -```php -function addition(): float -``` - -Là, c'est vraiment étrange, non ? Comment utilise-t-on cette fonction ? - -```php -echo addition(); // qu'est-ce que cela va afficher ? -``` - -Devant un tel code, nous serions perplexes. Non seulement un débutant ne le comprendrait pas, mais même un programmeur chevronné ne comprendrait pas un code pareil. - -Vous vous demandez à quoi ressemblerait une telle fonction à l'intérieur ? Où prendrait-elle les nombres à additionner ? Elle se les procurerait probablement *d'une manière ou d'une autre* elle-même, peut-être comme ceci : - -```php -function addition(): float -{ - $a = Input::get('a'); - $b = Input::get('b'); - return $a + $b; -} -``` - -Dans le corps de la fonction, nous avons découvert des dépendances cachées vers d'autres fonctions globales ou méthodes statiques. Pour savoir d'où viennent réellement les nombres, il nous faut enquêter plus loin. - - -Pas comme ça ! --------------- - -La conception que nous venons de montrer est à l'origine de nombreuses caractéristiques négatives : - -- La signature de la fonction faisait semblant de ne pas avoir besoin des nombres à additionner, ce qui nous a désorientés. -- Nous n'avons aucune idée de la façon de faire additionner à la fonction deux nombres différents. -- Nous avons dû regarder dans le code pour découvrir d'où elle prend les nombres. -- Nous avons découvert des dépendances cachées. -- Pour comprendre pleinement, il faut examiner ces dépendances également. - -Et est-ce seulement le rôle de la fonction d'addition d'obtenir les entrées ? Bien sûr que non. Sa responsabilité, c'est uniquement l'addition elle-même. - - -Nous ne voulons pas rencontrer un tel code, et nous ne voulons certainement pas l'écrire. La correction est simple : revenir aux fondamentaux et utiliser tout simplement des paramètres : - - -```php -function addition(float $a, float $b): float -{ - return $a + $b; -} -``` - - -Règle n° 1 : laissez-vous les passer ------------------------------------- - -La règle la plus importante est : **toutes les données dont les fonctions ou les classes ont besoin doivent leur être fournies**. - -Au lieu d'inventer des moyens cachés pour qu'elles obtiennent ces données, fournissez-leur simplement les paramètres. Vous économiserez le temps passé à inventer des chemins cachés qui, à coup sûr, n'amélioreront pas votre code. - -Si vous suivez toujours cette règle, partout, vous êtes sur la voie d'un code sans dépendances cachées. D'un code compréhensible non seulement par son auteur, mais aussi par quiconque le lira plus tard. Où tout se comprend à partir des signatures des fonctions et des classes, sans avoir à chercher des détails cachés dans l'implémentation. - -Cette technique porte le nom savant d'**injection de dépendances**. Et les données s'appellent les **dépendances.** Ce n'est que du passage de paramètres ordinaire, rien de plus. - -.[note] -Ne confondez pas l'injection de dépendances, qui est un patron de conception, avec le "conteneur d'injection de dépendances", qui est un outil, quelque chose de fondamentalement différent. Nous parlerons des conteneurs plus tard. - - -Des fonctions aux classes -------------------------- - -Et comment cela s'applique-t-il aux classes ? Une classe est une entité plus complexe qu'une simple fonction, mais la règle n° 1 s'y applique tout autant. Il y a seulement [plus de façons de passer les arguments |passing-dependencies]. Par exemple, d'une manière tout à fait semblable au cas de la fonction : - -```php -class Math -{ - public function sum(float $a, float $b): float - { - return $a + $b; - } -} - -$math = new Math; -echo $math->sum(23, 1); // 24 -``` - -Ou à l'aide d'autres méthodes, ou directement du constructeur : - -```php -class Sum -{ - public function __construct( - private float $a, - private float $b, - ) { - } - - public function calculate(): float - { - return $this->a + $this->b; - } -} - -$sum = new Sum(23, 1); -echo $sum->calculate(); // 24 -``` - -Les deux exemples respectent pleinement l'injection de dépendances. - - -Exemples de la vie réelle -------------------------- - -Dans le monde réel, vous n'écrirez pas de classes pour additionner des nombres. Passons à des exemples pratiques. - -Soit une classe `Article` représentant un article de blog : - -```php -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - // enregistre l'article dans la base de données - } -} -``` - -et son utilisation sera la suivante : - -```php -$article = new Article; -$article->title = '10 choses à savoir pour perdre du poids'; -$article->content = 'Chaque année, des millions de personnes ...'; -$article->save(); -``` - -La méthode `save()` enregistrera l'article dans une table de la base de données. L'implémenter à l'aide de [Nette Database |database:] serait simple, s'il n'y avait un hic : où `Article` prend-il la connexion à la base de données, c'est-à-dire un objet de la classe `Nette\Database\Connection` ? - -Il semble que nous ayons de nombreuses possibilités. Il pourrait la prendre dans une variable statique. Ou en héritant d'une classe qui fournit la connexion à la base de données. Ou utiliser un [singleton |global-state#Singleton]. Ou encore les fameuses façades, comme celles utilisées dans Laravel : - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - DB::insert( - 'INSERT INTO articles (title, content) VALUES (?, ?)', - [$this->title, $this->content], - ); - } -} -``` - -Parfait, nous avons résolu le problème. - -Vraiment ? - -Rappelons-nous la [#Règle n° 1 : laissez-vous les passer] : toutes les dépendances dont la classe a besoin doivent lui être passées. Car si nous enfreignons la règle, nous nous engageons sur la voie d'un code désordonné, plein de dépendances cachées et de zones d'ombre, et le résultat sera une application dont la maintenance et le développement relèveront du défi. - -L'utilisateur de la classe `Article` n'a aucune idée de l'endroit où la méthode `save()` enregistre l'article. Dans une table de base de données ? Laquelle, la base de production ou celle de test ? Et comment peut-on en changer ? - -L'utilisateur doit regarder comment la méthode `save()` est implémentée et il y trouve l'utilisation de la méthode `DB::insert()`. Il doit donc enquêter plus loin pour savoir comment cette méthode obtient la connexion à la base de données. Et les dépendances cachées peuvent former une chaîne assez longue. - -Dans un code propre et bien conçu, il n'y a jamais de dépendances cachées, de façades Laravel ni de variables statiques. Dans un code propre et bien conçu, on passe les arguments : - -```php -class Article -{ - public function save(Nette\Database\Connection $db): void - { - $db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -Encore plus pratique, comme nous le verrons plus loin, est l'utilisation du constructeur : - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function save(): void - { - $this->db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -.[note] -Si vous êtes un programmeur expérimenté, vous vous dites peut-être qu'`Article` ne devrait pas avoir de méthode `save()` du tout ; qu'il devrait représenter une pure structure de données et qu'un repository distinct devrait se charger de l'enregistrement. C'est pertinent. Mais cela nous emmènerait bien au-delà du sujet, qui est l'injection de dépendances, et de l'objectif de donner des exemples simples. - -Si vous écrivez une classe qui a besoin, par exemple, d'une base de données pour fonctionner, n'inventez pas d'où la prendre, mais faites-vous-la passer. Peut-être comme paramètre du constructeur ou d'une autre méthode. Reconnaissez les dépendances. Reconnaissez-les dans l'API de votre classe. Vous obtiendrez un code compréhensible et prévisible. - -Et que dire de cette classe, qui journalise les messages d'erreur : - -```php -class Logger -{ - public function log(string $message): void - { - $file = LOG_DIR . '/log.txt'; - file_put_contents($file, $message . "\n", FILE_APPEND); - } -} -``` - -Qu'en pensez-vous, avons-nous respecté la [#Règle n° 1 : laissez-vous les passer] ? - -Non. - -L'information essentielle, le répertoire contenant le fichier de log, est *obtenue par la classe elle-même* à partir d'une constante. - -Regardez l'exemple d'utilisation : - -```php -$logger = new Logger; -$logger->log('La température est de 23 °C'); -$logger->log('La température est de 10 °C'); -``` - -Sans connaître l'implémentation, pourriez-vous dire où les messages sont écrits ? Vous seriez-vous douté que l'existence de la constante `LOG_DIR` est nécessaire à son fonctionnement ? Et pourriez-vous créer une seconde instance qui écrirait ailleurs ? Certainement pas. - -Corrigeons la classe : - -```php -class Logger -{ - public function __construct( - private string $file, - ) { - } - - public function log(string $message): void - { - file_put_contents($this->file, $message . "\n", FILE_APPEND); - } -} -``` - -La classe est maintenant bien plus compréhensible, configurable et donc plus utile. - -```php -$logger = new Logger('/path/to/log.txt'); -$logger->log('La température est de 15 °C'); -``` - - -Mais je m'en fiche ! --------------------- - -*"Quand je crée un objet Article et que j'appelle save(), je ne veux pas m'occuper de la base de données ; je veux juste qu'il soit enregistré dans celle que j'ai configurée."* - -*"Quand j'utilise Logger, je veux juste que le message soit écrit, et je ne veux pas m'occuper de savoir où. Que les réglages globaux soient utilisés."* - -Ce sont des remarques légitimes. - -Prenons comme exemple une classe qui distribue des newsletters et journalise le résultat : - -```php -class NewsletterDistributor -{ - public function distribute(): void - { - $logger = new Logger(/* ... */); - try { - $this->sendEmails(); - $logger->log('Les e-mails ont été envoyés'); - - } catch (Exception $e) { - $logger->log('Une erreur est survenue pendant l\'envoi'); - throw $e; - } - } -} -``` - -Le `Logger` amélioré, qui n'utilise plus la constante `LOG_DIR`, exige le chemin du fichier dans son constructeur. Comment régler cela ? La classe `NewsletterDistributor` ne se soucie pas de l'endroit où les messages sont écrits ; elle veut simplement les journaliser. - -La solution est de nouveau la [#Règle n° 1 : laissez-vous les passer] : nous passons toutes les données dont la classe a besoin. - -Cela veut-il dire que nous passons le chemin du log par le constructeur, pour l'utiliser ensuite lors de la création de l'objet `Logger` ? - -```php -class NewsletterDistributor -{ - public function __construct( - private string $file, // ⛔ PAS COMME ÇA ! - ) { - } - - public function distribute(): void - { - $logger = new Logger($this->file); -``` - -Pas comme ça ! Car le chemin n'est **pas** une donnée dont la classe `NewsletterDistributor` a besoin ; c'est le `Logger` qui en a besoin. Percevez-vous la différence ? La classe `NewsletterDistributor` a besoin du logger lui-même. C'est donc le logger lui-même que nous allons passer : - -```php -class NewsletterDistributor -{ - public function __construct( - private Logger $logger, // ✅ - ) { - } - - public function distribute(): void - { - try { - $this->sendEmails(); - $this->logger->log('Les e-mails ont été envoyés'); - - } catch (Exception $e) { - $this->logger->log('Une erreur est survenue pendant l\'envoi'); - throw $e; - } - } -} -``` - -Maintenant, la signature de la classe `NewsletterDistributor` montre clairement que la journalisation fait partie de son fonctionnement. Et remplacer le logger par un autre, par exemple pour les tests, devient tout à fait simple. De plus, si le constructeur de la classe `Logger` venait à changer, cela n'aurait aucun impact sur notre classe. - - -Règle n° 2 : prenez ce qui est à vous -------------------------------------- - -Ne vous laissez pas embrouiller et n'acceptez pas les dépendances de vos dépendances. N'acceptez que vos propres dépendances. - -Grâce à cela, le code qui utilise d'autres objets sera totalement indépendant des changements de leurs constructeurs. Son API sera plus juste. Et surtout, il sera simple de remplacer ces dépendances par d'autres. - - -Un nouveau membre de la famille -------------------------------- - -L'équipe de développement a décidé de créer un second logger, qui écrit dans la base de données. Nous créons donc une classe `DatabaseLogger`. Nous avons maintenant deux classes, `Logger` et `DatabaseLogger` ; l'une écrit dans un fichier, l'autre dans la base de données... le nommage ne vous semble-t-il pas un peu étrange ? Ne vaudrait-il pas mieux renommer `Logger` en `FileLogger` ? Certainement. - -Mais faisons-le intelligemment. Nous créons une interface sous le nom d'origine : - -```php -interface Logger -{ - function log(string $message): void; -} -``` - -… que les deux loggers implémenteront : - -```php -class FileLogger implements Logger -// ... - -class DatabaseLogger implements Logger -// ... -``` - -Et grâce à cela, il n'y aura rien à modifier dans le reste du code où le logger est utilisé. Le constructeur de la classe `NewsletterDistributor`, par exemple, continuera de se contenter d'exiger un `Logger` en paramètre. Et c'est à nous de décider quelle instance nous lui fournissons. - -**C'est pourquoi nous n'ajoutons jamais le suffixe `Interface` ni le préfixe `I` aux noms des interfaces.** Sinon, il ne serait pas possible d'étendre le code aussi élégamment. - - -Houston, nous avons un problème -------------------------------- - -Alors que, dans toute l'application, nous pouvons nous contenter d'une seule instance du logger, qu'il écrive dans un fichier ou dans la base de données, et la passer simplement partout où l'on journalise, la situation est tout autre avec la classe `Article`. Nous créons ses instances au besoin, même plusieurs fois. Comment gérer la dépendance à la base de données dans son constructeur ? - -Prenons l'exemple d'un contrôleur qui doit enregistrer un article dans la base de données après la soumission d'un formulaire : - -```php -class EditController extends Controller -{ - public function formSubmitted($data) - { - $article = new Article(/* ... */); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Une solution possible paraît évidente : faisons passer l'objet base de données par le constructeur dans `EditController` et utilisons `$article = new Article($this->db)`. - -Tout comme dans le cas précédent du `Logger` et du chemin du fichier, ce n'est pas la bonne approche. La base de données n'est pas une dépendance d'`EditController`, mais d'`Article`. Passer la base de données enfreint donc la [Règle n° 2 : prenez ce qui est à vous |#Règle n° 2 : prenez ce qui est à vous]. Si le constructeur de la classe `Article` change (un nouveau paramètre y est ajouté), vous devrez modifier le code à tous les endroits où l'on crée des instances. Aïe. - -Houston, quelle est votre proposition ? - - -Règle n° 3 : laissez faire la factory -------------------------------------- - -En éliminant les dépendances cachées et en passant toutes les dépendances en arguments, nous avons obtenu des classes plus configurables et plus souples. Nous avons donc besoin de quelque chose de plus pour créer et configurer ces classes plus souples à notre place. Nous appellerons cela des factories. - -La règle est la suivante : si une classe a des dépendances, déléguez la création de ses instances à une factory. - -Les factories sont une alternative plus intelligente à l'opérateur `new` dans le monde de l'injection de dépendances. - -.[note] -Ne confondez pas cela avec le patron de conception *factory method*, qui décrit une façon particulière d'utiliser les factories et n'a rien à voir avec ce sujet. - - -Factory -------- - -Une factory est une méthode ou une classe qui crée et configure des objets. Nous appellerons `ArticleFactory` la classe qui produit des `Article`, et elle pourrait ressembler à ceci : - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Son utilisation dans le contrôleur sera la suivante : - -```php -class EditController extends Controller -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function formSubmitted($data) - { - // laissons la factory créer l'objet - $article = $this->articleFactory->create(); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -À ce stade, si la signature du constructeur de la classe `Article` change, la seule partie du code qui doit réagir est `ArticleFactory` elle-même. Tout le reste du code qui manipule des objets `Article`, comme `EditController`, n'est pas affecté. - -Vous vous grattez peut-être la tête en vous demandant si nous avons vraiment amélioré la situation. La quantité de code a augmenté et l'ensemble commence à paraître suspicieusement compliqué. - -Ne vous inquiétez pas, nous arriverons bientôt au conteneur DI de Nette. Et il a plus d'un tour dans son sac, ce qui simplifiera grandement la construction d'applications utilisant l'injection de dépendances. Par exemple, au lieu de la classe `ArticleFactory`, il suffira d'[écrire une simple interface |factory] : - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Mais nous anticipons, restez avec nous :-) - - -Résumé ------- - -Au début de ce chapitre, nous avons promis de montrer une méthode pour concevoir du code propre. Il suffit de veiller à ce que les classes : - -1) [reçoivent les dépendances dont elles ont besoin |#Règle n° 1 : laissez-vous les passer] -2) [et, à l'inverse, ne reçoivent pas ce dont elles n'ont pas directement besoin |#Règle n° 2 : prenez ce qui est à vous] -3) [et que les objets ayant des dépendances soient de préférence créés dans des factories |#Règle n° 3 : laissez faire la factory] - -Cela ne saute peut-être pas aux yeux au premier abord, mais ces trois règles ont des conséquences profondes. Elles mènent à une vision radicalement différente de la conception du code. Cela en vaut-il la peine ? Les programmeurs qui ont abandonné leurs vieilles habitudes et se sont mis à utiliser systématiquement l'injection de dépendances considèrent cette étape comme un moment décisif de leur carrière professionnelle. Elle leur a ouvert un monde d'applications claires et maintenables. - -Et si le code n'utilise pas systématiquement l'injection de dépendances ? S'il est bâti sur des méthodes statiques ou des singletons ? Cela conduit-il à des problèmes ? [Oui, et à des problèmes très importants |global-state]. diff --git a/dependency-injection/fr/nette-container.texy b/dependency-injection/fr/nette-container.texy deleted file mode 100644 index e318c94226..0000000000 --- a/dependency-injection/fr/nette-container.texy +++ /dev/null @@ -1,117 +0,0 @@ -Nette DI Container -****************** - -.[perex] -Nette DI est l'une des bibliothèques les plus intéressantes de Nette. Elle sait générer et mettre à jour automatiquement des conteneurs DI compilés, extrêmement rapides et remarquablement faciles à configurer. - -La forme des services que le conteneur DI doit créer se définit habituellement à l'aide de fichiers de configuration au [format NEON|neon:format]. Le conteneur que nous avons créé manuellement dans le [chapitre précédent|container] s'écrirait ainsi : - -```neon -parameters: - db: - dsn: 'mysql:' - user: root - password: '***' - -services: - - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - - ArticleFactory - - EditController -``` - -La syntaxe est très concise. - -Toutes les dépendances déclarées dans les constructeurs des classes `ArticleFactory` et `EditController` sont détectées et passées automatiquement par Nette DI grâce à ce qu'on appelle l'[autowiring|autowiring], il n'y a donc rien à indiquer dans le fichier de configuration. Ainsi, même si les paramètres changent, vous n'avez rien à modifier dans la configuration. Pendant le développement, Nette régénère le conteneur automatiquement. Vous pouvez vous concentrer purement sur le développement de l'application. - -Si nous voulons passer les dépendances à l'aide de setters, nous utilisons pour cela la section [setup |services#Setup]. - -Nette DI génère directement le code PHP du conteneur. Le résultat est donc un fichier `.php` que vous pouvez ouvrir et examiner. Vous voyez ainsi exactement comment le conteneur fonctionne. Vous pouvez aussi le déboguer dans votre IDE et parcourir son exécution pas à pas. Et surtout : le code PHP généré est extrêmement rapide. - -Nette DI sait également générer le code d'une [factory|factory] à partir d'une interface fournie. Au lieu de la classe `ArticleFactory`, il nous suffit donc de créer une interface dans l'application : - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Vous trouverez l'exemple complet [sur GitHub|https://github.com/nette-examples/di-example-doc]. - - -Utilisation autonome --------------------- - -Intégrer la bibliothèque Nette DI dans une application est très facile. Nous l'installons d'abord à l'aide de Composer (parce que télécharger des fichiers zip, c'est tellement dépassé) : - -```shell -composer require nette/di -``` - -Le code suivant utilise le [Compiler |api:Nette\DI\Compiler] pour créer une instance du conteneur DI selon la configuration stockée dans le fichier `config.neon` : - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp'); -$class = $loader->load(function ($compiler) { - $compiler->loadConfig(__DIR__ . '/config.neon'); -}); -$container = new $class; -``` - -Le conteneur n'est généré qu'une seule fois, son code est écrit dans le cache (le répertoire `__DIR__ . '/temp'`) et, lors des requêtes suivantes, il est seulement chargé depuis là. - -Seul, le `Compiler` n'active dans la configuration que les sections `services` et `parameters`. Pour utiliser les autres - comme `search`, `decorator`, `di` ou `inject` - enregistrez d'abord leurs extensions. Et pour permettre l'enregistrement d'extensions depuis la section `extensions` de la configuration, ajoutez l'`ExtensionsExtension` : - -```php -$compiler->addExtension('search', new Nette\DI\Extensions\SearchExtension($tempDir)); -$compiler->addExtension('extensions', new Nette\DI\Extensions\ExtensionsExtension); -``` - -Le [Configurator |application:bootstrapping] utilisé dans les applications Nette complètes les enregistre tous automatiquement. - -Si vous conservez plusieurs conteneurs différents dans le même répertoire de cache, distinguez-les par une clé passée en deuxième argument à `load()` ; elle fait partie du nom de la classe générée : - -```php -$class = $loader->load( - fn($compiler) => $compiler->loadConfig(__DIR__ . '/config.neon'), - 'my-key', -); -``` - -Les méthodes `getService()` ou `getByType()` servent à créer et à récupérer les services. Voici comment nous créons l'objet `EditController` : - -```php -$controller = $container->getByType(EditController::class); -$controller->someMethod(); -``` - -Pendant le développement, il est utile d'activer le mode de rafraîchissement automatique, où le conteneur se régénère dès qu'une classe ou un fichier de configuration est modifié. Il suffit de passer `true` en deuxième argument du constructeur de [ContainerLoader |api:Nette\DI\ContainerLoader]. - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp', true); -``` - - -Travailler avec le conteneur ----------------------------- - -Outre `getService()` et `getByType()`, l'objet conteneur offre plusieurs autres méthodes utiles : - -- `getByType(string $type, bool $throw = true): ?object` renvoie le service du type donné. Si vous passez `false` en deuxième argument, il renvoie `null` au lieu de lever une exception lorsqu'aucun service de ce type n'existe. -- `hasService(string $name): bool` et `isCreated(string $name): bool` vous disent si un service est défini et s'il a déjà été instancié. -- `getParameters(): array` renvoie tous les paramètres du conteneur, `getParameter($key)` en renvoie un seul. -- `createInstance(string $class, array $args = []): object` crée une nouvelle instance de la classe donnée et lui passe les dépendances du constructeur par autowiring. -- `callMethod(callable $function, array $args = []): mixed` appelle le callable donné et lui passe ses arguments par autowiring. -- `callInjects(object $service): void` appelle toutes les méthodes `inject*()` de l'objet donné et leur passe les dépendances. - -Le constructeur du conteneur accepte également un tableau de paramètres qui complètent ceux définis dans la configuration : - -```php -$container = new $class(['host' => 'localhost']); -``` - - -Utilisation avec Nette Framework --------------------------------- - -Comme nous l'avons montré, l'utilisation de Nette DI n'est pas réservée aux applications construites avec Nette Framework ; vous pouvez l'intégrer n'importe où en seulement trois lignes de code. Cependant, si vous développez des applications à l'aide de Nette Framework, la configuration et la création du conteneur sont prises en charge par [Bootstrap |application:bootstrapping#Configuration du Conteneur DI]. diff --git a/dependency-injection/fr/passing-dependencies.texy b/dependency-injection/fr/passing-dependencies.texy deleted file mode 100644 index 9276d54c77..0000000000 --- a/dependency-injection/fr/passing-dependencies.texy +++ /dev/null @@ -1,215 +0,0 @@ -Passage des dépendances -*********************** - -<div class=perex> - -Les arguments, ou 'dépendances' dans la terminologie DI, peuvent être passés aux classes des principales façons suivantes : - -* Injection par le constructeur -* Injection par méthode (dite injection par setter) -* Injection dans une propriété -* À l'aide de la méthode `inject*()` ou de l'attribut `#[Inject]` - -</div> - -Illustrons chaque variante par des exemples concrets. - - -Injection par le constructeur -============================= - -Les dépendances sont fournies comme arguments du constructeur au moment où l'objet est instancié : - -```php -class MyClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -$obj = new MyClass($cache); -``` - -Cette approche convient aux dépendances obligatoires, dont la classe a absolument besoin pour fonctionner, car sans elles l'instance ne peut pas être créée. - -Depuis PHP 8.0, nous pouvons utiliser une notation plus courte ([promotion des propriétés du constructeur |https://blog.nette.org/fr/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), fonctionnellement équivalente : - -```php -// PHP 8.0 -class MyClass -{ - public function __construct( - private Cache $cache, - ) { - } -} -``` - -Depuis PHP 8.1, une propriété peut être marquée du drapeau `readonly`, qui déclare que sa valeur ne changera plus après l'initialisation : - -```php -// PHP 8.1 -class MyClass -{ - public function __construct( - private readonly Cache $cache, - ) { - } -} -``` - -Le conteneur DI passe les dépendances au constructeur automatiquement grâce à l'[autowiring |autowiring]. Les arguments qui ne peuvent pas être fournis de cette façon (par exemple des chaînes, des nombres, des booléens) [sont indiqués dans la configuration |services#Arguments]. - - -Constructor hell ----------------- - -Le terme *constructor hell* décrit une situation où une classe enfant hérite d'une classe parente dont le constructeur exige des dépendances, alors que la classe enfant en exige elle aussi. Elle doit alors accepter et transmettre également les dépendances du parent : - -```php -abstract class BaseClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass extends BaseClass -{ - private Database $db; - - // ⛔ CONSTRUCTOR HELL - public function __construct(Cache $cache, Database $db) - { - parent::__construct($cache); - $this->db = $db; - } -} -``` - -Le problème surgit lorsque nous voulons modifier le constructeur de `BaseClass`, par exemple quand une nouvelle dépendance s'y ajoute. Il devient alors nécessaire de modifier aussi tous les constructeurs des classes enfants. Ce qui transforme une telle modification en enfer. - -Comment l'éviter ? La solution consiste à **préférer la [composition à l'héritage |faq#Pourquoi la composition est-elle préférée à l'héritage ?]**. - -Nous concevons donc le code autrement. Nous éviterons les classes [abstraites |nette:introduction-to-object-oriented-programming#Classes abstraites] `Base*`. Au lieu que `MyClass` obtienne une certaine fonctionnalité en héritant de `BaseClass`, cette fonctionnalité lui sera passée comme dépendance : - -```php -final class SomeFunctionality -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass -{ - private SomeFunctionality $sf; - private Database $db; - - public function __construct(SomeFunctionality $sf, Database $db) // ✅ - { - $this->sf = $sf; - $this->db = $db; - } -} -``` - - -Injection par setter -==================== - -Les dépendances sont fournies par l'appel d'une méthode qui les stocke dans une propriété privée. La convention de nommage habituelle de ces méthodes suit le modèle `set*()`, d'où leur nom de setters, mais elles peuvent bien sûr être nommées autrement. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - $this->cache = $cache; - } -} - -$obj = new MyClass; -$obj->setCache($cache); -``` - -Cette approche convient aux dépendances facultatives, qui ne sont pas indispensables au fonctionnement de la classe, car rien ne garantit que l'objet recevra effectivement la dépendance (c'est-à-dire que l'appelant invoquera la méthode). - -En même temps, cette méthode permet d'appeler le setter à plusieurs reprises pour changer la dépendance. Si ce n'est pas souhaitable, ajoutez une vérification dans la méthode ou, depuis PHP 8.1, marquez la propriété `$cache` du drapeau `readonly`. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - if (isset($this->cache)) { - throw new RuntimeException('La dépendance a déjà été définie'); - } - $this->cache = $cache; - } -} -``` - -L'appel du setter se définit dans la configuration du conteneur DI, dans la [clé setup |services#Setup]. Là aussi, la fourniture automatique des dépendances par autowiring est utilisée : - -```neon -services: - - create: MyClass - setup: - - setCache -``` - - -Injection dans une propriété -============================ - -Les dépendances sont fournies par écriture directe dans une propriété de l'objet : - -```php -class MyClass -{ - public Cache $cache; -} - -$obj = new MyClass; -$obj->cache = $cache; -``` - -Cette méthode est considérée comme inappropriée, car la propriété doit être déclarée `public`. Nous perdons donc le contrôle sur le fait que la dépendance passée est bien du type requis (c'était particulièrement vrai avant les déclarations de type des propriétés en PHP 7.4), ainsi que la possibilité de réagir à une dépendance nouvellement affectée par une logique propre, par exemple pour empêcher une modification ultérieure. En même temps, la propriété devient partie de l'API publique de la classe, ce qui n'est pas forcément voulu. - -L'affectation de la propriété se définit dans la configuration du conteneur DI, dans la [section setup |services#Setup] : - -```neon -services: - - create: MyClass - setup: - - $cache = @\Cache -``` - - -Inject -====== - -Alors que les trois approches précédentes s'appliquent de façon générale dans tous les langages orientés objet, l'injection par les méthodes `inject*()` ou par l'attribut `#[Inject]` est typiquement utilisée avec les presenters Nette, où elle est activée par défaut ; tout autre service peut l'activer via [`inject: true` |services#Mode inject]. Elles sont traitées dans un [chapitre distinct |best-practices:inject-method-attribute]. - - -Quelle méthode choisir ? -======================== - -- Le constructeur convient aux dépendances obligatoires, dont la classe a absolument besoin pour fonctionner. -- Le setter, à l'inverse, convient aux dépendances facultatives, ou à celles qu'il faudra peut-être changer par la suite. -- Les propriétés publiques ne sont généralement pas recommandées. diff --git a/dependency-injection/fr/services.texy b/dependency-injection/fr/services.texy deleted file mode 100644 index 8275eb3d75..0000000000 --- a/dependency-injection/fr/services.texy +++ /dev/null @@ -1,480 +0,0 @@ -Définition des services -*********************** - -.[perex] -La configuration est l'endroit où nous apprenons au conteneur DI comment créer chaque service et comment le relier à ses dépendances. Nette propose pour cela une manière très claire et élégante. - -La section `services` du fichier de configuration NEON est l'endroit où nous définissons nos propres services et leur configuration. Regardons un exemple simple qui définit un service nommé `database`, représentant une instance de la classe `PDO` : - -```neon -services: - database: PDO('sqlite::memory:') -``` - -La configuration ci-dessus produit la méthode fabrique suivante dans le [conteneur DI|container] : - -```php -public function createServiceDatabase(): PDO -{ - return new PDO('sqlite::memory:'); -} -``` - -Les noms des services permettent de les référencer dans d'autres parties du fichier de configuration, sous la forme `@nomDuService`. S'il n'est pas nécessaire de donner un nom au service, nous pouvons simplement utiliser un tiret (`-`) : - -```neon -services: - - PDO('sqlite::memory:') -``` - -Pour récupérer un service depuis le conteneur DI, nous pouvons utiliser la méthode `getService()` avec le nom du service en paramètre, ou la méthode `getByType()` avec le type du service : - -```php -$database = $container->getService('database'); -$database = $container->getByType(PDO::class); -``` - - -Création de services -==================== - -Habituellement, nous créons un service simplement en instanciant une classe précise. Par exemple : - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Si nous avons besoin d'étoffer la configuration avec d'autres clés, la définition peut s'étaler sur plusieurs lignes : - -```neon -services: - database: - create: PDO('sqlite::memory:') - setup: ... -``` - -La clé `create` a un alias `factory` ; les deux variantes sont couramment utilisées. Nous recommandons cependant `create`. - -Les arguments du constructeur ou de la méthode fabrique peuvent également être indiqués à l'aide de la clé `arguments` : - -```neon -services: - database: - create: PDO - arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] -``` - -Les services ne doivent pas nécessairement être créés par simple instanciation d'une classe ; ils peuvent aussi être le résultat de l'appel de méthodes statiques ou de méthodes d'autres services : - -```neon -services: - database: DatabaseFactory::create() - router: @routerFactory::create() -``` - -Notez que, par souci de simplicité, on écrit `::` au lieu de `->`, voir [#Langage d'expressions]. Ces méthodes fabriques seront générées : - -```php -public function createServiceDatabase(): PDO -{ - return DatabaseFactory::create(); -} - -public function createServiceRouter(): RouteList -{ - return $this->getService('routerFactory')->create(); -} -``` - -Le conteneur DI a besoin de connaître le type du service créé. Si nous créons un service à l'aide d'une méthode qui n'a pas de type de retour déclaré, nous devons indiquer ce type explicitement dans la configuration : - -```neon -services: - database: - create: DatabaseFactory::create() - type: PDO -``` - - -Arguments -========= - -Nous passons les arguments aux constructeurs et aux méthodes d'une manière très proche de celle de PHP lui-même : - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Pour une meilleure lisibilité, nous pouvons énumérer les arguments sur des lignes distinctes. Dans ce cas, les virgules deviennent facultatives : - -```neon -services: - database: PDO( - 'mysql:host=127.0.0.1;dbname=test' - root - secret - ) -``` - -Vous pouvez aussi nommer les arguments, ce qui vous dispense de vous soucier de leur ordre : - -```neon -services: - database: PDO( - username: root - password: secret - dsn: 'mysql:host=127.0.0.1;dbname=test' - ) -``` - -Si vous voulez omettre certains arguments et utiliser leur valeur par défaut, ou faire injecter un service par [autowiring|autowiring], utilisez un tiret bas (`_`) : - -```neon -services: - foo: Foo(_, %appDir%) -``` - -Les arguments peuvent contenir des services, des paramètres et bien plus encore, voir [#Langage d'expressions]. - - -Setup -===== - -Dans la section `setup`, nous définissons les méthodes qui doivent être appelées lors de la création du service. - -```neon -services: - database: - create: PDO(%dsn%, %user%, %password%) - setup: - - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) -``` - -En PHP, cela donnerait ceci : - -```php -public function createServiceDatabase(): PDO -{ - $service = new PDO('...', '...', '...'); - $service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - return $service; -} -``` - -Outre les appels de méthodes, il est aussi possible d'affecter des valeurs à des propriétés. L'ajout d'éléments à des tableaux est également pris en charge ; il faut alors mettre l'accès au tableau entre guillemets pour éviter tout conflit avec la syntaxe NEON : - -```neon -services: - foo: - create: Foo - setup: - - $value = 123 - - '$onClick[]' = [@bar, clickHandler] -``` - -Ce qui, en code PHP, donnerait ceci : - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - $service->value = 123; - $service->onClick[] = [$this->getService('bar'), 'clickHandler']; - return $service; -} -``` - -Dans le setup, vous pouvez cependant aussi appeler des méthodes statiques ou des méthodes d'autres services. Si vous avez besoin de passer le service courant lui-même en argument, référencez-le par `@self` : - -```neon -services: - foo: - create: Foo - setup: - - My\Helpers::initializeFoo(@self) - - @anotherService::setFoo(@self) -``` - -Notez que, par souci de simplicité, on écrit `::` au lieu de `->`, voir [#Langage d'expressions]. La méthode fabrique suivante sera générée : - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - My\Helpers::initializeFoo($service); - $this->getService('anotherService')->setFoo($service); - return $service; -} -``` - - -Langage d'expressions -===================== - -Nette DI propose un langage d'expressions exceptionnellement riche, qui nous permet de définir presque n'importe quoi. Dans les fichiers de configuration, nous pouvons ainsi utiliser des [paramètres |configuration#Paramètres] : - -```neon -# paramètre -%wwwDir% - -# valeur d'un paramètre sous une clé -%mailer.user% - -# paramètre à l'intérieur d'une chaîne -'%wwwDir%/images' -``` - -Nous pouvons également créer des objets, appeler des méthodes et des fonctions : - -```neon -# créer un objet -DateTime() - -# appeler une méthode statique -Collator::create(%locale%) - -# appeler une fonction PHP -::getenv(DB_USER) -``` - -Référencer les services par leur nom ou par leur type : - -```neon -# service par son nom -@database - -# service par son type -@Nette\Database\Connection -``` - -Utiliser la syntaxe first-class callable : .{data-version:3.2.0} - -```neon -# créer un callback, équivalent à [@user, logout] -@user::logout(...) -``` - -Utiliser des constantes : - -```neon -# constante de classe -FilesystemIterator::SKIP_DOTS - -# obtenir une constante globale à l'aide de la fonction PHP constant() -::constant(\PHP_VERSION) -``` - -Accéder aux propriétés publiques et aux constantes d'un service via `@service::membre`. C'est la première lettre du nom qui décide s'il s'agit d'une propriété ou d'une constante : une minuscule initiale signifie une propriété publique, une majuscule une constante : - -```neon -# propriété publique d'un service (commence par une minuscule) -@settings::apiUrl - -# constante de classe d'un service (commence par une majuscule) -@settings::Version -``` - -Les appels de méthodes peuvent être chaînés comme en PHP. Par souci de simplicité, on écrit `::` au lieu de `->` : - -```neon -DateTime()::format('Y-m-d') -# PHP: (new DateTime())->format('Y-m-d') - -@http.request::getUrl()::getHost() -# PHP: $this->getService('http.request')->getUrl()->getHost() -``` - -Vous pouvez utiliser ces expressions partout : lors de la [création des services |#Création de services], dans les [#Arguments], dans la section [#Setup] ou dans les [paramètres |configuration#Paramètres] : - -```neon -parameters: - ipAddress: @http.request::getRemoteAddress() - -services: - database: - create: DatabaseFactory::create( @anotherService::getDsn() ) - setup: - - initialize( ::getenv('DB_USER') ) -``` - - -Fonctions spéciales -------------------- - -Dans les fichiers de configuration, vous pouvez utiliser les fonctions spéciales suivantes : - -- `not()` inverse une valeur -- `bool()`, `int()`, `float()`, `string()` conversion sans perte vers le type indiqué .{data-version:3.0.5} -- `typed()` crée un tableau de tous les services du type indiqué -- `tagged()` crée un tableau de tous les services portant le tag donné - -```neon -services: - - Foo( - id: int(::getenv('ProjectId')) - productionMode: not(%debugMode%) - ) -``` - -Contrairement à la conversion standard de PHP, comme `(int)`, la conversion sans perte lève une exception pour les valeurs non numériques. - -La fonction `typed()` crée un tableau de tous les services du type indiqué (classe ou interface). Elle exclut les services dont l'autowiring est désactivé. Il est aussi possible d'indiquer plusieurs types, séparés par des virgules. - -```neon -services: - - BarsDependent( typed(Bar) ) -``` - -Un tableau de services d'un certain type peut aussi être passé automatiquement en argument grâce à l'[autowiring |autowiring#Collection de services]. - -La fonction `tagged()` crée quant à elle un tableau de tous les services portant un tag précis. Là encore, vous pouvez indiquer plusieurs tags séparés par des virgules. - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - - -Autowiring -========== - -La clé `autowired` vous permet d'influencer le comportement de l'autowiring pour un service donné. Pour les détails, voir le [chapitre sur l'autowiring|autowiring]. - -```neon -services: - foo: - create: Foo - autowired: false # le service foo est exclu de l'autowiring -``` - - -Services lazy .{data-version:3.2.4} -=================================== - -Le chargement lazy est une technique qui diffère la création d'un service jusqu'à ce qu'il soit réellement nécessaire. Dans la configuration globale, vous pouvez [activer la création lazy |configuration#Services lazy] pour tous les services d'un coup. Pour chaque service, vous pouvez ensuite redéfinir ce comportement : - -```neon -services: - foo: - create: Foo - lazy: false -``` - -Lorsqu'un service est défini comme lazy, nous recevons, en le demandant au conteneur DI, un objet proxy particulier. Ce proxy a l'apparence et le comportement du service réel, mais l'initialisation effective (l'appel du constructeur et des appels de setup) n'a lieu qu'au premier accès à l'une de ses méthodes ou propriétés. - -Gardez à l'esprit que, le service étant créé plus tard, les erreurs de sa configuration se manifestent elles aussi plus tard. Par exemple, des identifiants de base de données incorrects ne se révéleront pas au démarrage de l'application, mais seulement à la première requête. - -La création lazy atténue également les dépendances circulaires, c'est-à-dire la situation où le service A a besoin du service B et où B a en même temps besoin de A. Sans elle, le conteneur signale l'erreur `Circular reference detected`. Avec un proxy lazy, le service A ne reçoit qu'un proxy du service B, qui s'initialise au moment où il est réellement utilisé, alors que A existe déjà. Une dépendance circulaire signale néanmoins une conception défaillante et il vaut mieux s'en débarrasser. - -.[note] -Le chargement lazy nécessite PHP 8.4 ou plus récent et ne fonctionne que pour les services créés par instanciation directe d'une classe (par exemple `create: Foo`), pas pour ceux créés par une méthode fabrique. Il ne peut pas non plus être utilisé pour les classes qui, en fin de compte, étendent une classe interne de PHP. Lorsque le chargement lazy ne peut pas s'appliquer, le drapeau `lazy: true` est ignoré silencieusement. - - -Tags -==== - -Les tags servent à ajouter des informations complémentaires aux services. Vous pouvez attribuer un ou plusieurs tags à un service : - -```neon -services: - foo: - create: Foo - tags: - - cached -``` - -Les tags peuvent aussi porter des valeurs : - -```neon -services: - foo: - create: Foo - tags: - logger: monolog.logger.event -``` - -Pour récupérer tous les services associés à des tags précis, vous pouvez utiliser la fonction `tagged()` : - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - -Au sein du conteneur DI, vous pouvez obtenir les noms de tous les services portant un tag précis à l'aide de la méthode `findByTag()` : - -```php -$names = $container->findByTag('logger'); -// $names est un tableau dont les clés sont les noms des services et les valeurs celles des tags -// par ex. ['foo' => 'monolog.logger.event', ...] -``` - - -Mode inject -=========== - -Le drapeau `inject: true` active l'injection de dépendances par des propriétés publiques portant l'attribut [Inject |best-practices:inject-method-attribute#Attributs Inject] et par les méthodes [inject*() |best-practices:inject-method-attribute#Méthodes inject*()]. - -```neon -services: - articles: - create: App\Model\Articles - inject: true -``` - -Par défaut, le mode `inject` n'est activé que pour les presenters. - - -Modification des services -========================= - -Le conteneur DI contient de nombreux services ajoutés par des extensions intégrées ou [écrites par l'utilisateur|extensions]. Vous pouvez modifier les définitions de ces services existants directement dans la configuration. Vous pouvez par exemple remplacer la classe du service `application.application`, qui est par défaut `Nette\Application\Application`, par une autre : - -```neon -services: - application.application: - create: MyApplication - alteration: true -``` - -Le drapeau `alteration` indique que nous ne faisons que modifier un service existant. Il joue aussi le rôle de garde-fou : si le service modifié n'existe pas, la compilation échoue avec une exception. - -Nous pouvons aussi compléter le setup : - -```neon -services: - application.application: - create: MyApplication - alteration: true - setup: - - '$onStartup[]' = [@resource, init] -``` - -Vous n'êtes pas obligé d'identifier un service par son nom interne - vous pouvez le désigner par son type. L'exemple précédent peut aussi s'écrire ainsi : - -```neon -services: - @Nette\Application\Application: - create: MyApplication -``` - -Lors de la modification d'un service, nous pouvons vouloir supprimer les arguments, les éléments de setup ou les tags d'origine, à l'aide de la clé `reset` : - -```neon -services: - application.application: - create: MyApplication - alteration: true - reset: - arguments: true - setup: true - tags: true -``` - -Si vous voulez supprimer un service ajouté par une extension, vous pouvez procéder ainsi : - -```neon -services: - cache.journal: false -``` diff --git a/dependency-injection/fr/upgrading.texy b/dependency-injection/fr/upgrading.texy deleted file mode 100644 index eeeb7574c3..0000000000 --- a/dependency-injection/fr/upgrading.texy +++ /dev/null @@ -1,49 +0,0 @@ -Mise à niveau -************* - - -Mise à niveau vers la version 3.1 -================================= - -- l'autowiring ne passe plus `null` à un paramètre nullable sans valeur par défaut ; indiquez l'argument explicitement, ou donnez une valeur par défaut au paramètre -- la prise en charge de l'annotation `@return` a été abandonnée ; utilisez un type de retour, ou indiquez le type dans la définition du service à l'aide de `type:` -- la clé `dynamic` a été renommée en `imported` et `class` en `type` -- le symbole d'un argument omis passe de `...` à `_`, par exemple `MyService(_, 123)` -- dans les fichiers NEON, le caractère `@` au début d'une chaîne n'a plus besoin d'être échappé -- la clé `parameters` à l'intérieur des définitions de factories générées est obsolète -- la méthode `Nette\DI\Config\Loader::save()` est obsolète ; exportez la configuration à l'aide de `Nette\DI\Config\Adapters\NeonAdapter::dump()` - -La version 3.1 est une version de transition : elle n'apporte pas de nouvelles fonctionnalités, mais elle signale par des avertissements tout ce qui fonctionnera différemment par la suite. Voir l'article [Nette DI 3.1: transition release |https://blog.nette.org/en/nette-di-3-1-transition-release]. - - -Mise à niveau vers la version 3.0 -================================= - -- la prise en charge des fichiers INI a été supprimée -- l'écriture directe de code PHP dans la configuration à l'aide de points d'interrogation (par exemple `"$service->onError[] = ?"(...)`) a été supprimée ; utilisez à la place la syntaxe de tableau `'$onError[]' = [...]` -- dans les fichiers de configuration, utilisez `factory: PDO(...)` au lieu de `class: PDO(...)` -- le tag `nette.presenter` n'est plus utilisé pour les presenters - - -Pour les auteurs d'extensions du compilateur --------------------------------------------- - -Alors que Nette 2.4 décrivait en interne chaque service comme `Nette\DI\ServiceDefinition`, il existe désormais plusieurs types de définitions : `Nette\DI\Definitions\ImportedDefinition` pour les services importés (dynamiques), `Nette\DI\Definitions\FactoryDefinition` pour les factories générées à partir d'interfaces, `Nette\DI\Definitions\AccessorDefinition` pour les accesseurs générés et `Nette\DI\Definitions\ServiceDefinition` pour les services courants. - -C'est pourquoi, en plus de `ContainerBuilder::addDefinition()`, il existe plusieurs autres méthodes pour créer une nouvelle définition : `addFactoryDefinition()`, `addAccessorDefinition()` et `addImportedDefinition()`. - - -Mise à niveau vers la version 2.4 -================================= - -- les sections de configuration (par exemple production, development) dans un seul fichier de configuration sont obsolètes ; utilisez une paire de fichiers `config.neon` et `config.local.neon` -- l'héritage des définitions de services est obsolète -- `Statement::setEntity()` est obsolète - - -Mise à niveau vers la version 2.3 -================================= - -- la prise en charge du placement de services à l'intérieur de la section extension du fichier de configuration a été supprimée -- la prise en charge des extensions ajoutées dynamiquement a été supprimée -- lors du remplacement dynamique d'un service (via `removeService()`, `addService()`), le nouveau service doit être une instance de la même interface/classe que l'original diff --git a/dependency-injection/it/@home.texy b/dependency-injection/it/@home.texy deleted file mode 100644 index 6c42813c18..0000000000 --- a/dependency-injection/it/@home.texy +++ /dev/null @@ -1,22 +0,0 @@ -Nette DI -******** - -.[perex] -La dependency injection è un design pattern che cambierà radicalmente il vostro modo di vedere il codice e lo sviluppo. Apre la strada a un mondo di applicazioni progettate in modo pulito e sostenibile. - -- [Che cos'è la dependency injection? |introduction] -- [Stato globale e singleton |global-state] -- [Passaggio delle dipendenze |passing-dependencies] -- [Che cos'è un container DI? |container] -- [Domande frequenti |faq] - - -Il pacchetto `nette/di` offre per PHP un container DI compilato estremamente avanzato. - -- [Nette DI Container |nette-container] -- [Configurazione |configuration] -- [Definizione dei servizi |services] -- [Autowiring |autowiring] -- [Factory generate |factory] -- [Creare estensioni per Nette DI |extensions] -- [La compilazione del container in dettaglio |compilation-internals] diff --git a/dependency-injection/it/@left-menu.texy b/dependency-injection/it/@left-menu.texy deleted file mode 100644 index bef29981f4..0000000000 --- a/dependency-injection/it/@left-menu.texy +++ /dev/null @@ -1,28 +0,0 @@ -Dependency Injection -******************** -- [Che cos'è la DI? |introduction] -- [Stato globale e singleton |global-state] -- [Passaggio delle dipendenze |passing-dependencies] -- [Che cos'è un container DI? |container] -- [Domande frequenti |faq] - - -Nette DI --------- -- [Nette DI Container |nette-container] -- [Configurazione |configuration] -- [Definizione dei servizi |services] -- [Autowiring |autowiring] -- [Factory generate |factory] -- [Creare estensioni per Nette DI |extensions] -- [La compilazione in dettaglio |compilation-internals] -- [Aggiornamento|upgrading] - - -Letture consigliate -******************* -- [Documentazione di Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Best practice |best-practices:] -- [Risoluzione dei problemi |nette:troubleshooting] diff --git a/dependency-injection/it/@meta.texy b/dependency-injection/it/@meta.texy deleted file mode 100644 index 4647d0c8a2..0000000000 --- a/dependency-injection/it/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentazione Nette}} diff --git a/dependency-injection/it/autowiring.texy b/dependency-injection/it/autowiring.texy deleted file mode 100644 index c57398680a..0000000000 --- a/dependency-injection/it/autowiring.texy +++ /dev/null @@ -1,279 +0,0 @@ -Autowiring -********** - -.[perex] -L'autowiring è una funzionalità straordinaria che passa automaticamente al costruttore e agli altri metodi i servizi necessari, così non dobbiamo indicarli esplicitamente. Vi fa risparmiare molto tempo. - -Grazie a esso possiamo omettere la stragrande maggioranza degli argomenti quando scriviamo le definizioni dei servizi. Invece di: - -```neon -services: - articles: Model\ArticleRepository(@database, @cache.storage) -``` - -Basta scrivere: - -```neon -services: - articles: Model\ArticleRepository -``` - -L'autowiring si basa sui tipi, quindi perché funzioni la classe `ArticleRepository` deve essere definita più o meno così: - -```php -namespace Model; - -class ArticleRepository -{ - public function __construct(\PDO $db, \Nette\Caching\Storage $storage) - {} -} -``` - -L'autowiring non usa mai i nomi dei servizi. Si basa esclusivamente sul sistema di tipi di PHP, quindi sa anche che una classe soddisfa le interfacce che implementa e le classi da cui eredita. Grazie a questo il nome di un servizio è solo un identificatore di comodo, e rinominarlo non rompe nulla nell'applicazione. - -Perché si possa usare l'autowiring, nel container deve esserci **esattamente un servizio** di ogni tipo. Se ce ne fossero di più, l'autowiring non saprebbe quale passare e solleverebbe un'eccezione: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - tempDb: PDO('sqlite::memory:') - articles: Model\ArticleRepository # SOLLEVA UN'ECCEZIONE, corrispondono sia mainDb sia tempDb -``` - -Una soluzione è aggirare l'autowiring e indicare esplicitamente il nome del servizio (per esempio `articles: Model\ArticleRepository(@mainDb)`). Un approccio più comodo, però, è [disattivare |#Disattivare l'autowiring] l'autowiring per uno dei servizi oppure [preferire |#Preferenza nell'autowiring] un servizio agli altri. - - -Disattivare l'autowiring ------------------------- - -Possiamo disattivare l'autowiring di un servizio con l'opzione `autowired: false`: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - - tempDb: - create: PDO('sqlite::memory:') - autowired: false # il servizio tempDb è escluso dall'autowiring - - articles: Model\ArticleRepository # al costruttore viene quindi passato mainDb -``` - -Il servizio `articles` non solleverà un'eccezione sulla presenza di due servizi `PDO` corrispondenti (`mainDb` e `tempDb`) disponibili per il costruttore, perché prende in considerazione solo il servizio `mainDb`. - -L'autowiring si può disattivare globalmente anche per interi tipi, con l'opzione di configurazione [`di › excluded` |configuration#DI], che elenca i tipi (e i loro discendenti) che non devono mai subire l'autowiring. - -.[note] -La configurazione dell'autowiring in Nette differisce da quella di Symfony. In Symfony `autowire: false` significa che l'autowiring non deve essere usato per gli argomenti del costruttore del servizio. In Nette l'autowiring riguarda gli argomenti del costruttore e qualsiasi altro metodo richiamato tramite il container (come la setter injection). L'opzione `autowired: false` impedisce al container di passare automaticamente questa istanza di servizio come dipendenza ad altri servizi. - - -Preferenza nell'autowiring --------------------------- - -Se abbiamo più servizi dello stesso tipo e indichiamo l'opzione `autowired` per uno di essi, quel servizio diventa quello preferito: - -```neon -services: - mainDb: - create: PDO(%dsn%, %user%, %password%) - autowired: PDO # diventa quello preferito - - tempDb: - create: PDO('sqlite::memory:') - - articles: Model\ArticleRepository -``` - -Il servizio `articles` non solleverà un'eccezione sulla presenza di più servizi `PDO` corrispondenti (`mainDb` e `tempDb`), ma userà quello preferito, cioè `mainDb`. - - -Collezione di servizi ---------------------- - -L'autowiring può passare anche array di servizi di un determinato tipo. Poiché PHP non supporta nativamente l'indicazione del tipo degli elementi di un array nelle dichiarazioni di tipo, dovete integrare la dichiarazione `array` con un commento phpDoc che indichi il tipo degli elementi, come `ClassName[]`: - -```php -namespace Model; - -class ShipManager -{ - /** - * @param Shipper[] $shippers - */ - public function __construct(array $shippers) - {} -} -``` - -Il container DI passa allora automaticamente un array dei servizi corrispondenti al tipo indicato. Omette i servizi che hanno l'[autowiring disattivato |#Disattivare l'autowiring] e non include mai nella propria collezione il servizio attualmente in creazione. A differenza del passaggio di un singolo servizio, qui [restringere |#Restringere l'autowiring] l'autowiring a un tipo specifico o contrassegnare un servizio come [preferito |#Preferenza nell'autowiring] non ha alcun effetto: l'array contiene sempre tutti i servizi del tipo indicato. - -Il tipo nel commento può avere anche la forma `array<int, Class>` oppure `list<Class>`. Se non potete controllare la forma del commento phpDoc, potete passare un array di servizi direttamente nella configurazione con [`typed()` |services#Funzioni speciali]. - - -Argomenti scalari ------------------ - -L'autowiring funziona solo per gli oggetti e per gli array di oggetti. Gli argomenti scalari (per esempio stringhe, numeri, booleani) vanno [indicati nella configurazione |services#Argomenti]. Un'alternativa è creare un [oggetto di impostazioni|best-practices:passing-settings-to-presenters] che racchiuda il valore scalare (o più valori). Questo oggetto si può poi passare tramite autowiring. - -```php -class MySettings -{ - public function __construct( - // readonly si può usare da PHP 8.1 - public readonly bool $value, - ) - {} -} -``` - -Lo registrate come servizio aggiungendolo alla configurazione: - -```neon -services: - - MySettings('any value') -``` - -Le altre classi possono poi chiederlo tramite autowiring. - - -Dipendenze facoltative ----------------------- - -Se un parametro del costruttore o di un metodo ha un valore predefinito e nel container non esiste alcun servizio del tipo richiesto, l'autowiring non solleva un'eccezione: salta semplicemente l'argomento, così viene usato il valore predefinito. È così che si dichiarano le dipendenze facoltative: - -```php -class Foo -{ - public function __construct( - private ?Logger $logger = null, - ) {} -} -``` - -Al contrario, per un parametro senza valore predefinito un servizio mancante provoca sempre un'eccezione. - - -Restringere l'autowiring ------------------------- - -Per i singoli servizi l'autowiring si può restringere a determinate classi o interfacce. - -Normalmente l'autowiring passa un servizio a ogni parametro di metodo il cui tipo corrisponde al servizio. Restringere significa stabilire le condizioni che i tipi indicati per i parametri dei metodi devono soddisfare perché il servizio venga passato loro. - -Prendiamo un esempio: - -```php -class ParentClass -{} - -class ChildClass extends ParentClass -{} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Se li registrassimo tutti come servizi, l'autowiring fallirebbe: - -```neon -services: - parent: ParentClass - child: ChildClass - parentDep: ParentDependent # SOLLEVA UN'ECCEZIONE, corrispondono sia parent sia child - childDep: ChildDependent # l'autowiring passa al costruttore il servizio child -``` - -Il servizio `parentDep` solleva l'eccezione `Multiple services of type ParentClass found: child, parent`, perché nel suo costruttore entrano sia il servizio `parent` sia `child` e l'autowiring non riesce a decidere quale scegliere. - -Per il servizio `child` possiamo quindi restringerne l'autowiring al tipo `ChildClass`: - -```neon -services: - parent: ParentClass - child: - create: ChildClass - autowired: ChildClass # si può scrivere anche 'autowired: self' - - parentDep: ParentDependent # l'autowiring passa al costruttore il servizio parent - childDep: ChildDependent # l'autowiring passa al costruttore il servizio child -``` - -Ora al costruttore del servizio `parentDep` viene passato il servizio `parent`, perché è ormai l'unico oggetto corrispondente. Il servizio `child` non gli viene più passato dall'autowiring. Sì, il servizio `child` è ancora di tipo `ParentClass`, ma la condizione restrittiva `autowired: ChildClass` fa sì che venga passato solo a parametri dichiarati esplicitamente come `ChildClass` (o come suoi sottotipi). Poiché `ParentDependent` richiede `ParentClass`, il servizio `child` non è più considerato lì un candidato per l'autowiring. - -Per il servizio `child`, `autowired: ChildClass` si potrebbe scrivere anche come `autowired: self`, perché `self` è un segnaposto per la classe del servizio corrente. - -Nella chiave `autowired` è possibile indicare anche più classi o interfacce, come array: - -```neon -autowired: [ParentClass, FooInterface] -``` - -Proviamo ad aggiungere delle interfacce all'esempio: - -```php -interface FooInterface -{} - -interface BarInterface -{} - -class ParentClass implements FooInterface -{} - -class ChildClass extends ParentClass implements BarInterface -{} - -class FooDependent -{ - function __construct(FooInterface $obj) - {} -} - -class BarDependent -{ - function __construct(BarInterface $obj) - {} -} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Se non limitiamo in alcun modo il servizio `child`, esso entrerà nei costruttori di tutte le classi `FooDependent`, `BarDependent`, `ParentDependent` e `ChildDependent`, e l'autowiring ve lo passerà. - -Se però ne restringiamo l'autowiring a `ChildClass` con `autowired: ChildClass` (o `self`), l'autowiring lo passerà solo al costruttore di `ChildDependent`, perché richiede un argomento di tipo `ChildClass` e vale che `ChildClass` *è di tipo* `ChildClass`. Nessuno degli altri parametri richiede il tipo `ChildClass` o un suo sottotipo, quindi il servizio non viene passato loro. - -Se lo limitiamo a `ParentClass` con `autowired: ParentClass`, l'autowiring lo passerà di nuovo al costruttore di `ChildDependent` (perché il richiesto `ChildClass` è un sottotipo di `ParentClass`) e ora anche al costruttore di `ParentDependent`, perché anche il tipo richiesto `ParentClass` è adatto. - -Se lo limitiamo a `FooInterface`, subirà comunque l'autowiring in `ParentDependent` (il richiesto `ParentClass` è un sottotipo di `FooInterface`) e in `ChildDependent`, e in più nel costruttore di `FooDependent`, ma non in `BarDependent`, perché `BarInterface` non è un sottotipo di `FooInterface`. - -```neon -services: - child: - create: ChildClass - autowired: FooInterface - - fooDep: FooDependent # l'autowiring passa al costruttore il servizio child - barDep: BarDependent # SOLLEVA UN'ECCEZIONE, nessun servizio corrisponde - parentDep: ParentDependent # l'autowiring passa al costruttore il servizio child - childDep: ChildDependent # l'autowiring passa al costruttore il servizio child -``` diff --git a/dependency-injection/it/compilation-internals.texy b/dependency-injection/it/compilation-internals.texy deleted file mode 100644 index 9c89e43c60..0000000000 --- a/dependency-injection/it/compilation-internals.texy +++ /dev/null @@ -1,222 +0,0 @@ -La compilazione del container in dettaglio -****************************************** - -.[perex] -Questa pagina apre la compilazione del container: le fasi che attraversa, quando i parametri di configurazione vengono espansi, quando le stringhe `@service` diventano riferimenti reali e, la domanda che gli autori di estensioni pongono più spesso, in quale fase si possono cercare in sicurezza i servizi per tipo. È il complemento approfondito di [Creare estensioni |extensions]. - -Non vi serve nulla di tutto questo per scrivere una normale applicazione, e nemmeno una normale estensione. Ma quando la vostra estensione inizia a esaminare o a rimodellare il grafo dei servizi, il momento diventa tutto: la stessa chiamata a `getByType()` dà una risposta affidabile in una fase e una fuorviante in un'altra. Questa pagina spiega perché, così saprete sempre dove collocare il vostro codice. - - -Due mondi: compilazione ed esecuzione -===================================== - -La cosa più importante da capire è che un container di Nette **non viene assemblato a ogni richiesta**. Viene costruito una sola volta in una classe PHP ottimizzata, quella classe viene salvata su disco e ogni richiesta successiva si limita a fare `include` del file già pronto. Tutta la macchina descritta qui sotto (estensioni, resolver, generatore di codice) gira **solo durante la (ri)compilazione**. - -Questo divide il mondo in due rappresentazioni che non coesistono mai: - -| | durante la compilazione | in fase di esecuzione -|---|---|--- -| Cosa esiste | **definizioni** (ricette) in `ContainerBuilder` | **istanze** dei servizi in `Container` -| Classi chiave | `Compiler`, `ContainerBuilder`, `Resolver`, `PhpGenerator` | `Container` (genitore della classe generata) -| `%param%`, `@service` | marcatori testuali ancora da tradurre | già tradotti / incorporati nel codice - -La classe generata estende `Nette\DI\Container` e ha un metodo `createServiceXxx()` per ogni servizio. I suoi parametri e i metadati dell'autowiring sono precalcolati, quindi in fase di esecuzione non resta nulla da risolvere: solo da istanziare i servizi su richiesta. - -.[note] -In modalità di sviluppo il container viene ricostruito automaticamente ogni volta che cambiano un file di configurazione o una classe di estensione; entrambi sono tracciati come dipendenze. In produzione viene compilato una sola volta e non viene più controllato, ed è da lì che viene la velocità. - - -Le fasi a colpo d'occhio -======================== - -La compilazione è orchestrata da `Compiler::compile()` e si riduce a tre passaggi: - -```php -public function compile(): string -{ - $this->processExtensions(); // FASE A: schemi + loadConfiguration() - $this->processBeforeCompile(); // FASE B: resolve + beforeCompile() + complete - return $this->generateCode(); // FASE C: generazione del codice + afterCompile() -} -``` - -L'intero modello mentale sta in un'unica idea: **ogni fase sa più della precedente.** - -- **La fase A** riempie il grafo di definizioni. I **tipi dei servizi non sono ancora noti con certezza**, perché un tipo può provenire dal valore di ritorno di una factory che nessuno ha ancora esaminato. -- **La fase B** risolve prima tutti i tipi (`resolve`), poi lascia che le estensioni rimodellino il grafo (`beforeCompile`) e infine esegue l'[autowiring |autowiring] degli argomenti (`complete`). -- **La fase C** trasforma il grafo finito in PHP e lascia che le estensioni intervengano sul codice generato. - -È proprio questa conoscenza crescente a rendere la stessa operazione sicura in una fase e inaffidabile in un'altra. Il resto di questa pagina percorre le fasi tenendo a mente questa idea. - - -Fase A: registrazione delle definizioni -======================================= - -In questa fase Nette chiama tre metodi su ogni estensione (`getConfigSchema()`, poi `setConfig()`, poi `loadConfiguration()`), ma in un **ordine attentamente controllato**, perché qui l'ordine conta davvero. - - -Perché l'ordine conta ---------------------- - -- **`ParametersExtension` ed `ExtensionsExtension` vengono per prime.** La prima deve girare prima di tutto il resto per poter espandere `%param%` in tutta la configurazione: ogni altra estensione riceve poi la propria sezione con i valori già inseriti. La seconda registra le ulteriori estensioni elencate nella sezione `extensions:`, quindi anche lei deve esistere prima che vengano elaborate le altre. -- **`ServicesExtension` viene per ultima.** La sezione `services:` dell'utente ha quindi sempre l'ultima parola e può sovrascrivere qualsiasi cosa impostata dalle estensioni. -- **`InjectExtension` è spostata proprio alla fine**, così che il suo lavoro veda i setup aggiunti da tutte le altre estensioni. - -La conclusione per voi: quando il `loadConfiguration()` della vostra estensione viene eseguito, i parametri sono già espansi, ma i servizi dell'utente non ci sono ancora. Questo solo fatto guida quasi tutte le regole temporali qui sotto. - - -Da services: alle definizioni ------------------------------ - -La sezione `services:` dell'utente viene trasformata qui in [oggetti definizione |extensions#Tipi di definizione], nell'ultimo passaggio della fase A. Ogni voce NEON viene normalizzata (le notazioni abbreviate vengono uniformate), ne viene rilevato il tipo (servizio comune, factory, accessor, ...) e nel builder viene creata la definizione corrispondente. È anche il primo momento in cui i semplici argomenti `@name` / `@Type` diventano riferimenti, vedi [qui sotto |#Riferimenti: quando @service diventa un riferimento]. - -Alla fine della fase A tutte le definizioni sono presenti (ogni estensione e l'utente hanno registrato quel che volevano), ma il quadro non è ancora nitido: - -- **i tipi non sono risolti** per le definizioni il cui tipo proviene dal valore di ritorno di una factory, -- **gli argomenti non hanno subito l'autowiring**, -- alcuni riferimenti `@service` sono ancora semplici stringhe. - -È proprio per questo che qui la ricerca per tipo è inaffidabile: maggiori dettagli [qui sotto |#Esaminare ContainerBuilder: quando è sicuro]. - - -Parametri: quando viene espanso %param% -======================================= - -Una delle due domande principali. La risposta è breve: **una sola volta, proprio all'inizio della fase A, in tutto l'albero della configurazione.** - -`ParametersExtension` gira per prima e una delle prime cose che fa è espandere i segnaposto `%param%`: prima dentro i parametri stessi (un parametro può fare riferimento a un altro), poi in tutto il resto della configurazione. Quando quindi una qualsiasi altra estensione, compresa `ServicesExtension`, riceve la propria sezione, i segnaposto sono già spariti. Le estensioni lavorano con valori concreti, mai con `%...%`. - -Quando un segnaposto è l'intera stringa, il suo valore viene restituito *così com'è*, array e oggetti compresi, quindi `%mailer%` può espandersi in un intero array. In qualsiasi altra posizione viene concatenato in una stringa, e la notazione con il punto `%foo.bar%` raggiunge gli array annidati. - - -Parametri statici e dinamici ----------------------------- - -Non tutti i valori si possono incorporare nel codice. Un parametro il cui valore differisce da un ambiente all'altro (una variabile d'ambiente, il `baseUrl` ricavato dalla richiesta) deve restare **dinamico**. Parametri del genere si dichiarano con `setDynamicParameterNames()` oppure con `Expect::...->dynamic()` in uno schema; maggiori dettagli in [Parametri dinamici |application:bootstrapping#Parametri dinamici]. - -Un parametro dinamico non viene sostituito da un valore, ma da un'espressione che lo legge *in fase di esecuzione*. Perciò `%env.DB_HOST%` non si congela in una stringa: diventa una lettura a runtime nel container generato. Tutto il resto è statico e viene congelato in fase di compilazione, ed è la solita fonte della sorpresa "il valore del mio `getenv()` è uguale in ogni ambiente": il parametro era semplicemente statico. - -L'operazione opposta è l'**escaping**: perché un `%` o una `@` letterali non vengano interpretati, si raddoppiano (`%%`, `@@`). Nette lo fa automaticamente per i parametri che inserisce al posto vostro, così i loro valori non vengono mai scambiati per segnaposto o riferimenti. - - -Riferimenti: quando @service diventa un riferimento -=================================================== - -La seconda domanda principale. La traduzione di `@service` avviene **in più passaggi, in fasi diverse**, a seconda di quanto la stringa sia complessa. Raramente vi serve seguirla a mano, ma conoscere i passaggi spiega perché alcuni riferimenti si risolvono prima di altri. - -- **Analisi (caricamento della configurazione).** Una `@service` usata *come entità*, cioè come la cosa che crea un servizio, come in `Foo(@bar)`, diventa subito un riferimento. Una `@service` usata *come argomento* resta per ora una semplice stringa. Una `@` tra apici viene escapata in `@@`, quindi conta come testo letterale, non come riferimento. -- **Fase A (`loadConfiguration`).** Quando le definizioni vengono elaborate, un argomento `@name` o `@Type` pulito viene trasformato in un oggetto `Reference`. Questo riguarda solo le forme semplici; `@service::CONST` o una `@` dentro un'espressione più ampia restano per dopo. -- **Fase B (`complete`).** La vera traduzione "intelligente" avviene qui: `@service` → riferimento, `@service::CONSTANT` → una costante di classe letterale, `@service::property` → la lettura di quella proprietà, `@@x` → il testo letterale `@x`. - -C'è una seconda traduzione nascosta nella parola *riferimento* stessa. Un `Reference` può puntare per **nome** oppure per **tipo** (`@Namespace\Type`). Un riferimento per tipo **non è ancora un nome di servizio**: viene risolto in un nome concreto dall'autowiring, e questo avviene solo nel passaggio **complete**, una volta costruito l'indice dell'autowiring. È il ponte verso la sezione successiva: le ricerche dell'autowiring sono volutamente rimandate finché l'indice non è pronto. - -| Forma | Diventa riferimento/espressione in | Risolta in un servizio concreto in -|---|---|--- -| entità (`@foo` come factory) | analisi | complete -| argomento `@foo`, `@Type` | fase A | complete -| `@foo::CONST`, `@foo::prop` | fase B | complete -| riferimento per tipo `@Type` | fase A/B | complete (autowiring) - - -Esaminare ContainerBuilder: quando è sicuro -=========================================== - -Ed ecco la domanda che gli autori di estensioni pongono più spesso: **in quale metodo posso cercare i servizi per tipo?** La risposta deriva da una semplice regola su come il builder tiene traccia del proprio stato. - -La ricerca **per tipo** (`getByType()`, `getDefinitionByType()`, `findByType()`) richiede che il grafo dei servizi sia *risolto*: ogni tipo noto, l'indice dell'autowiring costruito. Perciò ogni volta che chiamate uno di questi metodi e il grafo è cambiato dall'ultimo resolve, il builder **risolve sul posto l'intero grafo noto**. Durante il resolve stesso, qualsiasi ricerca per tipo è vietata e solleva `NotAllowedDuringResolvingException`. - -La ricerca **per tag** (`findByTag()`) non ha questo requisito: i tag non dipendono dai tipi, quindi funziona in **ogni fase**. - -Fase per fase: - -- **`loadConfiguration()` (fase A): la ricerca per tipo è inaffidabile.** Il grafo è incompleto: le estensioni che girano dopo non hanno ancora registrato i propri servizi e, soprattutto, la sezione `services:` dell'utente (che gira per ultima) non c'è. Una chiamata a `getByType()` funziona, perché provoca un resolve anticipato di un grafo parziale, ma la risposta viene da un quadro incompleto e il resolve prematuro spreca lavoro. Regola pratica: **in `loadConfiguration()` limitatevi a registrare le definizioni; non cercate per tipo.** `findByTag()` va bene. -- **`beforeCompile()` (fase B): il posto giusto per esaminare.** A questo punto **tutte** le definizioni esistono (comprese quelle dell'utente), i **tipi sono risolti** e l'**indice dell'autowiring è costruito**, quindi `getByType()`, `findByType()` e `findByTag()` restituiscono tutti risposte **affidabili**. Gli argomenti *non* hanno ancora subito l'autowiring: è il passaggio immediatamente successivo (`complete`), dopo tutte le chiamate a `beforeCompile()`. Quando qui modificate una definizione, il successivo `getByType()` risolve di nuovo il grafo in modo trasparente, così potete alternare liberamente modifiche e interrogazioni. -- **`afterCompile()` (fase C): solo codice.** Lavora sulla classe generata, non sul builder. Il grafo è finito; qui plasmate il PHP risultante. - -| Voglio... | Fase -|---|--- -| registrare un servizio | `loadConfiguration()` -| cercare per **tag** e modificare le definizioni | `loadConfiguration()` oppure `beforeCompile()` -| cercare per **tipo** (`getByType`/`findByType`) | **`beforeCompile()`** -| dipendere dai servizi scelti dall'autowiring per gli argomenti | non in fase di compilazione: esaminatelo a runtime -| intervenire sul codice generato | `afterCompile()` -| eseguire codice dopo l'avvio del container | [codice di inizializzazione |extensions#Codice di inizializzazione] - - -Dentro la fase B: resolve e complete -==================================== - -La fase B è composta da due passaggi, con in mezzo le chiamate a `beforeCompile()`: - -```php -$this->builder->resolve(); // tipi risolti, indice dell'autowiring costruito -foreach ($this->extensions as $extension) { - $extension->beforeCompile(); -} -$this->builder->complete(); // SOLO ORA gli argomenti subiscono l'autowiring -``` - -**`resolve()`** determina il tipo di ogni servizio (preso dal suo `type` dichiarato oppure dedotto dalla sua factory: il tipo di ritorno di un metodo factory, la classe che istanzia o il servizio a cui punta un riferimento) e costruisce poi l'indice dell'autowiring, che associa ogni tipo (la classe più i suoi genitori e le sue interfacce) a un nome di servizio. Un servizio contrassegnato con `autowired: false` resta fuori dall'indice; `autowired: [A, B]` restringe i tipi sotto i quali è visibile. Fondamentale: il resolve fissa i *tipi*, non gli *argomenti*: per l'autowiring degli argomenti servirebbe l'indice finito, che esiste solo dopo questo passaggio. - -**`complete()`** è dove avviene davvero l'autowiring degli argomenti. Per ogni definizione riempie gli argomenti mancanti del costruttore e del setup cercandone i tipi nell'indice ormai completo. È per questo che i riferimenti per tipo erano rimasti irrisolti durante il resolve: la ricerca appartiene a questa fase, quando esiste un indice affidabile in cui cercare. - - -Fase C: generazione del codice -============================== - -`generateCode()` consegna il grafo finito a `PhpGenerator`, che produce una classe che estende `Container` con un metodo `createServiceXxx()` per ogni servizio, più i metadati precalcolati `aliases`, `tags` e `wiring`. Ogni `Statement` diventa testo PHP (`new Foo(...)`, chiamate di metodo, accesso alle proprietà) e ogni `Reference` diventa una chiamata a `$this->getService(...)`. - -Le estensioni ottengono poi un ultimo passaggio `afterCompile()` sulla classe generata: è qui che vengono emessi, per esempio, i getter dei parametri statici e dinamici, ed è qui che potete aggiungere il [codice di inizializzazione |extensions#Codice di inizializzazione] che gira a ogni richiesta. - - -La linea del tempo in un'immagine -================================= - -``` -COMPILAZIONE (una volta sola, nella cache) -│ -├─ carica i file di config NEON -> Statement/array; unisce i file -│ @ tra apici -> @@ ; entità -> Statement -│ -▼ Compiler::compile() -│ -├─ FASE A processExtensions() -│ ├─ ParametersExtension (PRIMA) ── %param% ESPANSI in tutta la config -│ │ quelli dinamici -> espressione a runtime -│ ├─ ExtensionsExtension (PRIMA) ── registra ulteriori estensioni -│ ├─ ...altre estensioni... ── loadConfiguration(): solo registrare definizioni -│ └─ ServicesExtension (ULTIMA) ── services: -> oggetti Definition -│ @name/@Type -> Reference -│ [grafo completo per numero; TIPI e ARGOMENTI non ancora; ricerca per tipo inaffidabile] -│ -├─ FASE B processBeforeCompile() -│ ├─ builder.resolve() ── risolve tutti i tipi; costruisce l'indice autowiring -│ │ [tipi pronti; indice pronto] -│ ├─ beforeCompile() estensioni ── qui getByType/findByType/findByTag SICURI -│ │ (gli argomenti non hanno ancora l'autowiring) -│ └─ builder.complete() ── autowiring degli ARGOMENTI; fine traduzione riferimenti -│ riferimenti per tipo -> nomi di servizio -│ -└─ FASE C generateCode() - ├─ PhpGenerator.generate() ── Statement -> PHP; metodi createServiceXxx() - ├─ afterCompile() estensioni ── ritocca il codice; emette i getter dei parametri - └─ toString() ── codice PHP finale -> cache - -──────────────────────────────────────────────────────────── - -ESECUZIONE (a ogni richiesta) -│ -├─ new Container($dynamicParams) -├─ initialize() ── codice di avvio delle estensioni (sessione, header, validazione) -└─ getService()/getByType() ── istanze pigre dai metadati precalcolati -``` - - -Fraintendimenti comuni -====================== - -- "In `loadConfiguration()` cercherò i servizi per tipo." No: il grafo è incompleto (la sezione `services:` dell'utente gira dopo di voi) e `getByType()` provoca un resolve prematuro di un grafo parziale. Spostatelo in `beforeCompile()`. `findByTag()` va bene anche qui. -- "Il valore di un `getenv()` in un parametro sarà diverso in ogni ambiente." Solo se il parametro è dinamico. Altrimenti viene incorporato in fase di compilazione e resta uguale ovunque. -- "Un riferimento `@Type` è già un nome di servizio." Non lo è: è un riferimento per tipo, risolto in un nome concreto dall'autowiring solo nel passaggio complete. -- "La mia estensione legge un file di supporto, ma le modifiche non compaiono." Registratelo con `$builder->addDependency($file)`, altrimenti la cache non ne sa nulla e non ricostruirà. -- "Durante `resolve()` posso chiamare `getByType()`." No: solleva `NotAllowedDuringResolvingException`. La ricerca per tipo appartiene a `beforeCompile()` o a fasi successive, mai in mezzo al resolve. diff --git a/dependency-injection/it/configuration.texy b/dependency-injection/it/configuration.texy deleted file mode 100644 index 002ee915cc..0000000000 --- a/dependency-injection/it/configuration.texy +++ /dev/null @@ -1,335 +0,0 @@ -Configurazione del container DI -******************************* - -.[perex] -Panoramica delle opzioni di configurazione del container DI di Nette. - - -File di configurazione -====================== - -Il container DI di Nette si governa facilmente con i file di configurazione. Di norma si scrivono nel [formato NEON|neon:format]. Consigliamo di usare [editor con supporto |tools:ide] per questo formato. - -<pre> -"decorator .[prism-token prism-atrule]":[#Decorator]: "Decorator .[prism-token prism-comment]"<br> -"di .[prism-token prism-atrule]":[#DI]: "Container DI .[prism-token prism-comment]"<br> -"extensions .[prism-token prism-atrule]":[#Estensioni]: "Installazione di altre estensioni DI .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#Inclusione di file]: "Inclusione di file .[prism-token prism-comment]"<br> -"parameters .[prism-token prism-atrule]":[#Parametri]: "Parametri .[prism-token prism-comment]"<br> -"search .[prism-token prism-atrule]":[#Search]: "Registrazione automatica dei servizi .[prism-token prism-comment]"<br> -"services .[prism-token prism-atrule]":[services]: "Servizi .[prism-token prism-comment]" -</pre> - -.[note] -Per scrivere una stringa che contiene il carattere `%`, dovete effettuare l'escape raddoppiandolo in `%%`. - - -Parametri -========= - -Nella configurazione potete definire parametri, che si possono poi usare all'interno delle definizioni dei servizi. Questo vi permette di rendere più chiara la configurazione o di centralizzare i valori che potrebbero cambiare. - -```neon -parameters: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: secret -``` - -Facciamo riferimento al parametro `dsn` in qualsiasi punto della configurazione con la notazione `%dsn%`. I parametri si possono usare anche dentro le stringhe, come `'%wwwDir%/images'`. - -I parametri non devono essere per forza solo stringhe o numeri: possono contenere anche array: - -```neon -parameters: - mailer: - host: smtp.example.com - secure: ssl - user: franta@gmail.com - languages: [cs, en, de] -``` - -Facciamo riferimento a una chiave specifica come `%mailer.user%`. - -Se il vostro codice (per esempio una classe) ha bisogno del valore di un parametro, passatelo alla classe. Per esempio nel costruttore. Non esiste un oggetto di configurazione globale a cui le classi possano chiedere i valori dei parametri. Sarebbe una violazione del principio della dependency injection. - - -Servizi -======= - -Vedi il [capitolo dedicato|services]. - - -Decorator -========= - -Come modificare in un colpo solo più servizi di un certo tipo? Per esempio, come chiamare un determinato metodo su tutti i presenter che ereditano da una certa classe base? A questo serve il decorator. - -```neon -decorator: - # per tutti i servizi che sono istanze di questa classe o interfaccia - App\Presentation\BasePresenter: - setup: - - setProjectId(10) # chiama questo metodo - - $absoluteUrls = true # e imposta la variabile -``` - -I decorator si possono usare anche per impostare i [tag |services#Tag] o per attivare la [modalità inject |services#Modalità inject]. - -```neon -decorator: - InjectableInterface: - tags: [mytag: 1] - inject: true -``` - - -DI -=== - -Impostazioni tecniche del container DI. - -```neon -di: - # mostrare il DIC nella Tracy Bar? - debugger: ... # (bool) di norma per rilevamento automatico (attivo quando Tracy è presente) - - # tipi di parametro a cui non applicare mai l'autowiring - excluded: ... # (string[]) - - # attivare la creazione pigra dei servizi? - lazy: ... # (bool) di norma false - - # la classe da cui eredita il container DI - parentClass: ... # (string) di norma Nette\DI\Container -``` - - -Servizi pigri .{data-version:3.2.4} ------------------------------------ - -Impostare `lazy: true` attiva la creazione pigra (differita) dei servizi. Significa che i servizi non vengono creati davvero nel momento in cui li si chiede al container DI, ma solo al momento del loro primo uso. Questo può accelerare l'avvio dell'applicazione e ridurre l'uso della memoria, perché vengono creati solo i servizi effettivamente necessari a una determinata richiesta. - -Per un servizio specifico la creazione pigra si può [regolare |services#Servizi pigri]. - -.[note] -Gli oggetti pigri si possono usare solo per le classi definite dall'utente, non per le classi interne di PHP. Richiede PHP 8.4 o successivo. - - -Esportazione dei metadati -------------------------- - -La classe del container DI contiene anche molti metadati. Potete ridurne la dimensione riducendo l'esportazione dei metadati. - -```neon -di: - export: - # esportare i parametri? - parameters: false # (bool) di norma true - - # esportare i tag e quali? - tags: # (string[]|bool) di norma tutti - - event.subscriber - - # esportare i dati per l'autowiring e quali? - types: # (string[]|bool) di norma tutti - - Nette\Database\Connection - - Symfony\Component\Console\Application -``` - -Se non usate `$container->getParameters()`, potete disattivare l'esportazione dei parametri. Potete inoltre esportare solo i tag che usate davvero per ottenere i servizi con `$container->findByTag(...)`. Se non chiamate affatto questo metodo, potete disattivare completamente l'esportazione dei tag con `false`. - -Potete ridurre notevolmente i metadati per l'[autowiring|autowiring] elencando solo le classi che chiedete davvero con `$container->getByType()`. Anche qui, se non chiamate questo metodo (o lo chiamate solo nel file di [bootstrap|application:bootstrapping], per esempio per ottenere `Nette\Application\Application`), potete disattivare completamente l'esportazione dei tipi con `false`. - - -Estensioni -========== - -Registrazione di ulteriori estensioni DI. Ecco come aggiungete, per esempio, l'estensione DI `Dibi\Bridges\Nette\DibiExtension3` con il nome `dibi`: - -```neon -extensions: - dibi: Dibi\Bridges\Nette\DibiExtension3 -``` - -La configurate poi nella sezione `dibi`: - -```neon -dibi: - host: localhost -``` - -Come estensione potete aggiungere anche una classe con parametri: - -```neon -extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, [%appDir%], %tempDir%/cache) -``` - - -Inclusione di file -================== - -Ulteriori file di configurazione si possono includere nella sezione `includes`: - -```neon -includes: - - parameters.php - - services.neon - - presenters.neon -``` - -Il nome `parameters.php` non è un errore di battitura: la configurazione si può scrivere anche in un file PHP che la restituisce come array: - -```php -<?php -return [ - 'database' => [ - 'main' => [ - 'dsn' => 'sqlite::memory:', - ], - ], -]; -``` - -Se in più file di configurazione compaiono elementi con le stesse chiavi, essi verranno sovrascritti oppure, nel caso degli array, [uniti |#Unione]. Un file incluso più tardi ha priorità maggiore rispetto al precedente. Il file in cui è indicata la sezione `includes` ha priorità maggiore rispetto ai file inclusi al suo interno. - - -Search -====== - -La registrazione automatica dei servizi nel container DI semplifica notevolmente lo sviluppo. Nette aggiunge automaticamente al container i presenter, ma potete aggiungervi facilmente anche qualsiasi altra classe. - -Basta indicare in quali directory (e sottodirectory) cercare le classi: - -```neon -search: - - in: %appDir%/Forms - - in: %appDir%/Model -``` - -Se vi serve una sola regola di ricerca, potete omettere l'elenco e scriverne le chiavi direttamente sotto `search`: - -```neon -search: - in: %appDir% -``` - -Di norma, però, non vogliamo aggiungere assolutamente tutte le classi e le interfacce, quindi possiamo filtrarle: - -```neon -search: - - in: %appDir%/Forms - - # filtraggio per nome di file (string|string[]) - files: - - *Factory.php - - # filtraggio per nome di classe (string|string[]) - classes: - - *Factory -``` - -Oppure possiamo selezionare le classi che ereditano o implementano almeno una delle classi elencate: - - -```neon -search: - - in: %appDir% - extends: - - App\*Form - implements: - - App\*FormInterface -``` - -Potete definire anche regole di esclusione, con maschere di nomi di classe o di antenati. Se una classe corrisponde a una regola di esclusione, non verrà aggiunta al container DI: - -```neon -search: - - in: %appDir% - exclude: - files: ... - classes: ... - extends: ... - implements: ... -``` - -A tutti i servizi registrati automaticamente si possono assegnare dei tag: - -```neon -search: - - in: %appDir% - tags: ... -``` - -Oltre alle classi, la ricerca registra anche le interfacce che hanno un unico metodo `create()` o `get()`, come [factory o accessor generati |factory]. Le classi per cui nel container è già registrato un servizio dello stesso tipo vengono saltate, così non si creano duplicati. - - -Unione -====== - -Se in più file di configurazione compaiono elementi con le stesse chiavi, essi verranno sovrascritti oppure, nel caso degli array, uniti. Il file incluso più tardi ha priorità maggiore rispetto al precedente. - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>risultato</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> - <td> -```neon -items: - - 1 - - 2 - - 3 -``` - </td> -</tr> -</table> - -Per gli array l'unione si può impedire aggiungendo un punto esclamativo dopo il nome della chiave: - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>risultato</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items!: - - 3 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> -</tr> -</table> - -{{maintitle: Configurazione della dependency injection}} diff --git a/dependency-injection/it/container.texy b/dependency-injection/it/container.texy deleted file mode 100644 index 2d70848770..0000000000 --- a/dependency-injection/it/container.texy +++ /dev/null @@ -1,142 +0,0 @@ -Che cos'è un container DI? -************************** - -.[perex] -Un container di dependency injection (DIC o container DI) è un oggetto che si occupa di istanziare e configurare altri oggetti (chiamati servizi). - -Vi sorprenderà, ma in molti casi non avete bisogno di un container di dependency injection per sfruttare i vantaggi della dependency injection (in breve DI). In fondo, anche nel [capitolo introduttivo|introduction] abbiamo mostrato esempi concreti di DI, e nessun container è stato necessario. - -Quando però si gestisce un gran numero di oggetti con dipendenze complesse, un container DI diventa molto utile. È spesso il caso delle applicazioni web costruite su un framework. - -Nel capitolo precedente abbiamo introdotto le classi `Article` ed `EditController`. Entrambe hanno dipendenze, cioè il database e la factory `ArticleFactory`. E per queste classi creeremo ora un container. Naturalmente creare un container per un esempio così semplice è eccessivo, ma lo creeremo per mostrare che aspetto ha e come funziona. - -Ecco un semplice container scritto a mano per l'esempio qui sopra: - -```php -class Container -{ - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection('mysql:', 'root', '***'); - } - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->createDatabase()); - } - - public function createEditController(): EditController - { - return new EditController($this->createArticleFactory()); - } -} -``` - -L'uso avrebbe questo aspetto: - -```php -$container = new Container; -$controller = $container->createEditController(); -``` - -Chiediamo semplicemente l'oggetto al container, senza bisogno di sapere come crearlo o quali siano le sue dipendenze: se ne occupa tutto il container. Le dipendenze vengono iniettate automaticamente dal container. È questa la sua forza. - -Al momento il container ha tutte le informazioni scritte a mano nel codice. Facciamo quindi il passo successivo e aggiungiamo dei parametri, per rendere il container davvero utile: - -```php -class Container -{ - public function __construct( - private array $parameters, - ) { - } - - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection( - $this->parameters['db.dsn'], - $this->parameters['db.user'], - $this->parameters['db.password'], - ); - } - - // ... -} - -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); -``` - -I lettori più attenti noteranno un problema. Ogni volta che otteniamo un oggetto `EditController` vengono create anche nuove istanze di `ArticleFactory` e della connessione al database. Non è affatto quello che vogliamo. - -Aggiungeremo quindi un metodo `getService()`, che restituirà sempre le stesse istanze: - -```php -class Container -{ - private array $services = []; - - public function __construct( - private array $parameters, - ) { - } - - public function getService(string $name): object - { - if (!isset($this->services[$name])) { - // getService('Database') chiamerà createDatabase() - $method = 'create' . $name; - $this->services[$name] = $this->$method(); - } - return $this->services[$name]; - } - - // ... -} -``` - -Alla prima chiamata, per esempio `$container->getService('Database')`, chiama `createDatabase()` per creare l'oggetto database, lo salva nell'array `$services` e lo restituisce. Alle chiamate successive restituisce direttamente l'istanza già salvata. - -Modifichiamo anche il resto del container perché usi `getService()`: - -```php -class Container -{ - // ... - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->getService('Database')); - } - - public function createEditController(): EditController - { - return new EditController($this->getService('ArticleFactory')); - } -} -``` - -A proposito, il termine servizio indica qualsiasi oggetto gestito dal container. Da qui il nome del metodo `getService()`. - -Fatto. Abbiamo un container DI pienamente funzionante! E possiamo usarlo: - -```php -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); - -$controller = $container->getService('EditController'); -$database = $container->getService('Database'); -``` - -Come vedete, scrivere un DIC non è difficile. Vale la pena notare che gli oggetti stessi non sanno che a crearli è un container. Di conseguenza è possibile creare così qualsiasi oggetto PHP, senza modificarne il codice sorgente. - -Creare e mantenere a mano la classe del container può però diventare rapidamente un incubo. Nel prossimo capitolo parleremo quindi di [Nette DI Container|nette-container], che sa generarsi e aggiornarsi quasi automaticamente. - - -{{maintitle: Che cos'è un container di dependency injection?}} diff --git a/dependency-injection/it/extensions.texy b/dependency-injection/it/extensions.texy deleted file mode 100644 index b9cea84239..0000000000 --- a/dependency-injection/it/extensions.texy +++ /dev/null @@ -1,443 +0,0 @@ -Creare estensioni per Nette DI -****************************** - -.[perex] -Un'estensione è una classe che si aggancia alla compilazione del container DI. Può registrare servizi da codice, validare la propria sezione di configurazione, modificare i servizi definiti da altri e perfino alterare il codice del container generato. Questa pagina vi insegna a scriverne una, cosa succede e quando, e a cosa fare attenzione. - -Le estensioni sono il modo nativo in cui i pacchetti si integrano in Nette: tutti i pacchetti `nette/*` le usano, e anche il vostro può farlo. Un'estensione tipica fa una o più di queste cose: - -- **integra una libreria**: ne registra i servizi nel container ed espone una sezione di configurazione amichevole e validata (è da lì che vengono le sezioni `mail:` o `database:`) -- **automatizza la registrazione**: registra in un ciclo, o in base a una regola, molti servizi simili, che sarebbe noioso elencare in `services:` -- **fa modifiche trasversali**: trova i servizi registrati da altri e li completa, per esempio aggancia un logger a ogni servizio con un certo tag - -Per il lavoro quotidiano su un'applicazione ne avete raramente bisogno: la sezione [services |services] della configurazione basta a registrare e collegare le vostre classi. Ricorrete a un'estensione quando la sola configurazione non basta più. - -Un'estensione si attiva nella sezione `extensions`. Ecco come aggiungete un'estensione rappresentata dalla classe `BlogExtension` con il nome `blog`: - -```neon -extensions: - blog: BlogExtension -``` - -Se il suo costruttore accetta argomenti, passateli lì: - -```neon -extensions: - blog: BlogExtension(%debugMode%) -``` - - -Come funziona la compilazione -============================= - -Per scrivere estensioni con sicurezza dovete conoscere una cosa fondamentale: **quando gira il vostro codice**. Nette non collega i servizi mentre gestisce le richieste. Invece *compila* il container in anticipo: legge tutti i file di configurazione, lascia lavorare le estensioni e genera una classe PHP ottimizzata, che salva su disco. Ogni richiesta successiva si limita a caricare questa classe già pronta. Il codice della vostra estensione gira quindi solo quando il container viene (ri)costruito, non a ogni richiesta. - -Questo ha una conseguenza importante: durante la compilazione non esiste ancora alcun servizio. Esistono le **definizioni**, cioè ricette che descrivono quale classe sarà ogni servizio, come crearlo e cosa chiamarci sopra dopo. Le definizioni vivono nell'oggetto [ContainerBuilder |#ContainerBuilder]. Un'estensione è in sostanza *configurazione programmabile*: tutto ciò che potete dichiarare nella sezione `services:` lo potete anche costruire in PHP, in modo condizionale, in cicli o reagendo a ciò che altri hanno registrato. - -La compilazione procede in fasi, e un'estensione può intervenire in ciascuna di esse: - -1) vengono validate le sezioni di configurazione di tutte le estensioni (`getConfigSchema()`) -2) ogni estensione registra i propri servizi (`loadConfiguration()`); la sezione `services:` dell'utente viene elaborata per ultima, così l'applicazione ha sempre l'ultima parola -3) una volta che tutte le definizioni sono a posto e i tipi dei servizi sono risolti, le estensioni possono modificarle (`beforeCompile()`) -4) viene generata la classe del container; le estensioni possono ancora ritoccarne il codice (`afterCompile()`) ed emettere codice che girerà all'avvio dell'applicazione ([inizializzazione |#Codice di inizializzazione]) - -.[note] -In modalità di sviluppo il container viene ricompilato automaticamente ogni volta che cambiate un file di configurazione o la classe dell'estensione stessa: entrambi sono tracciati come dipendenze. Potete quindi sviluppare estensioni senza mai svuotare la cache. - -.[tip] -Per uno sguardo più approfondito su cosa succede in ogni fase (quando i parametri vengono espansi, quando `@service` diventa un riferimento e quando esattamente è sicuro cercare i servizi per tipo) vedi [La compilazione del container in dettaglio |compilation-internals]. - - -La prima estensione -=================== - -Ecco un'estensione piccola ma completa. La attiviamo e la configuriamo nello stesso file: - -```neon -extensions: - blog: BlogExtension - -blog: - postsPerPage: 5 -``` - -E questa è tutta la classe: - -```php -use Nette\Schema\Expect; - -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function getConfigSchema(): Nette\Schema\Schema - { - return Expect::structure([ - 'postsPerPage' => Expect::int(10), - 'allowComments' => Expect::bool(true), - ]); - } - - - public function loadConfiguration(): void - { - $builder = $this->getContainerBuilder(); - - $builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class, ['postsPerPage' => $this->config->postsPerPage]); - - if ($this->config->allowComments) { - $builder->addDefinition($this->prefix('comments')) - ->setFactory(Blog\Comments::class); - } - } -} -``` - -`getConfigSchema()` descrive cosa può contenere la sezione `blog:` (dal nome della chiave sotto cui abbiamo registrato l'estensione), tipi e valori predefiniti compresi; i valori validati sono poi disponibili in `$this->config`. In `loadConfiguration()` registriamo i servizi. Notate i nomi: `$this->prefix('articles')` produce `blog.articles`, così i servizi di estensioni diverse non possono entrare in conflitto. - -E le ultime righe mostrano perché le estensioni esistono: il servizio `comments` viene registrato solo quando i commenti sono attivi. Un semplice file di configurazione non può prendere decisioni del genere. - -I servizi registrati così si comportano esattamente come se fossero scritti in `services:`: vengono creati pigramente su richiesta e l'autowiring li passa ovunque sia dichiarato il tipo `Blog\Articles`. - -I capitoli seguenti descrivono in dettaglio il ciclo di vita di un'estensione, poi l'API di [ContainerBuilder |#ContainerBuilder] che userete al suo interno e infine le [insidie |#Consigli e insidie] che vale la pena conoscere. - - -Ciclo di vita di un'estensione -============================== - -Un'estensione eredita da [api:Nette\DI\CompilerExtension] e sovrascrive alcuni dei quattro metodi `getConfigSchema()`, `loadConfiguration()`, `beforeCompile()` e `afterCompile()`, che il compilatore chiama in quest'ordine durante la compilazione. - - -getConfigSchema(): Nette\Schema\Schema .[method] ------------------------------------------------- - -Definisce lo schema della sezione di configurazione dell'estensione. Grazie a esso gli utenti ottengono gratis la validazione e messaggi di errore chiari: un errore di battitura o un tipo sbagliato nella sezione `blog:` viene segnalato con un messaggio comprensibile, senza che voi scriviate un solo controllo. - -Lo schema si descrive con la libreria [Schema |schema:] e può esprimere tipi, valori predefiniti, valori ammessi e molto altro: - -```php -public function getConfigSchema(): Nette\Schema\Schema -{ - return Expect::structure([ - 'postsPerPage' => Expect::int(10), - 'storage' => Expect::anyOf('files', 'database')->firstIsDefault(), - ]); -} -``` - -La configurazione validata è disponibile in `$this->config` come oggetto `stdClass` (oppure come array, se allo schema aggiungete `castTo('array')`). - -Se il valore di un'opzione non si può conoscere in fase di compilazione, perché per esempio proviene da una variabile d'ambiente, contrassegnatelo con `dynamic()`, per esempio `Expect::int()->dynamic()`. Maggiori dettagli in [parametri dinamici |application:bootstrapping#Parametri dinamici]. - - -loadConfiguration() .[method] ------------------------------ - -Il posto in cui l'estensione registra i propri servizi, usando [ContainerBuilder |#ContainerBuilder]: - -```php -public function loadConfiguration(): void -{ - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class); -} -``` - -Se un servizio deve essere disponibile anche con un nome breve, aggiungete un alias. Per convenzione lo si fa solo quando l'estensione è registrata con il suo nome consueto, così che più istanze dell'estensione non possano contenderselo: - -```php -if ($this->name === 'blog') { - $builder->addAlias('articles', $this->prefix('articles')); -} -``` - -Quando i servizi sono molti, può essere più comodo definirli in un file NEON separato con la familiare sintassi dei [services |services]. Il prefisso `@extension` fa riferimento all'estensione corrente: - -```neon -services: - articles: - create: MyBlog\ArticlesModel(@connection) - - comments: - create: MyBlog\CommentsModel(@connection, @extension.articles) -``` - -Carichiamo queste definizioni con `loadDefinitionsFromConfig()`; i nomi ricevono automaticamente il prefisso e il file viene tracciato come dipendenza, quindi modificarlo provoca la ricompilazione: - -```php -public function loadConfiguration(): void -{ - $this->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/services.neon')['services'], - ); -} -``` - - -beforeCompile() .[method] -------------------------- - -Quando questo metodo viene chiamato, il builder contiene già **tutte** le definizioni: le vostre, quelle delle altre estensioni e quelle dei file di configurazione dell'utente. Anche i tipi dei servizi sono ormai risolti, quindi la ricerca per tipo è affidabile. Questa fase è quindi ideale per esaminare e completare il grafo finale dei servizi. - -Di norma cercate i servizi per tag o per tipo e completate le definizioni trovate: - -```php -public function beforeCompile(): void -{ - $builder = $this->getContainerBuilder(); - - foreach ($builder->findByTag('logaware') as $name => $attrs) { - $builder->getDefinition($name)->addSetup('setLogger'); - } -} -``` - -La chiamata `setLogger()` non ha argomenti espliciti: li fornirà l'autowiring, esattamente come nelle factory. - -Potete anche collaborare con altre estensioni registrate, ottenute con `$this->compiler->getExtensions()`, filtrandole eventualmente per classe o interfaccia: - -```php -foreach ($this->compiler->getExtensions(FooExtension::class) as $extension) { - // ... -} -``` - - -afterCompile(Nette\PhpGenerator\ClassType $class) .[method] ------------------------------------------------------------ - -Nell'ultima fase la classe del container viene generata come oggetto [ClassType |php-generator:#Classi] della libreria [PHP Generator |php-generator:]. Contiene un metodo factory per ogni servizio ed è sul punto di essere scritta nella cache. Potete ancora modificarne il codice: - -```php -public function afterCompile(Nette\PhpGenerator\ClassType $class): void -{ - $method = $class->getMethod('__construct'); - // ... -} -``` - -Avrete bisogno di questa fase solo di rado. Per aggiungere codice che gira all'avvio dell'applicazione, usate piuttosto l'inizializzazione: - - -Codice di inizializzazione --------------------------- - -Tutte le fasi precedenti influenzano il modo in cui il container viene *costruito*. Un'estensione può inoltre emettere codice che gira *in fase di esecuzione*, subito dopo la creazione del container, per esempio per avviare una sessione o per far partire dei servizi. Il codice si scrive nell'oggetto `$this->initialization` con il suo metodo [addBody() |php-generator:#Corpi di metodi e funzioni]: - -```php -public function loadConfiguration(): void -{ - // i servizi con il tag 'run' vanno creati subito dopo l'avvio del container - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $attrs) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } -} -``` - -Nette stessa usa l'inizializzazione, per esempio, per avviare automaticamente la sessione o per inviare gli header HTTP di sicurezza. E tenete presente: a differenza di tutto il resto in un'estensione, questo codice gira a **ogni richiesta**, quindi mantenetelo essenziale. - - -ContainerBuilder -================ - -[api:Nette\DI\ContainerBuilder] è l'oggetto attraverso il quale un'estensione parla con il compilatore. Contiene le [definizioni |#Come funziona la compilazione] di tutti i servizi e offre metodi per aggiungerle, cercarle e modificarle. Lo ottenete in `loadConfiguration()` e in `beforeCompile()`: - -```php -$builder = $this->getContainerBuilder(); -``` - - -Aggiungere servizi ------------------- - -Registrare un servizio è la stessa cosa che fate nella sezione `services:` di un file NEON, solo scritta in PHP. A ogni chiave della configurazione corrisponde un metodo della definizione, quindi queste due notazioni sono equivalenti: - -```neon -services: - articles: - create: Blog\Articles(@connection) - setup: - - setLogger(@logger) - tags: [logaware] -``` - -```php -$builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class, ['@connection']) - ->addSetup('setLogger', ['@logger']) - ->addTag('logaware'); -``` - -La definizione restituita da `addDefinition()` è una [ServiceDefinition |#Tipi di definizione], che offre le controparti delle chiavi di configurazione: `setType()` (la classe del servizio), `setFactory()` (come crearlo), `setArguments()`, `addSetup()`, `addTag()` e `setAutowired()`. - -`addSetup()` rispecchia l'elenco `setup:` e accetta le stesse forme: una chiamata di metodo `addSetup('setLogger', ['@logger'])`, un'assegnazione a proprietà `addSetup('$cache', ['@cache'])` oppure una chiamata su un altro servizio `addSetup('@Tracy\Bar::addPanel', [$panel])`. - -Oltre ai servizi comuni, il builder può registrare anche [factory |factory] generate, accessor e locator, ognuno con un proprio metodo che restituisce il corrispondente [tipo di definizione |#Tipi di definizione]: - -| Metodo | Registra -|--------|---------- -| `addDefinition()` | un servizio comune (restituisce `ServiceDefinition`) -| `addFactoryDefinition()` | una [factory |factory] generata (interfaccia con un metodo `create()`) -| `addAccessorDefinition()` | un [accessor |factory#Accessor] generato (interfaccia con un metodo `get()`) -| `addLocatorDefinition()` | un [multifactory / locator |factory#Multifactory/accessor] che unisce più factory -| `addImportedDefinition()` | un servizio passato al container dall'esterno in fase di esecuzione -| `addAlias()` | un secondo nome per un servizio esistente - -Con una factory configurate l'oggetto che essa crea tramite `getResultDefinition()`; un accessor punta invece a un servizio esistente con `setReference()`: - -```php -$builder->addFactoryDefinition($this->prefix('latteFactory')) - ->setImplement(LatteFactory::class) - ->getResultDefinition() - ->setFactory(Latte\Engine::class) - ->addSetup('setStrictTypes', [true]); -``` - -`addLocatorDefinition()` e `addImportedDefinition()` servono di rado: servizi del genere provengono di norma dalle chiavi `implement:` e dai servizi importati nel NEON, invece di essere scritti a mano. - - -Cercare e modificare i servizi ------------------------------- - -Per cercare e percorrere le definizioni esistenti, il builder offre: - -| Metodo | Descrizione -|--------|------------ -| `getDefinition(string $name)` | la definizione con il nome indicato (solleva un'eccezione se manca) -| `hasDefinition(string $name)` | se esiste una definizione o un alias con quel nome -| `getDefinitions()` | tutte le definizioni -| `removeDefinition(string $name)` | rimuove una definizione -| `getByType(string $type)` | il nome del servizio autowired di quel tipo, oppure `null` -| `getDefinitionByType(string $type)` | la definizione autowired di quel tipo -| `findByType(string $type)` | tutte le definizioni di quel tipo, come coppie `nome => definizione` -| `findByTag(string $tag)` | i servizi che portano il tag, come coppie `nome => valore del tag` -| `addExcludedClasses(array $types)` | esclude classi e interfacce dall'autowiring - -Un idioma comodo è usare `getByType()` per scoprire se un servizio esiste, per esempio per agganciarsi a un logger solo quando l'applicazione ne ha uno: - -```php -if ($builder->getByType(Psr\Log\LoggerInterface::class)) { - $builder->getDefinition($this->prefix('articles')) - ->addSetup('setLogger'); -} -``` - - -Tipi di definizione -------------------- - -Ogni metodo `add*Definition()` restituisce un tipo diverso di definizione. Tutti estendono l'antenato comune `Nette\DI\Definitions\Definition`: - -- **`ServiceDefinition`**: un servizio comune; si configura con `setType()`, `setFactory()`, `addSetup()`, `addTag()` e `setAutowired()` -- **`FactoryDefinition`**: una [factory generata |factory], cioè un'interfaccia il cui metodo `create()` restituisce un nuovo oggetto a ogni chiamata -- **`AccessorDefinition`**: un [accessor generato |factory#Accessor], cioè un'interfaccia il cui metodo `get()` restituisce un servizio esistente -- **`LocatorDefinition`**: un [multifactory / locator |factory#Multifactory/accessor] che unisce più factory o accessor in un'unica interfaccia -- **`ImportedDefinition`**: un servizio che il container non crea da sé, ma riceve dall'esterno in fase di esecuzione - -Tenete presente che `getDefinition()` restituisce qualsiasi tipo di definizione si trovi sotto il nome indicato. Se il vostro codice può incontrare una factory generata, controllatene prima il tipo e configurate l'oggetto prodotto tramite `getResultDefinition()`: - -```php -$def = $builder->getDefinition($name); -if ($def instanceof Nette\DI\Definitions\FactoryDefinition) { - $def = $def->getResultDefinition(); -} -$def->addSetup('setLogger'); -``` - - -Consigli e insidie -================== - - -Fase di compilazione e fase di esecuzione ------------------------------------------ - -La fonte di confusione più comune: il codice dell'estensione gira quando il container viene **compilato**, non quando l'applicazione gestisce le richieste. In pratica significa che: - -- Un'estensione non lavora mai con istanze di servizi: non esistono ancora. Non istanziate i servizi con `new`; registrate una definizione e lasciate che sia il container a crearli. -- Tutti i valori di configurazione vengono incorporati nel codice generato. Un valore che può differire da un ambiente all'altro (un percorso, una password da `getenv()`) va contrassegnato come [dinamico |application:bootstrapping#Parametri dinamici], altrimenti viene congelato in fase di compilazione. -- Le stringhe passate a `$this->initialization->addBody()` non vengono eseguite ora: sono codice PHP emesso nel container ed eseguito a ogni richiesta. - - -Dipendenze dai file -------------------- - -Il container viene ricompilato quando cambiano i file di configurazione o le classi delle estensioni. Se però la vostra estensione legge qualche altro file (un elenco di entità, una configurazione XML di una libreria), il container non ha modo di saperlo. Registrate file del genere con: - -```php -$builder->addDependency($file); -``` - -Altrimenti vi aspetta un mistero classico: modificate il file, ma l'applicazione continua a comportarsi come prima; la modifica compare solo quando il container viene ricostruito per qualche altro motivo. (I file letti con `loadFromFile()` sono tracciati automaticamente.) - - -Registrazione condizionale --------------------------- - -Un'estensione può adattarsi al proprio ambiente. Le integrazioni facoltative si proteggono di norma con `class_exists()`: - -```php -if (class_exists(Symfony\Component\Console\Command\Command::class)) { - $builder->addDefinition($this->prefix('command')) - ->setFactory(Blog\Console\SitemapCommand::class); -} -``` - -E i valori come `%debugMode%` conviene passarli tramite il costruttore dell'estensione: - -```neon -extensions: - blog: BlogExtension(%debugMode%) -``` - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function __construct( - private bool $debugMode = false, - ) {} -} -``` - -Un uso tipico è registrare un pannello di Tracy solo in modalità di sviluppo. - - -Argomenti complessi -------------------- - -A volte un argomento di una factory o di una chiamata di setup non è un semplice valore, un nome di classe o un riferimento `@service`. Per questi casi esistono: - -- `new Nette\DI\Definitions\Statement(Blog\Panel::class, [$args])`: un oggetto creato sul posto, un "servizio anonimo" usato come argomento -- `new Nette\DI\Definitions\Reference('blog.articles')`: un riferimento a un servizio, la controparte a oggetti della stringa `@nome` -- `$builder::literal('PHP_SAPI')`: un frammento di codice PHP grezzo, inserito così com'è nel container generato - -Esempio: registrare un pannello di Tracy: - -```php -$builder->getDefinition($this->prefix('articles')) - ->addSetup('@Tracy\Bar::addPanel', [ - new Nette\DI\Definitions\Statement(Blog\ArticlesPanel::class), - ]); -``` - - -Tag e tipi esportati --------------------- - -L'[esportazione dei metadati |configuration#Esportazione dei metadati] si può limitare nella configurazione, così che il container compilato conservi solo i tag e i tipi di autowiring che l'applicazione usa davvero. Se la vostra estensione ottiene i servizi in fase di esecuzione con `$container->findByTag()` o `$container->getByType()`, una limitazione del genere potrebbe rimuovere proprio i metadati su cui contate. - -Per evitarlo, dite al compilatore quali tag e quali tipi vanno sempre esportati: - -```php -public function loadConfiguration(): void -{ - // questo tag verrà sempre esportato, anche se l'esportazione è limitata - $this->compiler->addExportedTag('event.subscriber'); - - // questo tipo sarà sempre disponibile per getByType() - $this->compiler->addExportedType(Nette\Database\Connection::class); -} -``` - -Entrambi i metodi si limitano ad aggiungere ai metadati esportati; non sovrascrivono mai la configurazione `di › export` dell'applicazione. Quindi, quando l'applicazione limita l'esportazione a un elenco, i tag e i tipi di cui la vostra estensione ha bisogno restano inclusi; solo disattivando del tutto l'esportazione dei tag (`tags: false`) essi vengono scartati insieme a tutto il resto. diff --git a/dependency-injection/it/factory.texy b/dependency-injection/it/factory.texy deleted file mode 100644 index f7fc9a08d1..0000000000 --- a/dependency-injection/it/factory.texy +++ /dev/null @@ -1,233 +0,0 @@ -Factory generate -**************** - -.[perex] -Nette DI sa generare automaticamente il codice delle factory a partire dalle interfacce, risparmiandovi di scrivere codice. - -Una factory è una classe che si occupa di creare oggetti e di passarne le dipendenze. Non confondetela con il design pattern *factory method*, che descrive un modo specifico di usare le factory e non ha nulla a che vedere con questo argomento. - -Abbiamo mostrato che aspetto ha una factory del genere nel [capitolo introduttivo |introduction#Factory]: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Nette DI sa generare automaticamente il codice della factory. Vi basta creare un'interfaccia e Nette DI ne genererà l'implementazione. L'interfaccia deve avere esattamente un metodo chiamato `create` e dichiarare un tipo di ritorno: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -La factory `ArticleFactory` ha quindi un metodo `create` che crea oggetti `Article`. La classe `Article` potrebbe avere per esempio questo aspetto: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } -} -``` - -Aggiungete la factory al file di configurazione: - -```neon -services: - - ArticleFactory -``` - -Nette DI genererà la corrispondente implementazione della factory. - -Nel codice che usa la factory, chiedete l'oggetto tramite la sua interfaccia e Nette DI vi fornirà l'implementazione generata: - -```php -class UserController -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function foo() - { - // lascia che sia la factory a creare l'oggetto - $article = $this->articleFactory->create(); - } -} -``` - - -Factory con parametri -===================== - -Il metodo `create` della factory può accettare parametri, che passa poi al costruttore. Aggiungiamo per esempio alla classe `Article` l'ID dell'autore dell'articolo: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - private int $authorId, - ) { - } -} -``` - -Aggiungiamo il parametro anche alla factory: - -```php -interface ArticleFactory -{ - function create(int $authorId): Article; -} -``` - -Poiché il nome del parametro nel costruttore (`$authorId`) coincide con il nome del parametro nel metodo della factory, Nette DI lo passa automaticamente. - - -Definizione avanzata -==================== - -La definizione si può scrivere anche su più righe, con la chiave `implement`: - -```neon -services: - articleFactory: - implement: ArticleFactory -``` - -Questo formato più lungo permette di indicare argomenti aggiuntivi per il costruttore tramite la chiave `arguments` e ulteriori configurazioni tramite `setup`, come nelle normali definizioni dei servizi. - -Esempio: se il metodo `create()` non accettasse il parametro `$authorId`, potremmo indicare nella configurazione un valore fisso da passare al costruttore di `Article`: - -```neon -services: - articleFactory: - implement: ArticleFactory - arguments: - authorId: 123 -``` - -Al contrario, se `create()` accettasse `$authorId` ma questo non facesse parte del costruttore e venisse invece passato tramite un metodo come `Article::setAuthorId()`, faremmo riferimento al parametro nella sezione `setup`: - -```neon -services: - articleFactory: - implement: ArticleFactory - setup: - - setAuthorId($authorId) -``` - - -Accessor -======== - -Oltre alle factory, Nette sa generare anche i cosiddetti accessor. Sono oggetti con un metodo `get()` che restituisce un determinato servizio dal container DI. Chiamate ripetute a `get()` restituiscono sempre la stessa istanza. - -Gli accessor offrono il caricamento pigro delle dipendenze. Immaginate una classe che registra gli errori in un database dedicato. Se questa classe ricevesse la connessione al database tramite dependency injection nel costruttore, la connessione verrebbe sempre stabilita, anche se gli errori sono rari e la connessione resta quasi sempre inutilizzata. La classe può invece ricevere un accessor. L'oggetto database (la connessione) viene creato solo quando il metodo `get()` dell'accessor viene chiamato la prima volta. - -Come si crea un accessor? Basta scrivere un'interfaccia e Nette DI ne genererà l'implementazione. L'interfaccia deve avere esattamente un metodo chiamato `get`, senza parametri e con un tipo di ritorno dichiarato: - -```php -interface PDOAccessor -{ - function get(): PDO; -} -``` - -Aggiungete l'accessor al file di configurazione, insieme alla definizione del servizio che deve restituire: - -```neon -services: - - PDOAccessor - - PDO(%dsn%, %user%, %password%) -``` - -Poiché l'accessor restituisce un servizio `PDO` e nella configurazione ne è definito uno solo di quel tipo, l'accessor restituirà quel servizio. Se esistessero più servizi di quel tipo, indicate per nome quale deve restituire l'accessor, per esempio `- PDOAccessor(@db1)`. - - -Multifactory/accessor -===================== - -Finora le nostre factory e i nostri accessor potevano creare o restituire un solo tipo di oggetto. Potete però creare facilmente delle multifactory, che uniscono le caratteristiche delle factory e degli accessor. L'interfaccia di un componente del genere può contenere più metodi chiamati `create<Nome>()` e `get<Nome>()`, per esempio: - -```php -interface MultiFactory -{ - function createArticle(): Article; - function getDb(): PDO; -} -``` - -Invece di iniettare più factory e accessor separati, potete quindi iniettare un unico componente più completo. - -In alternativa ai metodi multipli si può usare `get()` con un parametro: - -```php -interface MultiFactoryAlt -{ - function get($name): PDO; -} -``` - -`MultiFactory::getDb()` fa allora la stessa cosa di `MultiFactoryAlt::get('db')`. Questa notazione alternativa ha però lo svantaggio che i valori supportati per `$name` non sono esplicitamente chiari dalla firma dell'interfaccia. Inoltre nell'interfaccia non potete definire tipi di ritorno diversi per valori diversi di `$name`. - -Al posto di `get($name)` l'interfaccia può dichiarare `create($name)`, che restituisce una nuova istanza a ogni chiamata (mentre `get()` ne restituisce una condivisa). L'interfaccia può contenere un solo metodo con parametro di questo tipo. Se il tipo di ritorno del metodo è nullable (per esempio `?PDO`), per un `$name` sconosciuto restituisce `null` invece di sollevare un'eccezione. - - -Definizione con un elenco -------------------------- -Potete definire una multifactory nella configurazione con un elenco, scrivendo i servizi in linea: .{data-version:3.2.0} - -```neon -services: - - MultiFactory( - article: Article() # definisce createArticle() - db: PDO(%dsn%, %user%, %password%) # definisce getDb() - ) -``` - -In alternativa, nella definizione della multifactory potete fare riferimento a servizi esistenti tramite i riferimenti: - -```neon -services: - article: Article - - PDO(%dsn%, %user%, %password%) - - MultiFactory( - article: @article # definisce createArticle() - db: @\PDO # definisce getDb() - ) -``` - - -Definizione con i tag ---------------------- - -Un altro modo di definire una multifactory è usare i [tag |services#Tag]. Il valore del tag determina il nome del metodo corrispondente: - -```neon -services: - article: - create: Article - tags: {multi: article} # definisce createArticle() - db: - create: PDO(%dsn%, %user%, %password%) - tags: {multi: db} # definisce getDb() - - - MultiFactory(tagged: multi) -``` diff --git a/dependency-injection/it/faq.texy b/dependency-injection/it/faq.texy deleted file mode 100644 index 928dca3dea..0000000000 --- a/dependency-injection/it/faq.texy +++ /dev/null @@ -1,123 +0,0 @@ -Domande frequenti sulla DI (FAQ) -******************************** - - -DI è un altro nome per IoC? ---------------------------- - -L'*Inversion of Control* (IoC) è un principio che descrive il flusso di controllo di un programma: è il vostro codice a chiamare codice esterno, oppure è il codice esterno (per esempio un framework) a chiamare il vostro? IoC è un concetto ampio, che comprende gli [eventi |nette:glossary#Eventi], il cosiddetto [principio hollywoodiano |application:components#Stile hollywoodiano] e altri aspetti. Rientrano in questo concetto anche le factory, di cui si parla nella [Regola n. 3: lasciate fare alla factory |introduction#Regola n. 3: lasciate fare alla factory], che rappresentano un'inversione dell'operatore `new`. - -La *dependency injection* (DI) si concentra su come gli oggetti ottengono le proprie dipendenze (cioè gli altri oggetti con cui devono lavorare). È un design pattern che sostiene di passare esplicitamente le dipendenze agli oggetti, invece di far sì che siano gli oggetti a crearle o a cercarle. - -La DI si può quindi considerare una forma specifica di IoC. Non tutte le forme di IoC, però, favoriscono un codice pulito. Sono per esempio antipattern le tecniche che si affidano allo [stato globale|global-state] o al pattern [Service Locator |#Cos'è un Service Locator?]. - - -Cos'è un Service Locator? -------------------------- - -È un approccio alternativo alla dependency injection. Consiste in un oggetto centrale (il locator) in cui sono registrati tutti i servizi (le dipendenze) disponibili. Quando un oggetto ha bisogno di una dipendenza, la chiede al Service Locator. - -Rispetto alla DI, però, manca di trasparenza. Le dipendenze sono nascoste nel codice dell'oggetto (nelle chiamate al locator) invece di essere esplicite nella sua API (nel costruttore o nei metodi), e per capire i collegamenti bisogna esaminare il codice. Anche i test sono più complessi, perché non potete semplicemente passare dipendenze fittizie istanziando un oggetto: spesso dovete manipolare il Service Locator stesso. Inoltre il Service Locator introduce una dipendenza superflua: gli oggetti diventano accoppiati al locator, a differenza della DI, dove idealmente gli oggetti non sanno nulla del container. - - -Quando è meglio non usare la DI? --------------------------------- - -Non si conoscono svantaggi significativi nell'uso corretto del design pattern della dependency injection. Al contrario, ottenere le dipendenze da posizioni globalmente accessibili (come proprietà statiche o singleton) porta a [numerose complicazioni|global-state], così come l'uso di un Service Locator. Usare la DI è quindi in generale sempre consigliabile. Non è un dogma: semplicemente non si è affermata nessuna alternativa migliore per gestire le dipendenze in modo pulito. - -Esistono però situazioni specifiche e limitate in cui accedere agli oggetti globalmente può essere accettabile. Per esempio durante il debugging, quando dovete scaricare il valore di una variabile, misurare il tempo di esecuzione o registrare un messaggio in un punto preciso. In questi casi, che riguardano azioni temporanee destinate a essere rimosse in seguito dal codice, usare un dumper, un timer o un logger globalmente accessibile può essere legittimo. Questi strumenti non fanno parte della progettazione di fondo dell'applicazione. - - -L'uso della DI ha degli svantaggi? ----------------------------------- - -L'uso della dependency injection porta svantaggi, come più codice da scrivere o prestazioni ridotte? Cosa perdiamo quando iniziamo a scrivere codice conforme alla DI? - -La DI in sé ha un impatto trascurabile sulle prestazioni in fase di esecuzione o sull'uso della memoria. Le prestazioni del container DI possono contare, ma [Nette DI |nette-container] compila il container in semplice codice PHP, con un sovraccarico praticamente nullo durante l'esecuzione dell'applicazione. - -Scrivendo codice secondo i principi della DI, spesso dovete creare costruttori che accettano dipendenze. Se in passato poteva sembrare noioso, gli IDE moderni e funzionalità come la [constructor property promotion |https://blog.nette.org/en/php-8-0-complete-overview-of-news#toc-constructor-property-promotion] di PHP 8 lo rendono velocissimo. Le factory spesso possono essere generate automaticamente da Nette DI, riducendo ulteriormente il codice ripetitivo. D'altra parte, eliminate la necessità di scrivere singleton e accessor statici. - -Nel complesso, un'applicazione ben progettata che usa la DI non è di norma né molto più breve né molto più lunga di una che si affida ai singleton o all'accesso globale. Il codice relativo alla creazione e al collegamento delle dipendenze si sposta semplicemente dalle singole classi a posizioni dedicate: la configurazione del container DI e le factory. - - -Come si riscrive con la DI un'applicazione datata? --------------------------------------------------- - -Migrare un'applicazione datata verso la dependency injection può essere un processo impegnativo, soprattutto per applicazioni grandi e complesse. È importante affrontare questo processo in modo sistematico. - -- Passando alla dependency injection è importante che tutti i membri del team comprendano i principi e le pratiche usate. -- Analizzate per prima cosa l'applicazione esistente per individuarne i componenti chiave e le loro dipendenze. Fate un piano di quali parti verranno rifattorizzate e in quale ordine. -- Implementate un container DI oppure, meglio ancora, usate una libreria esistente come Nette DI. -- Rifattorizzate gradualmente le parti dell'applicazione perché usino la dependency injection. Questo può comportare la modifica dei costruttori o dei metodi perché accettino le dipendenze come parametri. -- Aggiornate il codice in cui gli oggetti vengono istanziati, perché li ottenga dal container o usi le factory fornite dal container. - -Ricordate che passare alla dependency injection è un investimento nella qualità del codice e nella manutenibilità a lungo termine dell'applicazione. Anche se può essere impegnativo apportare queste modifiche, il risultato dovrebbe essere un codice più pulito, più modulare e facilmente testabile, pronto per future estensioni e manutenzioni. - - -Perché si preferisce la composizione all'ereditarietà? ------------------------------------------------------- -Per il riuso del codice si preferisce di norma la [composizione |nette:introduction-to-object-oriented-programming#Composizione] all'[ereditarietà |nette:introduction-to-object-oriented-programming#Ereditarietà], perché porta a un accoppiamento più lasco. Con la composizione è meno probabile incontrare problemi in cui il cambiamento di una classe base rompe le sottoclassi che ne dipendono. Un esempio tipico è la situazione nota come [inferno dei costruttori |passing-dependencies#L'inferno dei costruttori]. - - -Nette DI Container si può usare fuori da Nette? ------------------------------------------------ - -Assolutamente sì. Nette DI Container fa parte di Nette, ma è progettato come libreria autonoma, utilizzabile indipendentemente dalle altre parti del framework. Basta installarlo con Composer, creare un file di configurazione che definisca i vostri servizi e poi usare qualche riga di codice PHP per creare il container DI. E potete subito iniziare a sfruttare la dependency injection nei vostri progetti. - -Il capitolo su [Nette DI Container |nette-container] descrive un caso d'uso concreto con esempi di codice. - - -Perché la configurazione è in file NEON? ----------------------------------------- - -NEON è un linguaggio di configurazione semplice e facilmente leggibile, sviluppato all'interno di Nette per configurare applicazioni, servizi e le loro dipendenze. Rispetto a JSON o YAML offre a questo scopo possibilità molto più intuitive e flessibili. In NEON potete descrivere in modo naturale definizioni di servizi e relazioni che sarebbe difficile o impossibile esprimere con altrettanta chiarezza in JSON o YAML. - - -L'analisi dei file NEON rallenta l'applicazione? ------------------------------------------------- - -Benché i file NEON vengano analizzati molto rapidamente, la velocità della loro analisi è in gran parte irrilevante in produzione. Il motivo è che i file di configurazione vengono analizzati una sola volta, alla prima esecuzione dell'applicazione (o quando cambiano). Dopo l'analisi viene generato il codice del container DI, messo in cache (salvato su disco), e a ogni richiesta successiva viene eseguito questo codice PHP compilato, senza bisogno di ulteriori analisi. - -Così funziona in un ambiente di produzione. Durante lo sviluppo i file NEON vengono analizzati ogni volta che il loro contenuto cambia, garantendo allo sviluppatore un container DI sempre aggiornato. Come detto, l'analisi in sé è velocissima. - - -Come accedo nella mia classe ai parametri del file di configurazione? ---------------------------------------------------------------------- - -Tenete presente la [Regola n. 1: fatevele passare |introduction#Regola n. 1: fatevelo passare]. Se una classe ha bisogno di un'informazione presente nel file di configurazione, non cercate di capire come la classe possa *procurarsela*. Limitatevi a chiederla, per esempio tramite il costruttore della classe. E poi fornite quel valore nel file di configurazione. - -In questo esempio `%myParameter%` è un segnaposto per il valore del parametro `myParameter`, che verrà passato al costruttore di `MyClass`: - -```neon -# config.neon -parameters: - myParameter: Some value - -services: - - MyClass(%myParameter%) -``` - -Se volete passare più parametri o usare l'autowiring, conviene [racchiudere i parametri in un oggetto |best-practices:passing-settings-to-presenters]. - - -Nette supporta l'interfaccia Container di PSR-11? -------------------------------------------------- - -[Nette DI Container |api:Nette\DI\Container] non supporta direttamente PSR-11. Se però vi serve interoperabilità tra Nette DI Container e librerie o framework che si aspettano la Container Interface di PSR-11, potete creare un [semplice adattatore |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f] che faccia da ponte tra Nette DI Container e PSR-11. - - -Cosa significano i termini container, compiler, definition ecc.? ----------------------------------------------------------------- - -Un breve vocabolario delle parole che ricorrono di continuo intorno a Nette DI, per lo più quando si [scrivono estensioni |extensions]: - -- **Container** - l'oggetto compilato (`Nette\DI\Container`) che crea i servizi su richiesta e li conserva in fase di esecuzione. Viene generato una sola volta come codice PHP ottimizzato. -- **Compiler** - il meccanismo che trasforma i file di configurazione e le estensioni nella classe di quel container. -- **ContainerBuilder** - il modello modificabile del container, usato durante la compilazione; contiene le definizioni dei servizi prima che esista un servizio reale. Vedi [Creare estensioni |extensions#ContainerBuilder]. -- **Servizio** - un oggetto gestito dal container, di norma creato una sola volta e condiviso (un singleton): una connessione al database, un mailer, un logger. -- **Definizione** - la ricetta di un servizio: il suo tipo, come crearlo e cosa fare dopo. Nette trasforma le definizioni nei metodi factory del container; ne esistono di vari tipi (vedi [tipi di definizione |extensions#Tipi di definizione]). -- **Tipo** - la classe o l'interfaccia di un servizio, usata dall'autowiring per abbinare i servizi ai punti che li richiedono. -- **Autowiring** - il passaggio automatico dei servizi ai costruttori e ai metodi in base al loro tipo, così che non dobbiate collegare le dipendenze a mano. -- **Tag** - un'etichetta attaccata a una definizione (facoltativamente con un valore); un'estensione può poi trovare con `findByTag()` tutti i servizi che la portano. -- **Setup** - chiamate aggiuntive eseguite su un servizio subito dopo la sua creazione: chiamate di metodi o assegnazioni di proprietà, aggiunte con `addSetup()`. -- **Alias** - un nome alternativo per un servizio esistente. diff --git a/dependency-injection/it/global-state.texy b/dependency-injection/it/global-state.texy deleted file mode 100644 index 3faf2c3817..0000000000 --- a/dependency-injection/it/global-state.texy +++ /dev/null @@ -1,294 +0,0 @@ -Stato globale e singleton -************************* - -.[perex] -Attenzione: i costrutti seguenti sono sintomi di codice progettato male: - -- `Foo::getInstance()` -- `DB::insert(...)` -- `Article::setDb($db)` -- `ClassName::$var` oppure `static::$var` - -Qualcuno di questi costrutti compare nel vostro codice? Se sì, avete un'occasione per migliorare. Penserete forse che siano costrutti comuni, magari visti nelle soluzioni di esempio di varie librerie e framework. Se è così, la progettazione del loro codice è difettosa. - -Non stiamo parlando di una purezza accademica. Tutti questi costrutti hanno una caratteristica in comune: usano lo stato globale. E lo stato globale ha un effetto deleterio sulla qualità del codice. Le classi diventano ingannevoli riguardo alle proprie dipendenze. Il codice diventa imprevedibile. Confonde gli sviluppatori e ne riduce l'efficienza. - -In questo capitolo spiegheremo perché è così e come evitare lo stato globale. - - -Intreccio globale ------------------ - -In un mondo ideale un oggetto dovrebbe comunicare solo con gli oggetti che gli sono stati [passati direttamente |passing-dependencies]. Se creo due oggetti `A` e `B` e non passo mai un riferimento tra di loro, allora né `A` né `B` possono accedere allo stato dell'altro o modificarlo. È una proprietà molto desiderabile del codice. È come avere una batteria e una lampadina: la lampadina non si accende finché non la collegate alla batteria con un filo. - -Questo però non vale per le variabili globali (statiche) o per i singleton. L'oggetto `A` potrebbe accedere *senza fili* all'oggetto `C` e modificarlo senza che sia passato alcun riferimento, chiamando `C::changeSomething()`. Se anche l'oggetto `B` attinge al `C` globale, allora `A` e `B` possono influenzarsi a vicenda attraverso `C`. - -L'uso delle variabili globali introduce una nuova forma di accoppiamento *senza fili*, invisibile dall'esterno. Crea una cortina di fumo che rende il codice più difficile da capire e da usare. Per cogliere davvero le dipendenze, gli sviluppatori devono leggere ogni riga del codice sorgente, invece di affidarsi alle interfacce delle classi. Per di più questo accoppiamento è del tutto superfluo. Lo stato globale si usa perché è facilmente accessibile da ovunque e permette, per esempio, di scrivere su un database tramite un metodo globale (statico) `DB::insert()`. Come mostreremo, però, la comodità percepita è minima rispetto alle gravi complicazioni che introduce. - -.[note] -Dal punto di vista del comportamento non c'è differenza tra una variabile globale e una statica. Sono ugualmente dannose. - - -L'inquietante azione a distanza -------------------------------- - -"Inquietante azione a distanza": così Albert Einstein chiamava un fenomeno della fisica quantistica che gli metteva i brividi. -Si riferisce all'entanglement quantistico, dove misurare una proprietà di una particella influisce istantaneamente su un'altra particella a essa legata, indipendentemente dalla distanza che le separa, anche milioni di anni luce, il che sembra violare la legge fondamentale dell'universo secondo cui nulla può viaggiare più veloce della luce. - -Nel mondo del software l'"inquietante azione a distanza" descrive la situazione in cui eseguiamo un processo che crediamo isolato (perché non è stata passata esplicitamente alcuna dipendenza), eppure in parti lontane del sistema avvengono interazioni e cambiamenti di stato inattesi, a nostra insaputa. Questo può accadere solo attraverso lo stato globale. - -Immaginate di entrare in un team di sviluppo, in un progetto con una base di codice grande e matura. Il vostro nuovo responsabile vi chiede di implementare una nuova funzionalità e voi, da bravi sviluppatori, iniziate scrivendo un test. Ma poiché siete nuovi nel progetto, fate molti test esplorativi del tipo "cosa succede se chiamo questo metodo". E provate a scrivere il test seguente: - -```php -function testCreditCardCharge() -{ - $cc = new CreditCard('1234567890123456', 5, 2028); // il numero della vostra carta - $cc->charge(100); -} -``` - -Eseguite il codice, magari più volte, e dopo un po' notate le notifiche della banca sul telefono: a ogni esecuzione sono stati addebitati 100 dollari sulla vostra carta di credito! 🤦‍♂️ - -Come diavolo ha potuto il test provocare un addebito reale? Operare con una carta di credito non è semplice. Serve interagire con un servizio web di terze parti, conoscerne l'URL, autenticarsi e così via. Nessuna di queste informazioni è presente nel test. Peggio ancora, non sapete dove si trovino queste informazioni, il che rende impossibile simulare le dipendenze esterne per evitare l'addebito di 100 dollari a ogni esecuzione del test. E come potevate sapere, da sviluppatori appena arrivati, che quello che stavate per fare vi avrebbe reso più poveri di 100 dollari? - -Questa è un'inquietante azione a distanza! - -Siete costretti a spulciare un codice sorgente enorme e a consultare i colleghi più esperti per capire i collegamenti del progetto. Questa difficoltà nasce perché l'interfaccia della classe `CreditCard` non rivela la necessaria inizializzazione dello stato globale. Anche esaminando il codice sorgente della classe potreste non capire quale metodo di inizializzazione chiamare. Nel migliore dei casi potreste trovare la variabile globale a cui si accede e provare a dedurre come inizializzarla. - -Le classi di un progetto del genere sono bugiarde patologiche. La classe `CreditCard` finge che basti istanziarla e chiamarne il metodo `charge()`. In segreto, però, interagisce con un'altra classe, `PaymentGateway`, che rappresenta il gateway di pagamento. Anche l'interfaccia di `PaymentGateway` può suggerire un'inizializzazione indipendente, ma in realtà potrebbe pescare le credenziali da un file di configurazione e così via. Gli sviluppatori originali sanno che `CreditCard` richiede `PaymentGateway`. Hanno scritto loro il codice così. Ma per chi arriva dopo è un mistero completo, che ne ostacola l'apprendimento e la capacità di contribuire efficacemente. - -Come sistemare la situazione? Facile. **Fate in modo che l'API dichiari le dipendenze.** - -```php -function testCreditCardCharge() -{ - $gateway = new PaymentGateway(/* ... */); - $cc = new CreditCard('1234567890123456', 5, 2028); - $cc->charge($gateway, 100); -} -``` - -Notate come le interdipendenze all'interno del codice diventino immediatamente evidenti. Poiché il metodo `charge()` dichiara di aver bisogno di un `PaymentGateway`, non dovete più indovinare questa dipendenza né chiedere in giro. Sapete di dover creare un'istanza e, così facendo, scoprirete i parametri di accesso necessari. Senza di essi il codice non girerebbe nemmeno. - -E soprattutto, ora potete simulare il gateway di pagamento, così non vi verranno addebitati 100 dollari a ogni esecuzione del test. - -Lo stato globale permette agli oggetti di accedere in segreto a dipendenze non dichiarate nelle loro API, trasformando di fatto le vostre API in bugiarde patologiche. - -Forse non l'avevate mai vista così, ma ogni volta che usate lo stato globale create canali di comunicazione segreti senza fili. Questa inquietante azione a distanza costringe gli sviluppatori a leggere ogni riga di codice per capire le possibili interazioni, riducendo la produttività e confondendo i nuovi membri del team. Se siete voi ad aver scritto il codice, conoscete le vere dipendenze, ma chiunque venga dopo di voi è all'oscuro di tutto. - -Evitate di scrivere codice che si affida allo stato globale; preferite passare le dipendenze esplicitamente. Abbracciate la dependency injection. - - -La fragilità dello stato globale --------------------------------- - -Nel codice che usa lo stato globale e i singleton non potete mai essere certi di quando o da chi lo stato sia stato modificato. Questo rischio si manifesta perfino durante l'inizializzazione. Il codice seguente vorrebbe creare una connessione al database e inizializzare un gateway di pagamento, ma solleva ripetutamente eccezioni, e trovarne la causa con il debug è estremamente noioso: - -```php -PaymentGateway::init(); -DB::init('mysql:', 'user', 'password'); -``` - -Dovete seguire con cura il codice per scoprire che l'oggetto `PaymentGateway` accede senza fili ad altri oggetti, alcuni dei quali richiedono una connessione al database. Il database va quindi inizializzato prima di `PaymentGateway`. La cortina di fumo dello stato globale, però, ve lo nasconde. Quanto tempo si risparmierebbe se le API di queste classi fossero oneste e dichiarassero le proprie dipendenze? - -```php -$db = new DB('mysql:', 'user', 'password'); -$gateway = new PaymentGateway($db, /* ... */); -``` - -Un problema simile nasce usando l'accesso globale a una connessione al database: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public function save(): void - { - DB::insert(/* ... */); - } -} -``` - -Chiamando il metodo `save()` non è chiaro se una connessione al database sia stata stabilita né chi sia responsabile di stabilirla. Se dobbiamo cambiare dinamicamente la connessione al database (per esempio per i test), potremmo ricorrere all'aggiunta di metodi come `DB::reconnect(...)` o `DB::reconnectForTest()`. - -Considerate un esempio: - -```php -$article = new Article; -// ... -DB::reconnectForTest(); -Foo::doSomething(); -$article->save(); -``` - -Come possiamo essere sicuri che, chiamando `$article->save()`, venga davvero usato il database di test? E se il metodo `Foo::doSomething()` avesse cambiato la connessione globale al database? Per stabilirlo dovremmo esaminare il codice sorgente di `Foo` e magari di molte altre classi. E questa indagine darebbe solo una risposta temporanea, perché la situazione potrebbe cambiare in seguito. - -E se spostassimo la connessione al database in una variabile statica dentro la classe `Article`? - -```php -class Article -{ - private static DB $db; - - public static function setDb(DB $db): void - { - self::$db = $db; - } - - public function save(): void - { - self::$db->insert(/* ... */); - } -} -``` - -Questo non cambia proprio nulla. Il problema è lo stato globale in sé, indipendentemente dalla classe in cui è nascosto. In questo scenario, come nel precedente, chiamando `$article->save()` non abbiamo alcuna certezza su quale database verranno scritti i dati. Chiunque, in qualsiasi punto dell'applicazione, potrebbe aver cambiato il database in qualsiasi momento con `Article::setDb()`. A nostra insaputa. - -Lo stato globale rende la nostra applicazione **estremamente fragile**. - -Esiste però un modo semplice di affrontare questo problema. Basta far dichiarare all'API le dipendenze necessarie al corretto funzionamento. - -```php -class Article -{ - public function __construct( - private DB $db, - ) { - } - - public function save(): void - { - $this->db->insert(/* ... */); - } -} - -$article = new Article($db); -// ... -Foo::doSomething(); -$article->save(); -``` - -Questo approccio elimina le preoccupazioni su cambiamenti nascosti o inattesi della connessione al database. Ora abbiamo la certezza di dove l'articolo venga salvato, e le modifiche in classi non correlate non possono più influenzarlo. Il codice non è più fragile, ma stabile. - -Evitate di scrivere codice che si affida allo stato globale; preferite passare le dipendenze esplicitamente. Abbracciate la dependency injection. - - -Singleton ---------- - -Il singleton è un design pattern che, per [definizione |https://it.wikipedia.org/wiki/Singleton_(informatica)] della famosa pubblicazione della Gang of Four, limita una classe a una sola istanza e ne offre un accesso globale. L'implementazione di questo pattern somiglia di norma al codice seguente: - -```php -class Singleton -{ - private static self $instance; - - public static function getInstance(): self - { - self::$instance ??= new self; - return self::$instance; - } - - // e altri metodi che svolgono le funzioni della classe -} -``` - -Purtroppo il singleton introduce nell'applicazione lo stato globale. E come abbiamo mostrato sopra, lo stato globale è indesiderabile. Ecco perché il singleton è considerato un antipattern. - -Non usate i singleton nel vostro codice e sostituiteli con altri meccanismi. Davvero non avete bisogno dei singleton. Se però dovete garantire che di una classe esista una sola istanza in tutta l'applicazione, delegate questa responsabilità al [container DI |container]. Nasce così un singleton nell'ambito dell'applicazione, comunemente chiamato servizio. La classe stessa è allora libera dalla gestione della propria unicità (cioè non avrà un metodo `getInstance()` né una proprietà statica con l'istanza) e può concentrarsi solo sulle proprie responsabilità. Smetterà così di violare il principio di responsabilità singola. - - -Stato globale e test --------------------- - -Scrivendo i test, idealmente presupponiamo che ogni test sia un'unità isolata, in cui non entra né da cui esce alcuno stato esterno. Al termine di un test, tutto lo stato a esso associato dovrebbe essere ripulito automaticamente dal garbage collector. Questo rende i test isolati. Possiamo quindi eseguirli in qualsiasi ordine. - -Quando però sono presenti stati globali o singleton, questi presupposti vantaggiosi crollano. Lo stato può entrare nei test e uscirne. All'improvviso l'ordine dei test può contare. - -Per riuscire perfino a testare del codice che usa i singleton, gli sviluppatori devono spesso comprometterne l'integrità, per esempio permettendo di sostituire l'istanza del singleton. Soluzioni del genere sono, nel migliore dei casi, espedienti che portano a codice difficile da mantenere e da capire. Ogni test (o il suo metodo `tearDown()`) che modifica lo stato globale deve annullare con cura quelle modifiche. - -Lo stato globale è il più grande grattacapo dei test unitari! - -Come sistemare la cosa? Semplice. Evitate di scrivere codice che usa i singleton; preferite passare le dipendenze esplicitamente. Abbracciate la dependency injection. - - -Costanti globali ----------------- - -Lo stato globale non si limita all'uso dei singleton e delle variabili statiche, ma può riguardare anche le costanti globali. - -Le costanti il cui valore rappresenta una verità universale (`M_PI`) oppure offre un'informazione autonoma (`PREG_BACKTRACK_LIMIT_ERROR`) sono in generale accettabili. Al contrario, le costanti usate come modo per iniettare *senza fili* informazioni nel codice sono di fatto dipendenze nascoste. Come `LOG_FILE` nell'esempio seguente. L'uso della costante `FILE_APPEND` è invece del tutto corretto. - -```php -const LOG_FILE = '...'; - -class Foo -{ - public function doSomething() - { - // ... - file_put_contents(LOG_FILE, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Dovremmo invece dichiarare il percorso del file di log come parametro del costruttore della classe `Foo`, rendendolo una parte esplicita della sua API: - -```php -class Foo -{ - public function __construct( - private string $logFile, - ) { - } - - public function doSomething() - { - // ... - file_put_contents($this->logFile, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Ora passiamo esplicitamente il percorso del file di log. Possiamo cambiarlo facilmente secondo necessità, il che semplifica i test e la manutenzione del codice. - - -Funzioni globali e metodi statici ---------------------------------- - -Vogliamo sottolineare che l'uso dei metodi statici e delle funzioni globali non è di per sé problematico. Abbiamo spiegato i problemi di metodi come `DB::insert()`, ma il problema di fondo è sempre stato lo stato globale sottostante, di norma salvato in una variabile statica. Il metodo `DB::insert()` si affida a una variabile statica per conservare la connessione al database. Senza quella variabile sarebbe impossibile implementare il metodo. - -Usare metodi statici e funzioni deterministici come `Closure::fromCallable()`, `strlen()` e molti altri è perfettamente compatibile con la dependency injection. Queste funzioni sono prevedibili, perché restituiscono sempre lo stesso risultato per gli stessi parametri di ingresso. Non usano alcuno stato globale. - -In PHP esistono però funzioni non deterministiche. Tra queste, per esempio, la funzione `htmlspecialchars()`. Il suo terzo parametro, `$encoding`, se omesso assume come valore predefinito quello dell'opzione di configurazione `default_charset` (`ini_get('default_charset')`). È quindi consigliabile indicare sempre questo parametro, per prevenire possibili comportamenti imprevedibili. Nette lo fa sistematicamente. - -Alcune funzioni, come `strtolower()` e `strtoupper()`, hanno avuto in tempi recenti un comportamento non deterministico, dipendente dall'impostazione del locale (`setlocale()`). Questo ha causato molte complicazioni, soprattutto lavorando con la lingua turca. Il turco, infatti, distingue tra la "I" con e senza punto, sia minuscola sia maiuscola. Di conseguenza `strtolower('I')` restituiva `ı` (i minuscola senza punto) e `strtoupper('i')` restituiva `İ` (I maiuscola con punto), il che portava a numerosi errori misteriosi nelle applicazioni. Questo problema è però stato risolto in PHP 8.2 e le funzioni non dipendono più dal locale. - -È un buon esempio di come lo stato globale (l'impostazione del locale) abbia creato problemi a migliaia di sviluppatori in tutto il mondo. La soluzione finale è consistita nel rendere le funzioni indipendenti dal locale, eliminando di fatto la dipendenza nascosta. - - -Quando è possibile usare lo stato globale? ------------------------------------------- - -Esistono situazioni specifiche e limitate in cui usare lo stato globale può essere accettabile. Per esempio durante il debugging, quando dovete scaricare il valore di una variabile o misurare il tempo di esecuzione di una porzione di codice. In questi casi, che riguardano azioni temporanee destinate a essere rimosse in seguito dal codice, usare un dumper o un timer globalmente accessibile può essere legittimo. Questi strumenti non fanno parte della progettazione di fondo dell'applicazione. - -Un altro esempio riguarda le funzioni di PHP per le espressioni regolari (`preg_*`), che internamente mettono in cache in memoria statica le espressioni regolari compilate. Quando chiamate più volte, in punti diversi del codice, funzioni con la stessa espressione regolare, l'espressione viene compilata una sola volta. Questa cache migliora le prestazioni ed è del tutto invisibile all'utente, il che rende questo uso dello stato statico interno in generale accettabile. - - -Riepilogo ---------- - -Abbiamo parlato del perché abbia senso: - -1) eliminare dal vostro codice tutte le proprietà statiche modificabili (lo stato globale) -2) dichiarare esplicitamente le dipendenze -3) e usare la dependency injection - -Progettando il vostro codice, ricordate che ogni `static $foo` modificabile è una potenziale fonte di problemi. Per creare un ambiente amico della DI è essenziale eliminare completamente lo stato globale e sostituirlo con la dependency injection. - -Durante questo processo potreste scoprire la necessità di dividere classi che hanno più responsabilità. Non esitate a farlo; puntate al principio di responsabilità singola. - -*Vorrei ringraziare Miško Hevery, i cui articoli, come [Flaw: Brittle Global State & Singletons |https://web.archive.org/web/20230321084133/http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/], sono alla base di questo capitolo.* diff --git a/dependency-injection/it/introduction.texy b/dependency-injection/it/introduction.texy deleted file mode 100644 index fa5ea49a89..0000000000 --- a/dependency-injection/it/introduction.texy +++ /dev/null @@ -1,525 +0,0 @@ -Che cos'è la dependency injection? -********************************** - -.[perex] -Questo capitolo presenta le pratiche di programmazione di base che dovreste seguire scrivendo qualsiasi applicazione. Sono i fondamenti necessari per scrivere codice pulito, comprensibile e manutenibile. - -Se adotterete e seguirete queste regole, Nette vi sosterrà a ogni passo. Svolgerà per voi i compiti di routine e vi offrirà il massimo della comodità, permettendovi di concentrarvi sulla logica vera e propria. - -I principi che mostreremo qui sono piuttosto semplici. Non c'è nulla da temere. - - -Vi ricordate il vostro primo programma? ---------------------------------------- - -Non sappiamo in quale linguaggio l'avete scritto, ma se fosse stato PHP avrebbe probabilmente avuto più o meno questo aspetto: - -```php -function addition(float $a, float $b): float -{ - return $a + $b; -} - -echo addition(23, 1); // stampa 24 -``` - -Poche righe di codice banali, eppure nascondono tanti concetti chiave. Che esistono le variabili. Che il codice è diviso in unità più piccole, come le funzioni. Che passiamo loro argomenti in ingresso ed esse restituiscono risultati. Mancano solo le condizioni e i cicli. - -Il fatto che passiamo dati in ingresso a una funzione e che essa restituisca un risultato è un concetto perfettamente comprensibile, usato anche in altri campi, come la matematica. - -Una funzione ha la propria firma, composta dal nome, dall'elenco dei parametri e dei loro tipi e, infine, dal tipo del valore di ritorno. A noi utenti interessa la firma; di norma non abbiamo bisogno di sapere nulla dell'implementazione interna. - -Immaginate ora che la firma della funzione fosse questa: - -```php -function addition(float $x): float -``` - -Una somma con un solo parametro? Strano... E questa? - -```php -function addition(): float -``` - -Ora è davvero strano, no? Come si usa la funzione? - -```php -echo addition(); // cosa stamperà? -``` - -Guardando un codice del genere resteremmo perplessi. Non solo un principiante non lo capirebbe: nemmeno un programmatore esperto capirebbe un codice così. - -Vi state chiedendo che aspetto avrebbe una funzione del genere all'interno? Da dove prenderebbe i numeri da sommare? Probabilmente se li procurerebbe *in qualche modo* da sola, magari così: - -```php -function addition(): float -{ - $a = Input::get('a'); - $b = Input::get('b'); - return $a + $b; -} -``` - -Nel corpo della funzione abbiamo scoperto dipendenze nascoste da altre funzioni globali o da metodi statici. Per scoprire da dove arrivino davvero i numeri dobbiamo indagare oltre. - - -Non così! ---------- - -Il progetto che abbiamo appena mostrato è l'essenza di molte caratteristiche negative: - -- La firma della funzione fingeva di non aver bisogno dei numeri da sommare, il che ci ha confusi. -- Non abbiamo idea di come far sommare alla funzione due numeri diversi. -- Abbiamo dovuto guardare nel codice per scoprire da dove prende i numeri. -- Abbiamo scoperto dipendenze nascoste. -- Per capire tutto bisogna esaminare anche queste dipendenze. - -Ed è davvero compito della funzione di somma procurarsi gli input? Certo che no. La sua responsabilità è solo la somma. - - -Non vogliamo incontrare codice del genere, e di sicuro non vogliamo scriverlo. La soluzione è semplice: tornare alle basi e usare semplicemente i parametri: - - -```php -function addition(float $a, float $b): float -{ - return $a + $b; -} -``` - - -Regola n. 1: fatevelo passare ------------------------------ - -La regola più importante è: **tutti i dati di cui le funzioni o le classi hanno bisogno devono essere forniti loro**. - -Invece di inventare modi nascosti perché se li procurino da sole, fornite semplicemente i parametri. Risparmierete il tempo speso a inventare percorsi nascosti che di sicuro non miglioreranno il vostro codice. - -Se seguite sempre e ovunque questa regola, siete sulla strada di un codice senza dipendenze nascoste. Di un codice comprensibile non solo all'autore, ma anche a chiunque lo legga in seguito. Dove tutto si capisce dalle firme delle funzioni e delle classi, e non serve cercare dettagli nascosti nell'implementazione. - -Questa tecnica si chiama tecnicamente **dependency injection**. E i dati si chiamano **dipendenze**. È solo un semplice passaggio di parametri, nulla di più. - -.[note] -Non confondete la dependency injection, che è un design pattern, con il "container di dependency injection", che è uno strumento, una cosa radicalmente diversa. Dei container parleremo più avanti. - - -Dalle funzioni alle classi --------------------------- - -E come si applica tutto questo alle classi? Una classe è un'entità più complessa di una semplice funzione, ma anche qui la regola n. 1 vale in pieno. Ci sono solo [più modi di passare gli argomenti |passing-dependencies]. Per esempio, in modo abbastanza simile al caso della funzione: - -```php -class Math -{ - public function sum(float $a, float $b): float - { - return $a + $b; - } -} - -$math = new Math; -echo $math->sum(23, 1); // 24 -``` - -Oppure con altri metodi, o direttamente con il costruttore: - -```php -class Sum -{ - public function __construct( - private float $a, - private float $b, - ) { - } - - public function calculate(): float - { - return $this->a + $this->b; - } -} - -$sum = new Sum(23, 1); -echo $sum->calculate(); // 24 -``` - -Entrambi gli esempi sono pienamente conformi alla dependency injection. - - -Esempi dalla vita reale ------------------------ - -Nel mondo reale non scriverete classi per sommare numeri. Passiamo a esempi pratici. - -Prendiamo una classe `Article`, che rappresenta un articolo di blog: - -```php -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - // salva l'articolo nel database - } -} -``` - -e l'uso sarà questo: - -```php -$article = new Article; -$article->title = '10 Things You Need to Know About Losing Weight'; -$article->content = 'Every year millions of people in ...'; -$article->save(); -``` - -Il metodo `save()` salverà l'articolo in una tabella del database. Implementarlo con [Nette Database |database:] sarebbe semplice, se non fosse per un intoppo: da dove prende `Article` la connessione al database, cioè un oggetto della classe `Nette\Database\Connection`? - -Sembra che le possibilità siano molte. Potrebbe prenderla da una variabile statica. Oppure ereditando da una classe che fornisce la connessione al database. Oppure usare un [singleton |global-state#Singleton]. Oppure le cosiddette facade, come si usano in Laravel: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - DB::insert( - 'INSERT INTO articles (title, content) VALUES (?, ?)', - [$this->title, $this->content], - ); - } -} -``` - -Ottimo, abbiamo risolto il problema. - -Oppure no? - -Ricordiamo la [Regola n. 1: fatevelo passare |#Regola n. 1: fatevelo passare]: tutte le dipendenze di cui la classe ha bisogno devono esserle passate. Perché se infrangiamo la regola, ci siamo incamminati verso un codice disordinato, pieno di dipendenze nascoste e poco chiaro, e il risultato sarà un'applicazione difficile da mantenere e da sviluppare. - -Chi usa la classe `Article` non ha idea di dove il metodo `save()` salvi l'articolo. In una tabella di database? Quale, quello di produzione o quello di test? E come si può cambiare? - -Deve guardare come è implementato il metodo `save()` e trova l'uso del metodo `DB::insert()`. Deve quindi indagare oltre per capire come questo metodo ottenga la connessione al database. E le dipendenze nascoste possono formare una catena piuttosto lunga. - -In un codice pulito e ben progettato non ci sono mai dipendenze nascoste, facade di Laravel o variabili statiche. In un codice pulito e ben progettato gli argomenti vengono forniti: - -```php -class Article -{ - public function save(Nette\Database\Connection $db): void - { - $db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -Ancora più pratico, come vedremo più avanti, è usare il costruttore: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function save(): void - { - $this->db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -.[note] -Se siete programmatori esperti, penserete forse che `Article` non dovrebbe avere affatto un metodo `save()`: dovrebbe rappresentare una pura struttura dati, e del salvataggio dovrebbe occuparsi un repository separato. Ha senso. Ma ci porterebbe ben oltre l'ambito dell'argomento, che è la dependency injection, e dell'obiettivo di fornire esempi semplici. - -Se scrivete una classe che per funzionare ha bisogno, per esempio, di un database, non inventatevi da dove prenderlo, ma fatevelo passare. Magari come parametro del costruttore o di un altro metodo. Riconoscete le dipendenze. Riconoscetele nell'API della vostra classe. Otterrete un codice comprensibile e prevedibile. - -E questa classe, che registra i messaggi di errore? - -```php -class Logger -{ - public function log(string $message): void - { - $file = LOG_DIR . '/log.txt'; - file_put_contents($file, $message . "\n", FILE_APPEND); - } -} -``` - -Cosa ne dite, abbiamo seguito la [Regola n. 1: fatevelo passare |#Regola n. 1: fatevelo passare]? - -No. - -L'informazione chiave, la directory che contiene il file di log, *se la procura la classe stessa* da una costante. - -Guardate l'esempio d'uso: - -```php -$logger = new Logger; -$logger->log('Temperature is 23 °C'); -$logger->log('Temperature is 10 °C'); -``` - -Senza conoscere l'implementazione, sapreste dire dove vengono scritti i messaggi? Vi verrebbe in mente che per funzionare è necessaria l'esistenza della costante `LOG_DIR`? E sapreste creare una seconda istanza che scriva altrove? Di certo no. - -Sistemiamo la classe: - -```php -class Logger -{ - public function __construct( - private string $file, - ) { - } - - public function log(string $message): void - { - file_put_contents($this->file, $message . "\n", FILE_APPEND); - } -} -``` - -La classe è ora molto più comprensibile, configurabile e quindi più utile. - -```php -$logger = new Logger('/path/to/log.txt'); -$logger->log('Temperature is 15 °C'); -``` - - -Ma a me non interessa! ----------------------- - -*"Quando creo un oggetto Article e chiamo save(), non voglio occuparmi del database; voglio solo che venga salvato in quello che ho configurato."* - -*"Quando uso Logger, voglio solo che il messaggio venga scritto e non voglio occuparmi di dove. Che vengano usate le impostazioni globali."* - -Sono obiezioni legittime. - -Come esempio, mostriamo una classe che distribuisce newsletter e registra l'esito: - -```php -class NewsletterDistributor -{ - public function distribute(): void - { - $logger = new Logger(/* ... */); - try { - $this->sendEmails(); - $logger->log('Emails have been sent out'); - - } catch (Exception $e) { - $logger->log('An error occurred during sending'); - throw $e; - } - } -} -``` - -Il `Logger` migliorato, che non usa più la costante `LOG_DIR`, richiede il percorso del file nel costruttore. Come si risolve? La classe `NewsletterDistributor` non si occupa di dove vengano scritti i messaggi: vuole solo registrarli. - -La soluzione è di nuovo la [Regola n. 1: fatevelo passare |#Regola n. 1: fatevelo passare]: passiamo tutti i dati di cui la classe ha bisogno. - -Significa quindi che passiamo il percorso del log attraverso il costruttore, per usarlo poi nella creazione dell'oggetto `Logger`? - -```php -class NewsletterDistributor -{ - public function __construct( - private string $file, // ⛔ NON COSÌ! - ) { - } - - public function distribute(): void - { - $logger = new Logger($this->file); -``` - -Non così! Perché il percorso **non** è un dato di cui ha bisogno la classe `NewsletterDistributor`: ne ha bisogno il `Logger`. Cogliete la differenza? La classe `NewsletterDistributor` ha bisogno del logger stesso. Passeremo quindi il logger stesso: - -```php -class NewsletterDistributor -{ - public function __construct( - private Logger $logger, // ✅ - ) { - } - - public function distribute(): void - { - try { - $this->sendEmails(); - $this->logger->log('Emails have been sent out'); - - } catch (Exception $e) { - $this->logger->log('An error occurred during sending'); - throw $e; - } - } -} -``` - -Ora dalla firma della classe `NewsletterDistributor` è chiaro che il logging fa parte delle sue funzioni. E sostituire il logger con un altro, magari per i test, è del tutto immediato. Per di più, se il costruttore della classe `Logger` cambiasse, non avrebbe alcun impatto sulla nostra classe. - - -Regola n. 2: prendete ciò che è vostro --------------------------------------- - -Non fatevi confondere e non accettate le dipendenze delle vostre dipendenze. Accettate solo le vostre. - -Grazie a questo, il codice che usa altri oggetti sarà completamente indipendente dalle modifiche ai loro costruttori. La sua API sarà più precisa. E soprattutto, sostituire queste dipendenze con altre sarà immediato. - - -Un nuovo membro della famiglia ------------------------------- - -Il team di sviluppo ha deciso di creare un secondo logger, che scrive nel database. Creiamo quindi una classe `DatabaseLogger`. Ora abbiamo due classi, `Logger` e `DatabaseLogger`; una scrive su file, l'altra nel database... la denominazione non sembra un po' strana? Non sarebbe meglio rinominare `Logger` in `FileLogger`? Certamente. - -Ma facciamolo con astuzia. Creiamo un'interfaccia con il nome originale: - -```php -interface Logger -{ - function log(string $message): void; -} -``` - -… che entrambi i logger implementeranno: - -```php -class FileLogger implements Logger -// ... - -class DatabaseLogger implements Logger -// ... -``` - -E grazie a questo non ci sarà bisogno di modificare nulla nel resto del codice in cui il logger viene usato. Per esempio il costruttore della classe `NewsletterDistributor` continuerà tranquillamente a richiedere `Logger` come parametro. E sta a noi decidere quale istanza fornirgli. - -**Ecco perché non aggiungiamo mai il suffisso `Interface` o il prefisso `I` ai nomi delle interfacce.** Altrimenti non sarebbe possibile estendere il codice in modo così elegante. - - -Houston, abbiamo un problema ----------------------------- - -Mentre in tutta l'applicazione ce la caviamo con una sola istanza del logger, sia esso su file o su database, e ci basta passarla ovunque avvenga il logging, la situazione è ben diversa con la classe `Article`. Ne creiamo istanze a seconda della necessità, anche più volte. Come gestiamo la dipendenza dal database nel suo costruttore? - -Un esempio può essere un controller che deve salvare un articolo nel database dopo l'invio di un form: - -```php -class EditController extends Controller -{ - public function formSubmitted($data) - { - $article = new Article(/* ... */); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Una possibile soluzione sembra ovvia: facciamoci passare l'oggetto database nel costruttore di `EditController` e usiamo `$article = new Article($this->db)`. - -Come nel caso precedente di `Logger` e del percorso del file, non è l'approccio corretto. Il database non è una dipendenza di `EditController`, ma di `Article`. Passare il database viola quindi la [Regola n. 2: prendete ciò che è vostro |#Regola n. 2: prendete ciò che è vostro]. Se il costruttore della classe `Article` cambia (viene aggiunto un nuovo parametro), dovrete modificare il codice in tutti i punti in cui vengono create le istanze. Uff. - -Houston, cosa proponi? - - -Regola n. 3: lasciate fare alla factory ---------------------------------------- - -Eliminando le dipendenze nascoste e passando tutte le dipendenze come argomenti, abbiamo ottenuto classi più configurabili e flessibili. Ci serve quindi qualcosa in più che crei e configuri per noi queste classi più flessibili. Le chiameremo factory. - -La regola è: se una classe ha delle dipendenze, delegate la creazione delle sue istanze a una factory. - -Le factory sono un'alternativa più intelligente all'operatore `new` nel mondo della dependency injection. - -.[note] -Non confondetela con il design pattern *factory method*, che descrive un modo specifico di usare le factory e non ha nulla a che vedere con questo argomento. - - -Factory -------- - -Una factory è un metodo o una classe che crea e configura oggetti. Chiameremo `ArticleFactory` la classe che produce `Article`, e potrebbe avere questo aspetto: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Il suo uso nel controller sarà questo: - -```php -class EditController extends Controller -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function formSubmitted($data) - { - // lascia che sia la factory a creare l'oggetto - $article = $this->articleFactory->create(); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -A questo punto, se la firma del costruttore della classe `Article` cambia, l'unica parte di codice che deve reagire è la `ArticleFactory` stessa. Tutto il resto del codice che lavora con gli oggetti `Article`, come `EditController`, resterà intatto. - -Vi starete forse grattando la testa, chiedendovi se abbiamo davvero migliorato la situazione. La quantità di codice è cresciuta e il tutto comincia ad apparire sospettosamente complesso. - -Niente paura, arriveremo presto al container DI di Nette. E ha diversi assi nella manica, che semplificheranno enormemente la costruzione di applicazioni con la dependency injection. Per esempio, al posto della classe `ArticleFactory` basterà [scrivere solo un'interfaccia |factory]: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Ma stiamo correndo troppo, restate sintonizzati :-) - - -Riepilogo ---------- - -All'inizio di questo capitolo avevamo promesso di mostrare un procedimento per progettare codice pulito. Basta assicurarsi che alle classi: - -1) [vengano passate le dipendenze di cui hanno bisogno |#Regola n. 1: fatevelo passare] -2) [e, al contrario, non venga passato ciò di cui non hanno bisogno direttamente |#Regola n. 2: prendete ciò che è vostro] -3) [e che gli oggetti con dipendenze si creino al meglio nelle factory |#Regola n. 3: lasciate fare alla factory] - -A prima vista può non sembrare, ma queste tre regole hanno conseguenze di vasta portata. Portano a una prospettiva radicalmente diversa sulla progettazione del codice. Ne vale la pena? I programmatori che hanno abbandonato le vecchie abitudini e hanno iniziato a usare sistematicamente la dependency injection considerano questo passo un momento decisivo della propria carriera professionale. Ha aperto loro un mondo di applicazioni chiare e manutenibili. - -E se il codice non usa sistematicamente la dependency injection? Se è costruito su metodi statici o su singleton? Questo porta a dei problemi? [Sì, e molto seri |global-state]. diff --git a/dependency-injection/it/nette-container.texy b/dependency-injection/it/nette-container.texy deleted file mode 100644 index bdc9338714..0000000000 --- a/dependency-injection/it/nette-container.texy +++ /dev/null @@ -1,117 +0,0 @@ -Nette DI Container -****************** - -.[perex] -Nette DI è una delle librerie più interessanti di Nette. Sa generare e aggiornare automaticamente container DI compilati, estremamente veloci e straordinariamente facili da configurare. - -La forma dei servizi che il container DI deve creare si definisce di norma con file di configurazione in [formato NEON|neon:format]. Il container che abbiamo creato manualmente nel [capitolo precedente|container] si scriverebbe così: - -```neon -parameters: - db: - dsn: 'mysql:' - user: root - password: '***' - -services: - - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - - ArticleFactory - - EditController -``` - -La sintassi è molto concisa. - -Tutte le dipendenze dichiarate nei costruttori delle classi `ArticleFactory` ed `EditController` vengono individuate e passate automaticamente da Nette DI grazie al cosiddetto [autowiring|autowiring], quindi non serve indicare nulla nel file di configurazione. Anche se i parametri cambiano, non dovete cambiare nulla nella configurazione. Durante lo sviluppo Nette rigenera automaticamente il container. Potete concentrarvi esclusivamente sullo sviluppo dell'applicazione. - -Se vogliamo passare le dipendenze con i setter, usiamo a questo scopo la sezione [setup |services#Setup]. - -Nette DI genera direttamente il codice PHP del container. Il risultato è quindi un file `.php` che potete aprire ed esaminare. Questo vi permette di vedere esattamente come funziona il container. Potete anche eseguirne il debug nel vostro IDE e percorrerne l'esecuzione passo passo. E soprattutto: il codice PHP generato è estremamente veloce. - -Nette DI sa generare anche il codice di una [factory|factory] a partire da un'interfaccia fornita. Al posto della classe `ArticleFactory` ci basta quindi creare nell'applicazione un'interfaccia: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Trovate l'esempio completo [su GitHub|https://github.com/nette-examples/di-example-doc]. - - -Uso autonomo ------------- - -Integrare la libreria Nette DI in un'applicazione è molto semplice. Per prima cosa la installiamo con Composer (perché scaricare file zip è ormai superato): - -```shell -composer require nette/di -``` - -Il codice seguente usa il [Compiler |api:Nette\DI\Compiler] per creare un'istanza del container DI secondo la configurazione salvata nel file `config.neon`: - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp'); -$class = $loader->load(function ($compiler) { - $compiler->loadConfig(__DIR__ . '/config.neon'); -}); -$container = new $class; -``` - -Il container viene generato una sola volta, il suo codice viene scritto nella cache (la directory `__DIR__ . '/temp'`) e alle richieste successive viene solo caricato da lì. - -Da solo, il `Compiler` abilita nella configurazione soltanto le sezioni `services` e `parameters`. Per usare le altre, come `search`, `decorator`, `di` o `inject`, registratene prima le estensioni. E per poter registrare le estensioni dalla sezione `extensions` della configurazione, aggiungete la `ExtensionsExtension`: - -```php -$compiler->addExtension('search', new Nette\DI\Extensions\SearchExtension($tempDir)); -$compiler->addExtension('extensions', new Nette\DI\Extensions\ExtensionsExtension); -``` - -Il [Configurator |application:bootstrapping] usato nelle applicazioni Nette complete le registra tutte automaticamente. - -Se tenete più container diversi nella stessa directory di cache, distingueteli con una chiave passata come secondo argomento a `load()`; essa entra a far parte del nome della classe generata: - -```php -$class = $loader->load( - fn($compiler) => $compiler->loadConfig(__DIR__ . '/config.neon'), - 'my-key', -); -``` - -Per creare e ottenere i servizi si usano i metodi `getService()` o `getByType()`. Ecco come creiamo l'oggetto `EditController`: - -```php -$controller = $container->getByType(EditController::class); -$controller->someMethod(); -``` - -Durante lo sviluppo è utile attivare la modalità di aggiornamento automatico, in cui il container si rigenera automaticamente se una classe o un file di configurazione vengono modificati. Basta passare `true` come secondo argomento del costruttore di [ContainerLoader |api:Nette\DI\ContainerLoader]. - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp', true); -``` - - -Lavorare con il container -------------------------- - -Oltre a `getService()` e `getByType()`, l'oggetto container offre altri metodi utili: - -- `getByType(string $type, bool $throw = true): ?object` restituisce il servizio del tipo indicato. Se passate `false` come secondo argomento, restituisce `null` invece di sollevare un'eccezione quando un servizio del genere non esiste. -- `hasService(string $name): bool` e `isCreated(string $name): bool` dicono se un servizio è definito e se è già stato istanziato. -- `getParameters(): array` restituisce tutti i parametri del container, `getParameter($key)` ne restituisce uno solo. -- `createInstance(string $class, array $args = []): object` crea una nuova istanza della classe indicata e ne passa le dipendenze del costruttore tramite autowiring. -- `callMethod(callable $function, array $args = []): mixed` chiama il callable indicato e ne passa gli argomenti tramite autowiring. -- `callInjects(object $service): void` chiama tutti i metodi `inject*()` dell'oggetto indicato e vi passa le dipendenze. - -Il costruttore del container accetta anche un array di parametri, che integrano quelli definiti nella configurazione: - -```php -$container = new $class(['host' => 'localhost']); -``` - - -Uso con Nette Framework ------------------------ - -Come abbiamo mostrato, l'uso di Nette DI non è limitato alle applicazioni costruite con Nette Framework: potete integrarlo ovunque con appena tre righe di codice. Se però sviluppate applicazioni con Nette Framework, della configurazione e della creazione del container si occupa [Bootstrap |application:bootstrapping#Configurazione del container DI]. diff --git a/dependency-injection/it/passing-dependencies.texy b/dependency-injection/it/passing-dependencies.texy deleted file mode 100644 index 255cf42efd..0000000000 --- a/dependency-injection/it/passing-dependencies.texy +++ /dev/null @@ -1,215 +0,0 @@ -Passaggio delle dipendenze -************************** - -<div class=perex> - -Gli argomenti, o "dipendenze" nella terminologia della DI, si possono passare alle classi nei modi principali seguenti: - -* iniezione tramite costruttore -* iniezione tramite metodo (la cosiddetta setter injection) -* iniezione tramite proprietà -* con il metodo `inject*()` o l'attributo `#[Inject]` - -</div> - -Mostriamo ogni variante con esempi concreti. - - -Iniezione tramite costruttore -============================= - -Le dipendenze vengono fornite come argomenti del costruttore nel momento in cui l'oggetto viene istanziato: - -```php -class MyClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -$obj = new MyClass($cache); -``` - -Questo approccio è adatto alle dipendenze obbligatorie, di cui la classe ha assolutamente bisogno per funzionare, perché senza di esse l'istanza non si può creare. - -Da PHP 8.0 possiamo usare una notazione più breve ([constructor property promotion |https://blog.nette.org/en/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), funzionalmente equivalente: - -```php -// PHP 8.0 -class MyClass -{ - public function __construct( - private Cache $cache, - ) { - } -} -``` - -Da PHP 8.1 una proprietà si può contrassegnare con il flag `readonly`, che dichiara che il valore della proprietà non cambierà dopo l'inizializzazione: - -```php -// PHP 8.1 -class MyClass -{ - public function __construct( - private readonly Cache $cache, - ) { - } -} -``` - -Il container DI passa automaticamente le dipendenze al costruttore tramite l'[autowiring |autowiring]. Gli argomenti che non si possono fornire in questo modo (per esempio stringhe, numeri, booleani) [si indicano nella configurazione |services#Argomenti]. - - -L'inferno dei costruttori -------------------------- - -Il termine *inferno dei costruttori* descrive la situazione in cui una classe figlia eredita da una classe genitore il cui costruttore richiede delle dipendenze, e anche la classe figlia richiede delle dipendenze. Deve allora accettare e girare anche le dipendenze del genitore: - -```php -abstract class BaseClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass extends BaseClass -{ - private Database $db; - - // ⛔ INFERNO DEI COSTRUTTORI - public function __construct(Cache $cache, Database $db) - { - parent::__construct($cache); - $this->db = $db; - } -} -``` - -Il problema nasce quando vogliamo cambiare il costruttore di `BaseClass`, per esempio quando si aggiunge una nuova dipendenza. Diventa allora necessario modificare anche tutti i costruttori delle classi figlie. Il che trasforma una modifica del genere in un inferno. - -Come si può evitare? La soluzione è **preferire la [composizione all'ereditarietà |faq#Perché si preferisce la composizione all'ereditarietà?]**. - -Progettiamo quindi il codice diversamente. Eviteremo le classi [astratte |nette:introduction-to-object-oriented-programming#Classi astratte] `Base*`. Invece che `MyClass` acquisisca una certa funzionalità ereditando da `BaseClass`, questa funzionalità le verrà passata come dipendenza: - -```php -final class SomeFunctionality -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass -{ - private SomeFunctionality $sf; - private Database $db; - - public function __construct(SomeFunctionality $sf, Database $db) // ✅ - { - $this->sf = $sf; - $this->db = $db; - } -} -``` - - -Setter injection -================ - -Le dipendenze vengono fornite chiamando un metodo che le salva in una proprietà privata. La convenzione di denominazione più diffusa per questi metodi è lo schema `set*()`, da cui il nome setter, ma naturalmente si possono chiamare diversamente. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - $this->cache = $cache; - } -} - -$obj = new MyClass; -$obj->setCache($cache); -``` - -Questo approccio è adatto alle dipendenze facoltative, non essenziali al funzionamento della classe, perché non è garantito che l'oggetto riceva davvero la dipendenza (cioè che il chiamante invochi il metodo). - -Allo stesso tempo questo metodo permette di chiamare ripetutamente il setter per cambiare la dipendenza. Se non è desiderabile, aggiungete un controllo nel metodo oppure, da PHP 8.1, contrassegnate la proprietà `$cache` con il flag `readonly`. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - if (isset($this->cache)) { - throw new RuntimeException('The dependency has already been set'); - } - $this->cache = $cache; - } -} -``` - -La chiamata del setter si definisce nella configurazione del container DI, nella [chiave setup |services#Setup]. Anche qui si usa la fornitura automatica delle dipendenze tramite autowiring: - -```neon -services: - - create: MyClass - setup: - - setCache -``` - - -Iniezione tramite proprietà -=========================== - -Le dipendenze vengono fornite scrivendo direttamente in una proprietà della classe: - -```php -class MyClass -{ - public Cache $cache; -} - -$obj = new MyClass; -$obj->cache = $cache; -``` - -Questo metodo è considerato inadatto, perché la proprietà deve essere dichiarata `public`. Di conseguenza perdiamo il controllo sul fatto che la dipendenza passata sia davvero del tipo richiesto (cosa particolarmente vera prima delle dichiarazioni di tipo delle proprietà di PHP 7.4) e perdiamo la possibilità di reagire con logica personalizzata a una dipendenza appena assegnata, per esempio per impedirne la successiva modifica. Allo stesso tempo la proprietà entra a far parte dell'API pubblica della classe, cosa che potrebbe non essere voluta. - -L'assegnazione alla proprietà si definisce nella configurazione del container DI, nella [sezione setup |services#Setup]: - -```neon -services: - - create: MyClass - setup: - - $cache = @\Cache -``` - - -Inject -====== - -Mentre i tre approcci precedenti valgono in generale in tutti i linguaggi orientati agli oggetti, l'iniezione tramite i metodi `inject*()` o l'attributo `#[Inject]` si usa di norma con i presenter di Nette, dove è attiva per impostazione predefinita; qualsiasi altro servizio può aderirvi con [`inject: true` |services#Modalità inject]. Se ne parla in un [capitolo a parte |best-practices:inject-method-attribute]. - - -Quale metodo scegliere? -======================= - -- Il costruttore è adatto alle dipendenze obbligatorie, di cui la classe ha assolutamente bisogno per funzionare. -- Il setter, al contrario, è adatto alle dipendenze facoltative o a quelle che potrebbe essere necessario cambiare in seguito. -- Le proprietà pubbliche in generale non sono consigliate. diff --git a/dependency-injection/it/services.texy b/dependency-injection/it/services.texy deleted file mode 100644 index f99d9d22e9..0000000000 --- a/dependency-injection/it/services.texy +++ /dev/null @@ -1,480 +0,0 @@ -Definizione dei servizi -*********************** - -.[perex] -La configurazione è il luogo in cui diciamo al container DI come creare i singoli servizi e come collegarli alle loro dipendenze. Nette offre un modo molto chiaro ed elegante di farlo. - -La sezione `services` del file di configurazione NEON è il luogo in cui definiamo i nostri servizi e le loro configurazioni. Guardiamo un semplice esempio che definisce un servizio chiamato `database`, il quale rappresenta un'istanza della classe `PDO`: - -```neon -services: - database: PDO('sqlite::memory:') -``` - -La configurazione qui sopra produce il seguente metodo factory nel [container DI|container]: - -```php -public function createServiceDatabase(): PDO -{ - return new PDO('sqlite::memory:'); -} -``` - -I nomi dei servizi permettono di farvi riferimento in altre parti del file di configurazione, con il formato `@nomeServizio`. Se non serve assegnare un nome al servizio, possiamo semplicemente usare un trattino (`-`): - -```neon -services: - - PDO('sqlite::memory:') -``` - -Per ottenere un servizio dal container DI possiamo usare il metodo `getService()`, con il nome del servizio come parametro, oppure il metodo `getByType()`, con il tipo del servizio: - -```php -$database = $container->getService('database'); -$database = $container->getByType(PDO::class); -``` - - -Creazione dei servizi -===================== - -Di norma creiamo un servizio semplicemente istanziando una determinata classe. Per esempio: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Se dobbiamo ampliare la configurazione con altre chiavi, la definizione si può spezzare su più righe: - -```neon -services: - database: - create: PDO('sqlite::memory:') - setup: ... -``` - -La chiave `create` ha un alias, `factory`; entrambe le varianti sono di uso comune. Consigliamo però di usare `create`. - -Gli argomenti del costruttore o del metodo factory si possono indicare, in alternativa, con la chiave `arguments`: - -```neon -services: - database: - create: PDO - arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] -``` - -I servizi non devono per forza essere creati con la semplice istanziazione di una classe: possono essere anche il risultato della chiamata di metodi statici o di metodi di altri servizi: - -```neon -services: - database: DatabaseFactory::create() - router: @routerFactory::create() -``` - -Notate che, per semplicità, si usa `::` invece di `->`, vedi [#Linguaggio delle espressioni]. Verranno generati questi metodi factory: - -```php -public function createServiceDatabase(): PDO -{ - return DatabaseFactory::create(); -} - -public function createServiceRouter(): RouteList -{ - return $this->getService('routerFactory')->create(); -} -``` - -Il container DI deve conoscere il tipo del servizio che sta creando. Se creiamo un servizio con un metodo privo di tipo di ritorno dichiarato, dobbiamo indicare esplicitamente questo tipo nella configurazione: - -```neon -services: - database: - create: DatabaseFactory::create() - type: PDO -``` - - -Argomenti -========= - -Passiamo gli argomenti ai costruttori e ai metodi in modo molto simile a come si fa in PHP: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Per una migliore leggibilità possiamo elencare gli argomenti su righe separate. In tal caso le virgole diventano facoltative: - -```neon -services: - database: PDO( - 'mysql:host=127.0.0.1;dbname=test' - root - secret - ) -``` - -Potete anche dare un nome agli argomenti, eliminando la necessità di preoccuparvi del loro ordine: - -```neon -services: - database: PDO( - username: root - password: secret - dsn: 'mysql:host=127.0.0.1;dbname=test' - ) -``` - -Se volete omettere certi argomenti e usarne i valori predefiniti, oppure farvi iniettare un servizio tramite l'[autowiring|autowiring], usate un trattino basso (`_`): - -```neon -services: - foo: Foo(_, %appDir%) -``` - -Gli argomenti possono comprendere servizi, parametri e molto altro, vedi [#Linguaggio delle espressioni]. - - -Setup -===== - -Nella sezione `setup` definiamo i metodi da chiamare al momento della creazione del servizio. - -```neon -services: - database: - create: PDO(%dsn%, %user%, %password%) - setup: - - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) -``` - -In PHP avrebbe questo aspetto: - -```php -public function createServiceDatabase(): PDO -{ - $service = new PDO('...', '...', '...'); - $service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - return $service; -} -``` - -Oltre alle chiamate di metodo si possono anche assegnare valori alle proprietà. È supportata anche l'aggiunta di elementi agli array, il che richiede di racchiudere tra apici l'accesso all'array, per evitare conflitti con la sintassi NEON: - -```neon -services: - foo: - create: Foo - setup: - - $value = 123 - - '$onClick[]' = [@bar, clickHandler] -``` - -Che in codice PHP avrebbe questo aspetto: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - $service->value = 123; - $service->onClick[] = [$this->getService('bar'), 'clickHandler']; - return $service; -} -``` - -Nel setup potete però chiamare anche metodi statici o metodi di altri servizi. Se dovete passare come argomento il servizio corrente stesso, fatevi riferimento con `@self`: - -```neon -services: - foo: - create: Foo - setup: - - My\Helpers::initializeFoo(@self) - - @anotherService::setFoo(@self) -``` - -Notate che, per semplicità, si usa `::` invece di `->`, vedi [#Linguaggio delle espressioni]. Verrà generato questo metodo factory: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - My\Helpers::initializeFoo($service); - $this->getService('anotherService')->setFoo($service); - return $service; -} -``` - - -Linguaggio delle espressioni -============================ - -Nette DI offre un linguaggio delle espressioni eccezionalmente ricco, con il quale possiamo definire quasi qualsiasi cosa. Nei file di configurazione possiamo quindi usare i [parametri |configuration#Parametri]: - -```neon -# parametro -%wwwDir% - -# valore di un parametro sotto una chiave -%mailer.user% - -# parametro dentro una stringa -'%wwwDir%/images' -``` - -Inoltre creare oggetti, chiamare metodi e funzioni: - -```neon -# crea un oggetto -DateTime() - -# chiama un metodo statico -Collator::create(%locale%) - -# chiama una funzione PHP -::getenv(DB_USER) -``` - -Fare riferimento ai servizi per nome oppure per tipo: - -```neon -# servizio per nome -@database - -# servizio per tipo -@Nette\Database\Connection -``` - -Usare la first-class callable syntax: .{data-version:3.2.0} - -```neon -# crea una callback, equivalente a [@user, logout] -@user::logout(...) -``` - -Usare le costanti: - -```neon -# costante di classe -FilesystemIterator::SKIP_DOTS - -# ottiene una costante globale con la funzione PHP constant() -::constant(\PHP_VERSION) -``` - -Accedere alle proprietà pubbliche e alle costanti di un servizio con `@servizio::membro`. Se il nome indichi una proprietà o una costante lo decide la sua prima lettera: un'iniziale minuscola significa una proprietà pubblica, una maiuscola significa una costante: - -```neon -# proprietà pubblica di un servizio (inizia con una lettera minuscola) -@settings::apiUrl - -# costante di classe di un servizio (inizia con una lettera maiuscola) -@settings::Version -``` - -Le chiamate di metodo si possono concatenare come in PHP. Per semplicità si usa `::` invece di `->`: - -```neon -DateTime()::format('Y-m-d') -# PHP: (new DateTime())->format('Y-m-d') - -@http.request::getUrl()::getHost() -# PHP: $this->getService('http.request')->getUrl()->getHost() -``` - -Potete usare queste espressioni ovunque: nella [creazione dei servizi |#Creazione dei servizi], negli [argomenti |#Argomenti], nella sezione [setup |#Setup] oppure nei [parametri |configuration#Parametri]: - -```neon -parameters: - ipAddress: @http.request::getRemoteAddress() - -services: - database: - create: DatabaseFactory::create( @anotherService::getDsn() ) - setup: - - initialize( ::getenv('DB_USER') ) -``` - - -Funzioni speciali ------------------ - -Nei file di configurazione potete usare queste funzioni speciali: - -- `not()` nega un valore -- `bool()`, `int()`, `float()`, `string()` conversione senza perdita nel tipo indicato .{data-version:3.0.5} -- `typed()` crea un array di tutti i servizi del tipo indicato -- `tagged()` crea un array di tutti i servizi con il tag indicato - -```neon -services: - - Foo( - id: int(::getenv('ProjectId')) - productionMode: not(%debugMode%) - ) -``` - -A differenza della conversione standard di PHP, come `(int)`, la conversione senza perdita solleva un'eccezione per i valori non numerici. - -La funzione `typed()` crea un array di tutti i servizi del tipo indicato (classe o interfaccia). Esclude i servizi che hanno l'autowiring disattivato. Si possono indicare anche più tipi, separati da virgole. - -```neon -services: - - BarsDependent( typed(Bar) ) -``` - -Un array di servizi di un certo tipo si può passare come argomento anche automaticamente, tramite l'[autowiring |autowiring#Collezione di servizi]. - -La funzione `tagged()` crea invece un array di tutti i servizi con un determinato tag. Anche qui potete indicare più tag separati da virgole. - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - - -Autowiring -========== - -La chiave `autowired` vi permette di influire sul comportamento dell'autowiring per un determinato servizio. Per i dettagli vedi il [capitolo sull'autowiring|autowiring]. - -```neon -services: - foo: - create: Foo - autowired: false # il servizio foo è escluso dall'autowiring -``` - - -Servizi pigri .{data-version:3.2.4} -=================================== - -Il caricamento pigro è una tecnica che rimanda la creazione di un servizio finché non serve davvero. Nella configurazione globale potete [attivare la creazione pigra |configuration#Servizi pigri] per tutti i servizi in una volta. Per i singoli servizi potete poi sovrascrivere questo comportamento: - -```neon -services: - foo: - create: Foo - lazy: false -``` - -Quando un servizio è definito come pigro, chiedendolo al container DI riceviamo un oggetto proxy speciale. Questo proxy sembra e si comporta esattamente come il servizio reale, ma l'inizializzazione vera e propria (la chiamata del costruttore e quelle del setup) avviene solo al primo accesso a uno qualsiasi dei suoi metodi o delle sue proprietà. - -Tenete presente che, poiché il servizio viene creato più tardi, anche gli errori nella sua configurazione si manifestano più tardi. Per esempio, credenziali del database sbagliate non si riveleranno all'avvio dell'applicazione, ma solo alla prima query. - -La creazione pigra attenua anche le dipendenze circolari, cioè la situazione in cui il servizio A richiede il servizio B e contemporaneamente B richiede A. Senza di essa il container segnala l'errore `Circular reference detected`. Con un proxy pigro il servizio A riceve solo un proxy del servizio B, che si inizializza quando viene davvero usato, in un momento in cui A esiste già. Una dipendenza circolare, comunque, segnala una progettazione difettosa, ed è meglio liberarsene. - -.[note] -Il caricamento pigro richiede PHP 8.4 o successivo e funziona solo per i servizi creati istanziando direttamente una classe (per esempio `create: Foo`), non per quelli creati da un metodo factory. Non si può usare nemmeno per le classi che in ultima analisi estendono una classe interna di PHP. Quando il caricamento pigro non si può applicare, il flag `lazy: true` viene ignorato in silenzio. - - -Tag -=== - -I tag servono ad aggiungere informazioni supplementari ai servizi. Potete assegnare a un servizio uno o più tag: - -```neon -services: - foo: - create: Foo - tags: - - cached -``` - -I tag possono anche contenere valori: - -```neon -services: - foo: - create: Foo - tags: - logger: monolog.logger.event -``` - -Per ottenere tutti i servizi associati a determinati tag potete usare la funzione `tagged()`: - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - -All'interno del container DI potete ottenere i nomi di tutti i servizi con un determinato tag usando il metodo `findByTag()`: - -```php -$names = $container->findByTag('logger'); -// $names è un array con i nomi dei servizi come chiavi e i valori dei tag come valori -// per esempio ['foo' => 'monolog.logger.event', ...] -``` - - -Modalità inject -=============== - -Usando il flag `inject: true` si attiva la dependency injection tramite le proprietà pubbliche con l'attributo [Inject |best-practices:inject-method-attribute#Attributi Inject] e tramite i metodi [inject*() |best-practices:inject-method-attribute#Metodi inject*()]. - -```neon -services: - articles: - create: App\Model\Articles - inject: true -``` - -Per impostazione predefinita la modalità `inject` è attiva solo per i presenter. - - -Modifiche ai servizi -==================== - -Il container DI contiene numerosi servizi aggiunti da estensioni integrate o [dell'utente|extensions]. Potete modificare le definizioni di questi servizi esistenti direttamente nella configurazione. Per esempio potete cambiare la classe del servizio `application.application`, che di norma è `Nette\Application\Application`, con un'altra: - -```neon -services: - application.application: - create: MyApplication - alteration: true -``` - -Il flag `alteration` indica che stiamo solo modificando un servizio esistente. Funge anche da protezione: se il servizio da modificare non esiste, la compilazione fallisce con un'eccezione. - -Possiamo anche integrare il setup: - -```neon -services: - application.application: - create: MyApplication - alteration: true - setup: - - '$onStartup[]' = [@resource, init] -``` - -Non dovete identificare un servizio con il suo nome interno: potete fare riferimento a esso per tipo. L'esempio precedente si può scrivere anche così: - -```neon -services: - @Nette\Application\Application: - create: MyApplication -``` - -Modificando un servizio potremmo voler rimuovere gli argomenti, gli elementi del setup o i tag originali, con la chiave `reset`: - -```neon -services: - application.application: - create: MyApplication - alteration: true - reset: - arguments: true - setup: true - tags: true -``` - -Se volete rimuovere un servizio aggiunto da un'estensione, potete farlo così: - -```neon -services: - cache.journal: false -``` diff --git a/dependency-injection/it/upgrading.texy b/dependency-injection/it/upgrading.texy deleted file mode 100644 index fb5e8eb11c..0000000000 --- a/dependency-injection/it/upgrading.texy +++ /dev/null @@ -1,49 +0,0 @@ -Aggiornamento -************* - - -Aggiornamento alla versione 3.1 -=============================== - -- l'autowiring non passa più `null` a un parametro nullable privo di valore predefinito; passate l'argomento esplicitamente oppure date al parametro un valore predefinito -- il supporto dell'annotazione `@return` è stato eliminato; usate un tipo di ritorno oppure indicate il tipo nella definizione del servizio con `type:` -- la chiave `dynamic` è stata rinominata in `imported` e `class` in `type` -- il simbolo per un argomento omesso è cambiato da `...` a `_`, per esempio `MyService(_, 123)` -- nei file NEON il carattere `@` all'inizio di una stringa non ha più bisogno di essere escapato -- la chiave `parameters` all'interno delle definizioni delle factory generate è deprecata -- il metodo `Nette\DI\Config\Loader::save()` è deprecato; esportate la configurazione con `Nette\DI\Config\Adapters\NeonAdapter::dump()` - -La versione 3.1 è una versione di transizione: non porta nuove funzionalità, ma avvisa con dei notice di tutto ciò che in seguito funzionerà diversamente. Vedi l'articolo [Nette DI 3.1: transition release |https://blog.nette.org/en/nette-di-3-1-transition-release]. - - -Aggiornamento alla versione 3.0 -=============================== - -- il supporto dei file INI è stato rimosso -- la scrittura diretta di codice PHP nella configurazione tramite i punti interrogativi (per esempio `"$service->onError[] = ?"(...)`) è stata rimossa; usate invece la sintassi ad array `'$onError[]' = [...]` -- nei file di configurazione usate `factory: PDO(...)` invece di `class: PDO(...)` -- il tag `nette.presenter` non viene più usato per i presenter - - -Per gli autori di estensioni del compilatore --------------------------------------------- - -Mentre Nette 2.4 descriveva internamente ogni servizio come `Nette\DI\ServiceDefinition`, ora esistono diversi tipi di definizione: `Nette\DI\Definitions\ImportedDefinition` per i servizi importati (dinamici), `Nette\DI\Definitions\FactoryDefinition` per le factory generate a partire da un'interfaccia, `Nette\DI\Definitions\AccessorDefinition` per gli accessor generati e `Nette\DI\Definitions\ServiceDefinition` per i servizi comuni. - -Perciò, oltre a `ContainerBuilder::addDefinition()`, esistono altri metodi per creare una nuova definizione: `addFactoryDefinition()`, `addAccessorDefinition()` e `addImportedDefinition()`. - - -Aggiornamento alla versione 2.4 -=============================== - -- le sezioni di configurazione (per esempio production, development) in un unico file di config sono deprecate; usate una coppia di file `config.neon` e `config.local.neon` -- l'ereditarietà delle definizioni dei servizi è deprecata -- `Statement::setEntity()` è deprecato - - -Aggiornamento alla versione 2.3 -=============================== - -- il supporto per collocare i servizi dentro la sezione delle estensioni del file di configurazione è stato rimosso -- il supporto per le estensioni aggiunte dinamicamente è stato rimosso -- sostituendo dinamicamente un servizio (con `removeService()`, `addService()`), il nuovo servizio deve essere un'istanza della stessa interfaccia o classe dell'originale diff --git a/dependency-injection/ja/@home.texy b/dependency-injection/ja/@home.texy deleted file mode 100644 index b31effb927..0000000000 --- a/dependency-injection/ja/@home.texy +++ /dev/null @@ -1,22 +0,0 @@ -Nette DI -******** - -.[perex] -依存性注入(Dependency Injection)は、コードと開発に対する見方を根本から変えるデザインパターンです。きれいに設計され、長く保てるアプリケーションの世界への道を開きます。 - -- [Dependency Injection とは |introduction] -- [グローバル状態とシングルトン |global-state] -- [依存関係の受け渡し |passing-dependencies] -- [DI コンテナとは |container] -- [よくある質問 |faq] - - -`nette/di` パッケージは、PHP のためのきわめて高度なコンパイル済み DI コンテナを提供します。 - -- [Nette DI Container |nette-container] -- [設定 |configuration] -- [サービスの定義 |services] -- [オートワイヤリング |autowiring] -- [生成されるファクトリ |factory] -- [Nette DI の拡張の作成 |extensions] -- [コンテナのコンパイルの詳細 |compilation-internals] diff --git a/dependency-injection/ja/@left-menu.texy b/dependency-injection/ja/@left-menu.texy deleted file mode 100644 index e0ca8dae9b..0000000000 --- a/dependency-injection/ja/@left-menu.texy +++ /dev/null @@ -1,28 +0,0 @@ -Dependency Injection -******************** -- [DI とは |introduction] -- [グローバル状態とシングルトン |global-state] -- [依存関係の受け渡し |passing-dependencies] -- [DI コンテナとは |container] -- [よくある質問 |faq] - - -Nette DI --------- -- [Nette DI Container |nette-container] -- [設定 |configuration] -- [サービスの定義 |services] -- [オートワイヤリング |autowiring] -- [生成されるファクトリ |factory] -- [Nette DI の拡張の作成 |extensions] -- [コンパイルの詳細 |compilation-internals] -- [アップグレード|upgrading] - - -関連情報 -**** -- [Nette ドキュメント |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [ベストプラクティス |best-practices:] -- [トラブルシューティング |nette:troubleshooting] diff --git a/dependency-injection/ja/@meta.texy b/dependency-injection/ja/@meta.texy deleted file mode 100644 index 43b85f3cac..0000000000 --- a/dependency-injection/ja/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette ドキュメント}} diff --git a/dependency-injection/ja/autowiring.texy b/dependency-injection/ja/autowiring.texy deleted file mode 100644 index a0a962b972..0000000000 --- a/dependency-injection/ja/autowiring.texy +++ /dev/null @@ -1,279 +0,0 @@ -オートワイヤリング -********* - -.[perex] -オートワイヤリングは、必要なサービスをコンストラクタやほかのメソッドに自動的に渡してくれる素晴らしい機能で、明示的に指定する必要がなくなります。多くの時間を節約できます。 - -おかげで、サービスの定義を書くときに大部分の引数を省けます。次のように書く代わりに、 - -```neon -services: - articles: Model\ArticleRepository(@database, @cache.storage) -``` - -こう書くだけで済みます。 - -```neon -services: - articles: Model\ArticleRepository -``` - -オートワイヤリングは型に導かれるので、それが働くには `ArticleRepository` クラスがおおよそ次のように定義されている必要があります。 - -```php -namespace Model; - -class ArticleRepository -{ - public function __construct(\PDO $db, \Nette\Caching\Storage $storage) - {} -} -``` - -オートワイヤリングはサービスの名前を決して使いません。PHP の型システムだけに導かれるので、クラスが実装するインターフェースや継承するクラスも満たすと分かります。おかげでサービスの名前は補助的な識別子にすぎず、名前を変えてもアプリケーションの何も壊れません。 - -オートワイヤリングを使うには、コンテナの中に各型の**サービスがちょうどひとつ**なければなりません。複数あると、オートワイヤリングはどれを渡すべきか分からず例外を投げます。 - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - tempDb: PDO('sqlite::memory:') - articles: Model\ArticleRepository # 例外を投げます。mainDb と tempDb の両方が当てはまります -``` - -ひとつの解は、オートワイヤリングを迂回してサービス名を明示的に指定すること(たとえば `articles: Model\ArticleRepository(@mainDb)`)です。とはいえ、いずれかのサービスのオートワイヤリングを[無効にする |#オートワイヤリングの無効化]か、ひとつのサービスをほかより[優先する |#オートワイヤリングの優先]ほうが便利です。 - - -オートワイヤリングの無効化 -------------- - -`autowired: false` オプションを使うと、サービスのオートワイヤリングを無効にできます。 - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - - tempDb: - create: PDO('sqlite::memory:') - autowired: false # tempDb サービスはオートワイヤリングから除かれます - - articles: Model\ArticleRepository # なのでコンストラクタには mainDb が渡されます -``` - -`articles` サービスは、コンストラクタに使える `PDO` サービスが 2 つ(`mainDb` と `tempDb`)あるという例外を投げません。`mainDb` サービスだけを考慮するからです。 - -オートワイヤリングは、[`di › excluded` |configuration#DI]設定オプションを使って型ごとにまとめて無効にすることもできます。ここには、決してオートワイヤリングされるべきでない型(とその子孫)を並べます。 - -.[note] -Nette のオートワイヤリングの設定は Symfony とは違います。Symfony では `autowire: false` は、そのサービスのコンストラクタの引数にオートワイヤリングを使わない、という意味です。Nette では、オートワイヤリングはコンストラクタの引数と、コンテナ経由で呼ばれるほかのメソッド(セッターインジェクションなど)に当てはまります。`autowired: false` オプションは、そのサービスのインスタンスがほかのサービスの依存関係として自動的に渡されるのをコンテナに禁じます。 - - -オートワイヤリングの優先 ------------- - -同じ型のサービスが複数あり、そのひとつに `autowired` オプションを指定すると、そのサービスが優先されるようになります。 - -```neon -services: - mainDb: - create: PDO(%dsn%, %user%, %password%) - autowired: PDO # 優先されるようになります - - tempDb: - create: PDO('sqlite::memory:') - - articles: Model\ArticleRepository -``` - -`articles` サービスは、当てはまる `PDO` サービスが複数(`mainDb` と `tempDb`)あるという例外を投げず、優先されるほう、つまり `mainDb` を使います。 - - -サービスのコレクション ------------ - -オートワイヤリングは、特定の型のサービスの配列を渡すこともできます。PHP は型宣言で配列の要素の型を指定できないので、`array` の型宣言に `ClassName[]` のように要素の型を示す phpDoc のコメントを添える必要があります。 - -```php -namespace Model; - -class ShipManager -{ - /** - * @param Shipper[] $shippers - */ - public function __construct(array $shippers) - {} -} -``` - -すると DI コンテナは、その型に対応するサービスの配列を自動的に渡します。[オートワイヤリングが無効にされた |#オートワイヤリングの無効化]サービスは除かれ、今まさに作られているサービス自身がそのコレクションに含まれることもありません。個々のサービスを渡す場合と違い、オートワイヤリングを特定の型に[絞り込んだり |#オートワイヤリングの絞り込み]、サービスを[優先 |#オートワイヤリングの優先]にしたりしても、ここでは影響がありません。配列には常にその型のすべてのサービスが入ります。 - -コメントの型は `array<int, Class>` や `list<Class>` の形でもかまいません。phpDoc のコメントの形を自分で決められない場合は、[`typed()` |services#特別な関数]を使って設定でサービスの配列を直接渡せます。 - - -スカラーの引数 -------- - -オートワイヤリングが働くのはオブジェクトとオブジェクトの配列だけです。スカラーの引数(文字列、数値、真偽値など)は[設定で指定 |services#引数]しなければなりません。別の方法として、そのスカラー値(や複数の値)を包む[設定オブジェクト|best-practices:passing-settings-to-presenters]を作ることもできます。そのオブジェクトはオートワイヤリングで渡せます。 - -```php -class MySettings -{ - public function __construct( - // readonly は PHP 8.1 以降で使えます - public readonly bool $value, - ) - {} -} -``` - -設定に足してサービスとして登録します。 - -```neon -services: - - MySettings('any value') -``` - -これでほかのクラスがオートワイヤリングで要求できます。 - - -省略可能な依存関係 ---------- - -コンストラクタやメソッドのパラメータに既定値があり、求める型のサービスがコンテナにない場合、オートワイヤリングは例外を投げず、その引数を単に飛ばすので既定値が使われます。省略可能な依存関係はこうして宣言します。 - -```php -class Foo -{ - public function __construct( - private ?Logger $logger = null, - ) {} -} -``` - -これに対し、既定値のないパラメータでは、サービスがないと常に例外になります。 - - -オートワイヤリングの絞り込み --------------- - -個々のサービスについて、オートワイヤリングを特定のクラスやインターフェースに絞り込めます。 - -ふつうオートワイヤリングは、そのサービスが型として当てはまるすべてのメソッドのパラメータにサービスを渡します。絞り込みとは、サービスが渡されるためにメソッドのパラメータの型が満たすべき条件を設けることです。 - -例を見てみましょう。 - -```php -class ParentClass -{} - -class ChildClass extends ParentClass -{} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -これらをすべてサービスとして登録すると、オートワイヤリングは失敗します。 - -```neon -services: - parent: ParentClass - child: ChildClass - parentDep: ParentDependent # 例外を投げます。parent と child の両方が当てはまります - childDep: ChildDependent # オートワイヤリングは child サービスをコンストラクタに渡します -``` - -`parentDep` サービスは `Multiple services of type ParentClass found: child, parent` という例外を投げます。`parent` と `child` の両方がそのコンストラクタに当てはまり、オートワイヤリングがどちらを選ぶか決められないからです。 - -そこで `child` サービスについて、オートワイヤリングを `ChildClass` 型に絞り込めます。 - -```neon -services: - parent: ParentClass - child: - create: ChildClass - autowired: ChildClass # 'autowired: self' とも書けます - - parentDep: ParentDependent # オートワイヤリングは parent サービスをコンストラクタに渡します - childDep: ChildDependent # オートワイヤリングは child サービスをコンストラクタに渡します -``` - -これで `parentDep` サービスのコンストラクタには `parent` サービスが渡されます。当てはまるオブジェクトがそれだけになったからです。`child` サービスはもうそこへオートワイヤリングされません。たしかに `child` サービスは今も `ParentClass` 型ですが、絞り込みの条件 `autowired: ChildClass` により、明示的に `ChildClass`(またはその派生型)と型付けされたパラメータにしか渡されません。`ParentDependent` は `ParentClass` を求めるので、`child` サービスはそこでのオートワイヤリングの候補から外れます。 - -`child` サービスの `autowired: ChildClass` は `autowired: self` とも書けます。`self` は現在のサービスのクラスを表すプレースホルダーだからです。 - -`autowired` キーには、複数のクラスやインターフェースを配列として指定することもできます。 - -```neon -autowired: [ParentClass, FooInterface] -``` - -例にインターフェースを足してみましょう。 - -```php -interface FooInterface -{} - -interface BarInterface -{} - -class ParentClass implements FooInterface -{} - -class ChildClass extends ParentClass implements BarInterface -{} - -class FooDependent -{ - function __construct(FooInterface $obj) - {} -} - -class BarDependent -{ - function __construct(BarInterface $obj) - {} -} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -`child` サービスを何も制限しなければ、`FooDependent`、`BarDependent`、`ParentDependent`、`ChildDependent` のすべてのクラスのコンストラクタに当てはまり、オートワイヤリングはそこへ渡します。 - -しかし `autowired: ChildClass`(または `self`)でオートワイヤリングを `ChildClass` に絞り込むと、渡されるのは `ChildDependent` のコンストラクタだけになります。そこは `ChildClass` 型の引数を求めており、`ChildClass` は `ChildClass` *である*からです。ほかのパラメータが求める型はどれも `ChildClass` でもその派生型でもないので、サービスは渡されません。 - -`autowired: ParentClass` で `ParentClass` に制限すると、オートワイヤリングは再び `ChildDependent` のコンストラクタに渡し(求められる `ChildClass` は `ParentClass` の派生型だからです)、さらに `ParentDependent` のコンストラクタにも渡します。求められる型 `ParentClass` も適合するからです。 - -`FooInterface` に制限すると、引き続き `ParentDependent`(求められる `ParentClass` は `FooInterface` の派生型です)と `ChildDependent` にオートワイヤリングされ、加えて `FooDependent` のコンストラクタにも渡されますが、`BarDependent` には渡されません。`BarInterface` は `FooInterface` の派生型ではないからです。 - -```neon -services: - child: - create: ChildClass - autowired: FooInterface - - fooDep: FooDependent # オートワイヤリングは child サービスをコンストラクタに渡します - barDep: BarDependent # 例外を投げます。当てはまるサービスがありません - parentDep: ParentDependent # オートワイヤリングは child サービスをコンストラクタに渡します - childDep: ChildDependent # オートワイヤリングは child サービスをコンストラクタに渡します -``` diff --git a/dependency-injection/ja/compilation-internals.texy b/dependency-injection/ja/compilation-internals.texy deleted file mode 100644 index 0a6d842e3e..0000000000 --- a/dependency-injection/ja/compilation-internals.texy +++ /dev/null @@ -1,222 +0,0 @@ -コンテナのコンパイルの詳細 -************* - -.[perex] -このページはコンテナのコンパイルを解きほぐします。どんな段階を経るのか、設定のパラメータがいつ展開されるのか、`@service` の文字列がいつ本当の参照になるのか、そして拡張の作者が最もよく尋ねる問い、つまりどの段階なら型でサービスを安全に探せるのかを扱います。[拡張の作成 |extensions]の、より深い姉妹編です。 - -ふつうのアプリケーションを書くのに、いえ、ふつうの拡張を書くのにさえ、この内容は必要ありません。しかし拡張がサービスのつながりを調べたり組み替えたりし始めると、タイミングがすべてになります。同じ `getByType()` の呼び出しが、ある段階では確実な答えを、別の段階では誤解を招く答えを返すのです。このページはその理由を説明するので、自分のコードをどこに置くべきかがいつでも分かるようになります。 - - -2 つの世界: コンパイル時と実行時 -================== - -まず理解すべき最も重要なことは、Nette のコンテナが**リクエストごとに組み立てられるのではない**という点です。最適化された PHP のクラスとして一度だけ構築され、そのクラスはディスクに保存され、以降のリクエストは出来上がったファイルを `include` するだけです。以下で説明するしくみ、つまり拡張、リゾルバ、コードジェネレータは、**(再)コンパイルのときにだけ**走ります。 - -これにより世界は、決して同時には存在しない 2 つの表現に分かれます。 - -| | コンパイル時 | 実行時 -|---|---|--- -| 存在するもの | `ContainerBuilder` の中の**定義**(レシピ) | `Container` の中のサービスの**インスタンス** -| 主なクラス | `Compiler`、`ContainerBuilder`、`Resolver`、`PhpGenerator` | `Container`(生成されるクラスの親) -| `%param%`、`@service` | まだ変換の途中にあるテキストの印 | すでに変換済み/コードに焼き込み済み - -生成されるクラスは `Nette\DI\Container` を継承し、サービスごとに `createServiceXxx()` メソッドを持ちます。そのパラメータとオートワイヤリングのメタデータはあらかじめ計算されているので、実行時に解決すべきものは何も残っておらず、必要に応じてサービスを生成するだけです。 - -.[note] -開発モードでは、設定ファイルや拡張のクラスが変わるたびにコンテナが自動的に再構築されます。どちらも依存関係として追跡されているからです。本番では一度コンパイルされたきりで、二度と確認されません。速さはそこから来ています。 - - -段階のあらまし -======= - -コンパイルは `Compiler::compile()` が取り仕切り、結局は 3 つの段階に落ち着きます。 - -```php -public function compile(): string -{ - $this->processExtensions(); // 段階 A: スキーマ + loadConfiguration() - $this->processBeforeCompile(); // 段階 B: resolve + beforeCompile() + complete - return $this->generateCode(); // 段階 C: コード生成 + afterCompile() -} -``` - -全体の見取り図はひとつの考えに収まります。**あとの段階ほど多くを知っている**、ということです。 - -- **段階 A** はグラフを定義で満たします。サービスの**型はまだ確実には分かりません**。型がファクトリの戻り値から来ていて、まだ誰もそれを見ていないことがあるからです。 -- **段階 B** はまずすべての型を解決し(`resolve`)、次に拡張にグラフを組み替えさせ(`beforeCompile`)、最後に引数を[オートワイヤリング |autowiring]します(`complete`)。 -- **段階 C** は出来上がったグラフを PHP に変え、拡張が生成されたコードに触れられるようにします。 - -知識が増えていくこの流れこそ、同じ操作がある段階では安全で別の段階では当てにならない理由です。このページの残りは、その考えを念頭に段階を辿っていきます。 - - -段階 A: 定義の登録 -=========== - -この段階で Nette は、各拡張の 3 つのメソッド、`getConfigSchema()`、次に `setConfig()`、そして `loadConfiguration()` を呼びます。ただし**慎重に制御された順序**で呼びます。ここでは順序が本当に重要だからです。 - - -なぜ順序が重要なのか ----------- - -- **`ParametersExtension` と `ExtensionsExtension` が最初です。** 前者はほかの何よりも先に走って、設定全体で `%param%` を展開しなければなりません。そうすればほかのすべての拡張は、値が埋まった状態で自分のセクションを受け取れます。後者は `extensions:` セクションに並べられたさらなる拡張を登録するので、これも残りが処理される前に存在している必要があります。 -- **`ServicesExtension` が最後です。** ですからユーザーの `services:` セクションが常に最終決定権を持ち、拡張が用意したものを何でも上書きできます。 -- **`InjectExtension` は最後尾に移されます。** その仕事が、ほかのすべての拡張が足した setup を見られるようにするためです。 - -あなたにとっての要点はこうです。あなたの拡張の `loadConfiguration()` が走る時点で、パラメータはすでに展開されていますが、ユーザーのサービスはまだそこにありません。この事実ひとつが、以下のタイミングの規則のほとんどを決めています。 - - -services: を定義に変える ------------------ - -ユーザーの `services:` セクションは、段階 A の最後の手順としてここで[定義のオブジェクト |extensions#定義の種類]に変えられます。NEON の各項目は正規化され(略記が統一され)、その種類が判別され(通常のサービス、ファクトリ、アクセサ、…)、builder に対応する定義が作られます。単純な `@name` / `@Type` の引数が参照になるのも、ここが最初の瞬間です。[後述 |#参照: @service はいつ参照になるか]をご覧ください。 - -段階 A の終わりには、すべての定義がそろっています。どの拡張もユーザーも、登録したいものを登録し終えています。それでも絵はまだ鮮明ではありません。 - -- 型がファクトリの戻り値から来る定義については、**型が解決されていません**、 -- **引数がオートワイヤリングされていません**、 -- 一部の `@service` の参照はまだただの文字列です。 - -だからこそ、ここで型を使って探すのは当てになりません。詳しくは[後述 |#ContainerBuilder を覗く: いつなら安全か]をご覧ください。 - - -パラメータ: %param% はいつ展開されるか -======================== - -2 つの目玉の問いのひとつです。答えは短く、**段階 A のいちばん最初に、設定のツリー全体に対して一度だけ**です。 - -`ParametersExtension` が最初に走り、その最初の仕事のひとつが `%param%` のプレースホルダーの展開です。まずパラメータ自身の中で(パラメータはほかのパラメータを参照できます)、次に設定の残り全体で展開します。ですから `ServicesExtension` を含むほかの拡張が自分のセクションを受け取る時点で、プレースホルダーはもう消えています。拡張が扱うのは具体的な値であって、`%...%` ではありません。 - -プレースホルダーが文字列の全体である場合、その値は*そのまま*返されます。配列やオブジェクトも含みます。ですから `%mailer%` は配列まるごとに展開できます。それ以外の場所では文字列に連結され、ドット記法 `%foo.bar%` は入れ子の配列の中に届きます。 - - -静的なパラメータと動的なパラメータ ------------------ - -すべての値をコードに焼き込めるわけではありません。環境ごとに異なる値、たとえば環境変数や、リクエストから導かれる `baseUrl` は**動的**でなければなりません。そうしたパラメータは `setDynamicParameterNames()` か、スキーマの `Expect::...->dynamic()` で宣言します。詳しくは[動的パラメータ |application:bootstrapping#動的パラメータ]をご覧ください。 - -動的なパラメータは値に置き換えられるのではなく、*実行時に*それを読む式に置き換えられます。ですから `%env.DB_HOST%` は文字列に固定されず、生成されたコンテナの中で実行時に参照されます。それ以外はすべて静的で、コンパイル時に固定されます。「`getenv()` の値がどの環境でも同じになる」という驚きは、たいていここから来ます。そのパラメータが単に静的だっただけです。 - -逆の操作が**エスケープ**です。文字どおりの `%` や `@` が解釈されないようにするには、二重にします(`%%`、`@@`)。Nette は自分が注入するパラメータについてこれを自動的に行うので、その値がプレースホルダーや参照と取り違えられることはありません。 - - -参照: @service はいつ参照になるか -====================== - -2 つめの目玉の問いです。`@service` の変換は、文字列がどれくらい複雑かに応じて、**異なる段階にまたがる何段階か**で起こります。これを手で追う必要はめったにありませんが、その手順を知っておくと、一部の参照がほかより早く解決される理由が分かります。 - -- **解析(設定の読み込み)。** *エンティティとして*使われた `@service`、つまり `Foo(@bar)` のようにサービスを作るものは、ただちに参照になります。*引数として*使われた `@service` は、いまのところただの文字列のままです。引用符で囲まれた `@` は `@@` にエスケープされるので、参照ではなく文字どおりのテキストとして扱われます。 -- **段階 A(`loadConfiguration`)。** 定義が処理されるとき、素の `@name` や `@Type` の引数が `Reference` オブジェクトになります。ここで捕まるのは単純な形だけで、`@service::CONST` や大きな式の中の `@` はあとに回されます。 -- **段階 B(`complete`)。** 本当に「賢い」変換はここで起こります。`@service` → 参照、`@service::CONSTANT` → クラス定数のリテラル、`@service::property` → そのプロパティの読み取り、`@@x` → 文字どおりのテキスト `@x` です。 - -*参照*という言葉自体にも、もうひとつの変換が隠れています。`Reference` は**名前**でも**型**(`@Namespace\Type`)でも指せます。型による参照は**まだサービス名ではありません**。具体的な名前への解決はオートワイヤリングが行い、それは**complete** の手順、つまりオートワイヤリングの索引が組み上がってからにしか起こりません。これが次の節への橋渡しです。オートワイヤリングによる検索は、索引が整うまで意図的に先送りされているのです。 - -| 形 | 参照や式になるのは | 具体的なサービスに解決されるのは -|---|---|--- -| エンティティ(ファクトリとしての `@foo`) | 解析 | complete -| 引数の `@foo`、`@Type` | 段階 A | complete -| `@foo::CONST`、`@foo::prop` | 段階 B | complete -| 型による参照 `@Type` | 段階 A/B | complete(オートワイヤリング) - - -ContainerBuilder を覗く: いつなら安全か -============================= - -さて、拡張の作者が最もよく尋ねる問いです。**どのメソッドなら型でサービスを探せるのか。** 答えは、builder が自分の状態をどう追跡しているかについての単純な規則から導かれます。 - -**型による**検索(`getByType()`、`getDefinitionByType()`、`findByType()`)には、サービスのつながりが*解決済み*であること、つまりすべての型が分かり、オートワイヤリングの索引が組み上がっていることが必要です。ですからこれらを呼んだとき、前回の解決以降にグラフが変わっていれば、builder は**その場で分かっている範囲のグラフ全体を解決します**。解決そのものの最中は型による検索が禁じられ、`NotAllowedDuringResolvingException` が投げられます。 - -**タグによる**検索(`findByTag()`)にはそうした前提がありません。タグは型に依存しないので、**どの段階でも**働きます。 - -段階ごとに見ていきましょう。 - -- **`loadConfiguration()`(段階 A)— 型による検索は当てになりません。** グラフは未完成です。あとで走る拡張はまだ自分のサービスを登録していませんし、何よりユーザーの `services:`(最後に走ります)がまだありません。`getByType()` の呼び出し自体は動きます。部分的なグラフの早すぎる解決を引き起こすからです。しかし答えは未完成の絵から来ますし、早すぎる解決は無駄な労力です。目安はこうです。**`loadConfiguration()` では定義の登録だけを行い、型で検索しないこと。** `findByTag()` なら問題ありません。 -- **`beforeCompile()`(段階 B)— 覗くのに適した場所です。** この時点で(ユーザーのものも含めて)**すべての定義**が存在し、**型は解決済み**で、**オートワイヤリングの索引も組み上がっています**。ですから `getByType()`、`findByType()`、`findByTag()` はどれも**確実な**答えを返します。引数はまだオートワイヤリングされて*いません*。それはすぐ次の手順(`complete`)で、すべての `beforeCompile()` の呼び出しのあとに行われます。ここで定義を変更すると、次の `getByType()` がグラフを透過的に解決し直すので、編集と問い合わせを自由に交互に行えます。 -- **`afterCompile()`(段階 C)— コードだけ。** builder ではなく、生成されたクラスに対して働きます。グラフは完成しています。ここでは出来上がる PHP の形を整えます。 - -| やりたいこと | 段階 -|---|--- -| サービスを登録する | `loadConfiguration()` -| **タグ**で検索して定義を変更する | `loadConfiguration()` または `beforeCompile()` -| **型**で検索する(`getByType`/`findByType`) | **`beforeCompile()`** -| 引数にオートワイヤリングが選んだサービスに依存する | コンパイル時には不可。実行時に調べてください -| 生成されたコードに手を入れる | `afterCompile()` -| コンテナの起動後にコードを走らせる | [初期化のコード |extensions#初期化のコード] - - -段階 B の中身: resolve と complete -============================ - -段階 B は 2 回の走査で、そのあいだに `beforeCompile()` の呼び出しが挟まれます。 - -```php -$this->builder->resolve(); // 型を解決し、オートワイヤリングの索引を作ります -foreach ($this->extensions as $extension) { - $extension->beforeCompile(); -} -$this->builder->complete(); // ここではじめて引数がオートワイヤリングされます -``` - -**`resolve()`** はすべてのサービスの型を決めます。宣言された `type` から取るか、ファクトリから推し量ります。ファクトリメソッドの戻り値の型、インスタンス化されるクラス、参照が指すサービスからです。そして各型(そのクラスと親、インターフェース)をサービス名に対応づけるオートワイヤリングの索引を作ります。`autowired: false` と印を付けたサービスは索引から外れ、`autowired: [A, B]` はそれが見える型を絞ります。大事なのは、resolve が決めるのは*型*であって*引数*ではないという点です。引数のオートワイヤリングには完成した索引が必要で、それはこの走査のあとにしか存在しません。 - -**`complete()`** で、引数のオートワイヤリングが実際に起こります。各定義について、足りないコンストラクタと setup の引数を、いまや完成した索引でその型を引いて埋めます。型による参照が resolve の最中に未解決のまま残されていたのはこのためです。その検索は、頼れる索引ができてからのこの場所に属するのです。 - - -段階 C: コードの生成 -============ - -`generateCode()` は出来上がったグラフを `PhpGenerator` に渡し、`Container` を継承しサービスごとに `createServiceXxx()` メソッドを持つクラスと、あらかじめ計算された `aliases`、`tags`、`wiring` のメタデータを作らせます。各 `Statement` は PHP のテキスト(`new Foo(...)`、メソッドの呼び出し、プロパティへのアクセス)になり、各 `Reference` は `$this->getService(...)` の呼び出しになります。 - -そして拡張は、生成されたクラスに対する最後の `afterCompile()` の機会を得ます。たとえば静的・動的なパラメータのゲッターはここで出力されます。あわせて、リクエストごとに走る[初期化のコード |extensions#初期化のコード]を足す機会もあります。 - - -ひとつの図で見る時間の流れ -============= - -``` -コンパイル(一度だけ。キャッシュへ) -│ -├─ 設定ファイルを読み込む NEON -> Statement/配列; ファイルの統合 -│ 引用符の中の @ -> @@ ; エンティティ -> Statement -│ -▼ Compiler::compile() -│ -├─ PHASE A processExtensions() -│ ├─ ParametersExtension(最初) ── %param% を設定全体で展開 -│ │ 動的なものは実行時の式へ -│ ├─ ExtensionsExtension(最初) ── さらなる拡張を登録 -│ ├─ ...ほかの拡張... ── loadConfiguration(): 定義の登録だけ -│ └─ ServicesExtension(最後) ── services: -> Definition オブジェクト -│ @name/@Type -> Reference -│ [グラフは数の上では完成; 型と引数はまだ; 型による検索は当てにならない] -│ -├─ PHASE B processBeforeCompile() -│ ├─ builder.resolve() ── すべての型を解決; オートワイヤリングの索引を作成 -│ │ [型が整った; 索引が整った] -│ ├─ beforeCompile() 拡張 ── ここでは getByType/findByType/findByTag が安全 -│ │ (引数はまだオートワイヤリングされていない) -│ └─ builder.complete() ── 引数をオートワイヤリング; 参照の変換を仕上げ -│ 型による参照 -> サービス名 -│ -└─ PHASE C generateCode() - ├─ PhpGenerator.generate() ── Statement -> PHP; createServiceXxx() メソッド - ├─ afterCompile() 拡張 ── コードを調整; パラメータのゲッターを出力 - └─ toString() ── 最終的な PHP コード -> キャッシュ - -──────────────────────────────────────────────────────────── - -実行時(リクエストごと) -│ -├─ new Container($dynamicParams) -├─ initialize() ── 拡張の起動コード(セッション、ヘッダー、検証) -└─ getService()/getByType() ── あらかじめ計算されたメタデータから遅延生成 -``` - - -よくある誤解 -====== - -- 「`loadConfiguration()` で型からサービスを引こう」。いけません。グラフは未完成で(ユーザーの `services:` はあなたのあとに走ります)、`getByType()` は部分的なグラフの早すぎる解決を引き起こします。`beforeCompile()` に移してください。`findByTag()` ならここでも問題ありません。 -- 「パラメータの `getenv()` の値は環境ごとに違うはずだ」。そのパラメータが動的な場合だけです。そうでなければコンパイル時に焼き込まれ、どこでも同じままです。 -- 「`@Type` の参照はもうサービス名だ」。違います。それは型による参照で、具体的な名前への解決は complete の手順でオートワイヤリングが行います。 -- 「拡張が補助ファイルを読んでいるのに、変更が反映されない」。`$builder->addDependency($file)` で登録してください。さもないとキャッシュはそれを知らず、作り直されません。 -- 「`resolve()` の最中に `getByType()` を呼べる」。いけません。`NotAllowedDuringResolvingException` を投げます。型による検索は `beforeCompile()` かそれ以降に属し、解決の最中には決して属しません。 diff --git a/dependency-injection/ja/configuration.texy b/dependency-injection/ja/configuration.texy deleted file mode 100644 index ad291dd4cb..0000000000 --- a/dependency-injection/ja/configuration.texy +++ /dev/null @@ -1,335 +0,0 @@ -DI コンテナの設定 -********** - -.[perex] -Nette DI コンテナの設定オプションの概要です。 - - -設定ファイル -====== - -Nette DI コンテナは設定ファイルで簡単に制御できます。設定ファイルはふつう [NEON 形式|neon:format]で書きます。この形式に[対応したエディタ |tools:ide]を使うことをおすすめします。 - -<pre> -"decorator .[prism-token prism-atrule]":[#Decorator]: "Decorator .[prism-token prism-comment]"<br> -"di .[prism-token prism-atrule]":[#DI]: "DI コンテナ .[prism-token prism-comment]"<br> -"extensions .[prism-token prism-atrule]":[#Extensions]: "追加の DI 拡張のインストール .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#ファイルの読み込み]: "ファイルの読み込み .[prism-token prism-comment]"<br> -"parameters .[prism-token prism-atrule]":[#パラメータ]: "パラメータ .[prism-token prism-comment]"<br> -"search .[prism-token prism-atrule]":[#Search]: "サービスの自動登録 .[prism-token prism-comment]"<br> -"services .[prism-token prism-atrule]":[services]: "サービス .[prism-token prism-comment]" -</pre> - -.[note] -`%` を含む文字列を書くには、`%%` のように二重にしてエスケープする必要があります。 - - -パラメータ -===== - -設定では、サービスの定義の中で使えるパラメータを定義できます。おかげで設定が分かりやすくなり、変わり得る値をひとつの場所にまとめられます。 - -```neon -parameters: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: secret -``` - -`dsn` パラメータは、設定のどこからでも `%dsn%` という書き方で参照します。パラメータは `'%wwwDir%/images'` のように文字列の中でも使えます。 - -パラメータは文字列や数値だけでなく、配列を含むこともできます。 - -```neon -parameters: - mailer: - host: smtp.example.com - secure: ssl - user: franta@gmail.com - languages: [cs, en, de] -``` - -特定のキーは `%mailer.user%` のように参照します。 - -コード(クラスなど)がパラメータの値を必要とするなら、その値をクラスに渡してください。たとえばコンストラクタで渡します。クラスがパラメータの値を問い合わせられるグローバルな設定オブジェクトはありません。それは依存性注入の原則に反するからです。 - - -サービス -==== - -[別の章|services]をご覧ください。 - - -Decorator -========= - -ある型の複数のサービスを一度に変更するにはどうすればよいでしょうか。たとえば、ある基底クラスを継承するすべてのプレゼンターで特定のメソッドを呼ぶには? そのための decorator です。 - -```neon -decorator: - # このクラスやインターフェースのインスタンスであるすべてのサービスに対して - App\Presentation\BasePresenter: - setup: - - setProjectId(10) # このメソッドを呼びます - - $absoluteUrls = true # そして変数を設定します -``` - -decorator は[タグ |services#タグ]の設定や [inject モード |services#Inject モード]の有効化にも使えます。 - -```neon -decorator: - InjectableInterface: - tags: [mytag: 1] - inject: true -``` - - -DI -=== - -DI コンテナの技術的な設定です。 - -```neon -di: - # DIC を Tracy バーに表示しますか? - debugger: ... # (bool) 既定は自動検出(Tracy があれば有効) - - # 決してオートワイヤリングしないパラメータの型 - excluded: ... # (string[]) - - # サービスの遅延生成を有効にしますか? - lazy: ... # (bool) 既定は false - - # DI コンテナが継承するクラス - parentClass: ... # (string) 既定は Nette\DI\Container -``` - - -遅延サービス .{data-version:3.2.4} ----------------------------- - -`lazy: true` を設定すると、サービスの遅延生成が有効になります。つまりサービスは DI コンテナに要求された時点では実際には作られず、最初に使われるときにはじめて作られます。そのリクエストで本当に必要なサービスだけが作られるので、アプリケーションの起動が速くなり、メモリの使用量も減らせます。 - -個々のサービスについて、遅延生成は[調整できます |services#遅延サービス]。 - -.[note] -遅延オブジェクトはユーザー定義のクラスにしか使えず、PHP の内部クラスには使えません。PHP 8.4 以上が必要です。 - - -メタデータの書き出し ----------- - -DI コンテナのクラスには多くのメタデータも含まれます。メタデータの書き出しを減らせば、その大きさを抑えられます。 - -```neon -di: - export: - # パラメータを書き出しますか? - parameters: false # (bool) 既定は true - - # タグを書き出しますか、そしてどれを? - tags: # (string[]|bool) 既定はすべて - - event.subscriber - - # オートワイヤリング用のデータを書き出しますか、そしてどれを? - types: # (string[]|bool) 既定はすべて - - Nette\Database\Connection - - Symfony\Component\Console\Application -``` - -`$container->getParameters()` を使わないなら、パラメータの書き出しを無効にできます。さらに、`$container->findByTag(...)` でサービスを取得するのに実際に使うタグだけを書き出せます。このメソッドをまったく呼ばないなら、`false` を使ってタグの書き出しを完全に無効にできます。 - -[オートワイヤリング|autowiring]のメタデータも、`$container->getByType()` で実際に要求するクラスだけを並べれば大きく減らせます。このメソッドを呼ばないなら(あるいは `Nette\Application\Application` を得るために [bootstrap|application:bootstrapping]ファイルでしか呼ばないなら)、`false` を使って型の書き出しを完全に無効にできます。 - - -Extensions -========== - -追加の DI 拡張の登録です。たとえば DI 拡張 `Dibi\Bridges\Nette\DibiExtension3` を `dibi` という名前で追加するにはこうします。 - -```neon -extensions: - dibi: Dibi\Bridges\Nette\DibiExtension3 -``` - -そして `dibi` セクションで設定します。 - -```neon -dibi: - host: localhost -``` - -パラメータ付きのクラスを拡張として追加することもできます。 - -```neon -extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, [%appDir%], %tempDir%/cache) -``` - - -ファイルの読み込み -========= - -追加の設定ファイルは `includes` セクションで読み込めます。 - -```neon -includes: - - parameters.php - - services.neon - - presenters.neon -``` - -`parameters.php` という名前は誤りではありません。設定は、それを配列として返す PHP ファイルで書くこともできます。 - -```php -<?php -return [ - 'database' => [ - 'main' => [ - 'dsn' => 'sqlite::memory:', - ], - ], -]; -``` - -複数の設定ファイルに同じキーの項目が現れた場合、それらは上書きされるか、配列の場合は[統合されます |#統合]。あとで読み込まれたファイルのほうが、前のものより優先度が高くなります。`includes` セクションを持つファイルは、そこで読み込まれるファイルより優先度が高くなります。 - - -Search -====== - -DI コンテナへのサービスの自動登録は、開発を大きく楽にします。Nette はプレゼンターを自動的にコンテナへ追加しますが、ほかのクラスも簡単に追加できます。 - -クラスをどのディレクトリ(とサブディレクトリ)で探すべきかを指定するだけです。 - -```neon -search: - - in: %appDir%/Forms - - in: %appDir%/Model -``` - -探索の規則がひとつだけなら、リストを省いてそのキーを `search` の直下に書けます。 - -```neon -search: - in: %appDir% -``` - -とはいえ、ふつうはすべてのクラスとインターフェースを追加したいわけではないので、絞り込めます。 - -```neon -search: - - in: %appDir%/Forms - - # ファイル名による絞り込み (string|string[]) - files: - - *Factory.php - - # クラス名による絞り込み (string|string[]) - classes: - - *Factory -``` - -あるいは、並べたクラスの少なくともひとつを継承または実装するクラスを選べます。 - - -```neon -search: - - in: %appDir% - extends: - - App\*Form - implements: - - App\*FormInterface -``` - -クラス名のマスクや祖先を使って除外の規則を定めることもできます。クラスが除外の規則に当てはまれば、DI コンテナには追加されません。 - -```neon -search: - - in: %appDir% - exclude: - files: ... - classes: ... - extends: ... - implements: ... -``` - -自動登録されるすべてのサービスにタグを割り当てられます。 - -```neon -search: - - in: %appDir% - tags: ... -``` - -クラスのほかに、探索は `create()` または `get()` メソッドをひとつだけ持つインターフェースも[生成されるファクトリやアクセサ |factory]として登録します。同じ型のサービスがすでにコンテナに登録されているクラスは飛ばされるので、重複はできません。 - - -統合 -=== - -複数の設定ファイルに同じキーの要素が現れた場合、それらは上書きされるか、配列の場合は統合されます。あとで読み込まれたファイルのほうが、前のものより優先度が高くなります。 - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>結果</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> - <td> -```neon -items: - - 1 - - 2 - - 3 -``` - </td> -</tr> -</table> - -配列については、キー名のあとに感嘆符を付けると統合を防げます。 - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>結果</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items!: - - 3 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> -</tr> -</table> - -{{maintitle: Dependency Injection の設定}} diff --git a/dependency-injection/ja/container.texy b/dependency-injection/ja/container.texy deleted file mode 100644 index 691ce75896..0000000000 --- a/dependency-injection/ja/container.texy +++ /dev/null @@ -1,142 +0,0 @@ -DI コンテナとは? -********** - -.[perex] -依存性注入コンテナ(DIC、または DI コンテナ)は、ほかのオブジェクト(サービスと呼ばれます)を生成して設定する役目を持つオブジェクトです。 - -意外に思うかもしれませんが、多くの場合、依存性注入(略して DI)の恩恵を受けるのに依存性注入コンテナは必要ありません。実際、[はじめの章|introduction]でも DI の具体的な例を示しましたが、コンテナは不要でした。 - -とはいえ、複雑な依存関係を持つ多数のオブジェクトを管理するようになると、DI コンテナはとても役に立ちます。フレームワークの上に作られたウェブアプリケーションでは、たいていそうなります。 - -前の章で `Article` と `EditController` のクラスを紹介しました。どちらも依存関係、すなわちデータベースとファクトリ `ArticleFactory` を持っています。これらのクラスのために、ここでコンテナを作ってみましょう。もちろん、これほど単純な例にコンテナを作るのはやりすぎです。それでも、どんな形でどう動くかを示すために作ります。 - -上の例のための、値を直接書いた単純なコンテナです。 - -```php -class Container -{ - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection('mysql:', 'root', '***'); - } - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->createDatabase()); - } - - public function createEditController(): EditController - { - return new EditController($this->createArticleFactory()); - } -} -``` - -使い方は次のようになります。 - -```php -$container = new Container; -$controller = $container->createEditController(); -``` - -オブジェクトをコンテナに要求するだけで、その作り方も依存関係も知る必要はありません。すべてコンテナが引き受けます。依存関係はコンテナが自動的に注入します。それがコンテナの強みです。 - -今のところ、コンテナはすべての情報を直接書き込んでいます。次の段階として、コンテナを本当に使えるものにするためにパラメータを足しましょう。 - -```php -class Container -{ - public function __construct( - private array $parameters, - ) { - } - - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection( - $this->parameters['db.dsn'], - $this->parameters['db.user'], - $this->parameters['db.password'], - ); - } - - // ... -} - -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); -``` - -目ざとい読者は問題に気づくかもしれません。`EditController` オブジェクトを取り出すたびに、`ArticleFactory` とデータベース接続の新しいインスタンスも作られてしまいます。これは望ましくありません。 - -そこで、常に同じインスタンスを返す `getService()` メソッドを足します。 - -```php -class Container -{ - private array $services = []; - - public function __construct( - private array $parameters, - ) { - } - - public function getService(string $name): object - { - if (!isset($this->services[$name])) { - // getService('Database') は createDatabase() を呼びます - $method = 'create' . $name; - $this->services[$name] = $this->$method(); - } - return $this->services[$name]; - } - - // ... -} -``` - -たとえば `$container->getService('Database')` の最初の呼び出しでは、`createDatabase()` を呼んでデータベースのオブジェクトを作り、`$services` 配列に保存して返します。以降の呼び出しでは、保存されたインスタンスをそのまま返します。 - -コンテナの残りの部分も `getService()` を使うように書き換えます。 - -```php -class Container -{ - // ... - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->getService('Database')); - } - - public function createEditController(): EditController - { - return new EditController($this->getService('ArticleFactory')); - } -} -``` - -ちなみに、サービスという言葉はコンテナが管理する任意のオブジェクトを指します。メソッド名が `getService()` なのはそのためです。 - -これで完成です。きちんと動く DI コンテナができました。さっそく使ってみましょう。 - -```php -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); - -$controller = $container->getService('EditController'); -$database = $container->getService('Database'); -``` - -ご覧のとおり、DIC を書くのは難しくありません。注目すべきは、オブジェクト自身はコンテナが自分を作っていることを知らない、という点です。ですから、ソースコードを変えずに任意の PHP オブジェクトをこの方法で作れます。 - -とはいえ、コンテナのクラスを手で作って保守するのは、すぐに悪夢になりかねません。そこで次の章では、ほぼ自動で自分自身を生成・更新できる [Nette DI Container|nette-container]を扱います。 - - -{{maintitle: 依存性注入コンテナとは?}} diff --git a/dependency-injection/ja/extensions.texy b/dependency-injection/ja/extensions.texy deleted file mode 100644 index 6b0233efd5..0000000000 --- a/dependency-injection/ja/extensions.texy +++ /dev/null @@ -1,443 +0,0 @@ -Nette DI の拡張の作成 -*************** - -.[perex] -拡張は、DI コンテナのコンパイルに割り込むクラスです。サービスをプログラムから登録し、自分の設定セクションを検証し、ほかが定義したサービスを変更し、さらには生成されるコンテナのコードまで変えられます。このページでは、その書き方、何がいつ起こるのか、そして何に気をつけるべきかを学びます。 - -拡張は、パッケージが Nette に本来の形で組み込まれるための手段です。すべての `nette/*` パッケージがそれを使っていますし、あなたのパッケージもそうできます。典型的な拡張は、次のうちひとつ以上を行います。 - -- **ライブラリを統合する** - そのサービスをコンテナに登録し、親しみやすく検証済みの設定セクションを公開します(`mail:` や `database:` のセクションはここから来ます) -- **登録を自動化する** - `services:` に並べるのが面倒になるほど似たサービスを、ループや規則にもとづいてまとめて登録します -- **横断的な変更を加える** - ほかが登録したサービスを見つけて補います。たとえば特定のタグを持つすべてのサービスにロガーを結びつけます - -日々のアプリケーション開発で拡張が必要になることはめったにありません。クラスの登録と結びつけは、設定の [services |services]セクションで足ります。設定だけでは足りなくなったときに拡張へ手を伸ばしてください。 - -拡張は `extensions` セクションで有効にします。`BlogExtension` クラスが表す拡張を `blog` という名前で追加するにはこうします。 - -```neon -extensions: - blog: BlogExtension -``` - -コンストラクタが引数を取るなら、その場で渡します。 - -```neon -extensions: - blog: BlogExtension(%debugMode%) -``` - - -コンパイルのしくみ -========= - -拡張を自信を持って書くには、ひとつ重要なことを知っておく必要があります。**あなたのコードがいつ走るのか**です。Nette はリクエストの処理中にサービスを結びつけたりしません。代わりにコンテナをあらかじめ*コンパイル*します。すべての設定ファイルを読み、拡張に仕事をさせ、最適化された PHP のクラスを生成してディスクに保存します。以降のリクエストは、この出来上がったクラスを読み込むだけです。ですから拡張のコードは、コンテナが(再)構築されるときにだけ走り、リクエストごとには走りません。 - -これには重要な帰結があります。コンパイル中はまだサービスが存在しません。存在するのは**定義**です。各サービスがどのクラスになるか、どう作るか、そのあと何を呼ぶかを記したレシピです。定義は [ContainerBuilder |#ContainerBuilder]オブジェクトの中にあります。拡張は本質的に*スクリプトで書ける設定*です。`services:` セクションで宣言できることは何でも、条件つきで、ループで、あるいはほかが登録したものに反応して、PHP でも組み立てられます。 - -コンパイルは段階を追って進み、拡張はそのそれぞれに入り込めます。 - -1) すべての拡張の設定セクションが検証されます(`getConfigSchema()`) -2) 各拡張が自分のサービスを登録します(`loadConfiguration()`)。ユーザーの `services:` セクションは最後に処理されるので、アプリケーションが常に最終決定権を持ちます -3) すべての定義がそろい、サービスの型が解決されると、拡張はそれらを変更できます(`beforeCompile()`) -4) コンテナのクラスが生成されます。拡張はまだそのコードを調整でき(`afterCompile()`)、アプリケーションの起動時に走るコードを出力できます([初期化 |#初期化のコード]) - -.[note] -開発モードでは、設定ファイルや拡張のクラス自体を変えるたびにコンテナが自動的に再コンパイルされます。どちらも依存関係として追跡されているからです。ですからキャッシュを消さずに拡張を開発できます。 - -.[tip] -各段階で何が起きるのか、パラメータがいつ展開されるのか、`@service` がいつ参照になるのか、そして型でサービスを探しても安全なのは正確にいつなのかを深く知りたい場合は、[コンテナのコンパイルの詳細 |compilation-internals]をご覧ください。 - - -最初の拡張 -===== - -小さいながら完全な拡張の例です。同じファイルで有効にし、設定します。 - -```neon -extensions: - blog: BlogExtension - -blog: - postsPerPage: 5 -``` - -そしてこれがクラスの全体です。 - -```php -use Nette\Schema\Expect; - -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function getConfigSchema(): Nette\Schema\Schema - { - return Expect::structure([ - 'postsPerPage' => Expect::int(10), - 'allowComments' => Expect::bool(true), - ]); - } - - - public function loadConfiguration(): void - { - $builder = $this->getContainerBuilder(); - - $builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class, ['postsPerPage' => $this->config->postsPerPage]); - - if ($this->config->allowComments) { - $builder->addDefinition($this->prefix('comments')) - ->setFactory(Blog\Comments::class); - } - } -} -``` - -`getConfigSchema()` は、`blog:` セクション(拡張を登録したキーにちなんだ名前です)に何を書けるかを、型と既定値も含めて記述します。検証された値は `$this->config` から使えます。`loadConfiguration()` ではサービスを登録します。名前に注目してください。`$this->prefix('articles')` は `blog.articles` を生むので、異なる拡張のサービスが衝突することはありません。 - -そして最後の数行が、そもそも拡張が存在する理由を示しています。`comments` サービスはコメントが有効なときにだけ登録されます。ただの設定ファイルでは、こうした判断はできません。 - -こうして登録されたサービスは、`services:` に書いた場合とまったく同じように振る舞います。必要になったときに遅延して作られ、`Blog\Articles` と型宣言されたところにはオートワイヤリングが渡します。 - -以降の章では、拡張のライフサイクルを詳しく説明し、次に拡張の中で使う [ContainerBuilder |#ContainerBuilder]の API を、最後に知っておく価値のある[落とし穴 |#ヒントと落とし穴]を扱います。 - - -拡張のライフサイクル -========== - -拡張は [api:Nette\DI\CompilerExtension]を継承し、`getConfigSchema()`、`loadConfiguration()`、`beforeCompile()`、`afterCompile()` の 4 つのメソッドのうちいくつかを上書きします。コンパイラはコンパイル中にこの順序でそれらを呼びます。 - - -getConfigSchema(): Nette\Schema\Schema .[method] ------------------------------------------------- - -拡張の設定セクションのスキーマを定義します。おかげで利用者は、検証と分かりやすいエラーメッセージをただで手に入れます。`blog:` セクションの打ち間違いや型の誤りは、あなたがチェックを 1 行も書かずに、理解しやすいメッセージで報告されます。 - -スキーマは [Schema |schema:]ライブラリで記述し、型、既定値、許される値などを表せます。 - -```php -public function getConfigSchema(): Nette\Schema\Schema -{ - return Expect::structure([ - 'postsPerPage' => Expect::int(10), - 'storage' => Expect::anyOf('files', 'database')->firstIsDefault(), - ]); -} -``` - -検証された設定は `stdClass` オブジェクトとして `$this->config` から使えます(スキーマに `castTo('array')` を足せば配列としても使えます)。 - -オプションの値がコンパイル時には分からない場合、たとえば環境変数から来る場合は、`dynamic()` で印を付けてください。たとえば `Expect::int()->dynamic()` です。詳しくは[動的パラメータ |application:bootstrapping#動的パラメータ]をご覧ください。 - - -loadConfiguration() .[method] ------------------------------ - -拡張が [ContainerBuilder |#ContainerBuilder]を使って自分のサービスを登録する場所です。 - -```php -public function loadConfiguration(): void -{ - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class); -} -``` - -サービスを短い名前でも使えるようにしたいなら、別名を足します。慣習として、これは拡張が通常の名前で登録されている場合にだけ行い、拡張の複数のインスタンスがその名前を奪い合わないようにします。 - -```php -if ($this->name === 'blog') { - $builder->addAlias('articles', $this->prefix('articles')); -} -``` - -サービスが多い場合は、見慣れた [services |services]の構文を使って別の NEON ファイルで定義するほうが便利かもしれません。`@extension` の接頭辞は現在の拡張を指します。 - -```neon -services: - articles: - create: MyBlog\ArticlesModel(@connection) - - comments: - create: MyBlog\CommentsModel(@connection, @extension.articles) -``` - -これらの定義は `loadDefinitionsFromConfig()` で読み込みます。名前には自動的に接頭辞が付き、ファイルは依存関係として追跡されるので、変更すれば再コンパイルが起こります。 - -```php -public function loadConfiguration(): void -{ - $this->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/services.neon')['services'], - ); -} -``` - - -beforeCompile() .[method] -------------------------- - -このメソッドが呼ばれる時点で、builder は**すべての**定義を持っています。あなたの定義、ほかの拡張の定義、そしてユーザーの設定ファイルの定義です。サービスの型も解決済みなので、型による検索が確実に働きます。この段階は、最終的なサービスのつながりを調べて補うのにうってつけです。 - -ふつうはタグや型でサービスを探し、見つけた定義を補います。 - -```php -public function beforeCompile(): void -{ - $builder = $this->getContainerBuilder(); - - foreach ($builder->findByTag('logaware') as $name => $attrs) { - $builder->getDefinition($name)->addSetup('setLogger'); - } -} -``` - -`setLogger()` の呼び出しには明示的な引数がありません。ファクトリの場合と同じく、オートワイヤリングが渡してくれます。 - -`$this->compiler->getExtensions()` で取得したほかの登録済みの拡張と協調することもできます。クラスやインターフェースで絞り込むこともできます。 - -```php -foreach ($this->compiler->getExtensions(FooExtension::class) as $extension) { - // ... -} -``` - - -afterCompile(Nette\PhpGenerator\ClassType $class) .[method] ------------------------------------------------------------ - -最後の段階で、コンテナのクラスが [ClassType |php-generator:#クラス]オブジェクト([PHP Generator |php-generator:]ライブラリのもの)として生成されます。そこにはサービスごとのファクトリメソッドが入っていて、これからキャッシュに書き込まれます。そのコードはまだ変更できます。 - -```php -public function afterCompile(Nette\PhpGenerator\ClassType $class): void -{ - $method = $class->getMethod('__construct'); - // ... -} -``` - -この段階が必要になることはめったにありません。アプリケーションの起動時に走るコードを足したいなら、代わりに初期化を使ってください。 - - -初期化のコード -------- - -ここまでの段階はすべて、コンテナがどう*構築される*かに影響します。それに加えて拡張は、コンテナが作られた直後、つまり*実行時*に走るコードも出力できます。たとえばセッションを始めたり、サービスを起動したりするためです。そのコードは `$this->initialization` オブジェクトの [addBody() |php-generator:#メソッドと関数の本体]メソッドで書き込みます。 - -```php -public function loadConfiguration(): void -{ - // 'run' タグを持つサービスは、コンテナの起動直後に作らなければなりません - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $attrs) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } -} -``` - -Nette 自身も、セッションの自動開始やセキュリティ用の HTTP ヘッダーの送信などに初期化を使っています。そして覚えておいてください。拡張のほかの部分と違い、このコードは**リクエストごとに**走るので、小さく保ちましょう。 - - -ContainerBuilder -================ - -[api:Nette\DI\ContainerBuilder]は、拡張がコンパイラと話すためのオブジェクトです。すべてのサービスの[定義 |#コンパイルのしくみ]を保持し、その追加、検索、変更のためのメソッドを提供します。`loadConfiguration()` と `beforeCompile()` で取得します。 - -```php -$builder = $this->getContainerBuilder(); -``` - - -サービスの追加 -------- - -サービスの登録は、NEON ファイルの `services:` セクションでやることと同じで、それを PHP で書くだけです。設定の各キーには対応するメソッドが定義側にあるので、次の 2 つの書き方は等価です。 - -```neon -services: - articles: - create: Blog\Articles(@connection) - setup: - - setLogger(@logger) - tags: [logaware] -``` - -```php -$builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class, ['@connection']) - ->addSetup('setLogger', ['@logger']) - ->addTag('logaware'); -``` - -`addDefinition()` が返す定義は [ServiceDefinition |#定義の種類]で、設定のキーに対応するものを提供します。`setType()`(サービスのクラス)、`setFactory()`(作り方)、`setArguments()`、`addSetup()`、`addTag()`、`setAutowired()` です。 - -`addSetup()` は `setup:` のリストに対応し、同じ形を受け付けます。メソッドの呼び出し `addSetup('setLogger', ['@logger'])`、プロパティへの代入 `addSetup('$cache', ['@cache'])`、ほかのサービスの呼び出し `addSetup('@Tracy\Bar::addPanel', [$panel])` です。 - -通常のサービスのほかに、builder は[生成される |factory]ファクトリ、アクセサ、ロケーターも登録できます。それぞれに対応する[定義の型 |#定義の種類]を返すメソッドがあります。 - -| メソッド | 登録するもの -|--------|---------- -| `addDefinition()` | 通常のサービス(`ServiceDefinition` を返します) -| `addFactoryDefinition()` | 生成される[ファクトリ |factory](`create()` メソッドを持つインターフェース) -| `addAccessorDefinition()` | 生成される[アクセサ |factory#アクセサ](`get()` メソッドを持つインターフェース) -| `addLocatorDefinition()` | 複数のファクトリをまとめた[マルチファクトリ/ロケーター |factory#マルチファクトリ/アクセサ] -| `addImportedDefinition()` | 実行時に外部からコンテナへ渡されるサービス -| `addAlias()` | 既存のサービスの別名 - -ファクトリでは、それが作るオブジェクトを `getResultDefinition()` で設定します。アクセサは代わりに `setReference()` で既存のサービスを指します。 - -```php -$builder->addFactoryDefinition($this->prefix('latteFactory')) - ->setImplement(LatteFactory::class) - ->getResultDefinition() - ->setFactory(Latte\Engine::class) - ->addSetup('setStrictTypes', [true]); -``` - -`addLocatorDefinition()` と `addImportedDefinition()` が必要になることはめったにありません。そうしたサービスはふつう、手で書くのではなく NEON の `implement:` キーや取り込みサービスのキーから来るからです。 - - -サービスの検索と変更 ----------- - -既存の定義を探したり辿ったりするために、builder は次のものを提供します。 - -| メソッド | 説明 -|--------|------------ -| `getDefinition(string $name)` | 指定した名前の定義(なければ例外を投げます) -| `hasDefinition(string $name)` | その名前の定義や別名が存在するか -| `getDefinitions()` | すべての定義 -| `removeDefinition(string $name)` | 定義を取り除きます -| `getByType(string $type)` | その型のオートワイヤリング対象のサービス名、またはなければ `null` -| `getDefinitionByType(string $type)` | その型のオートワイヤリング対象の定義 -| `findByType(string $type)` | その型のすべての定義を `名前 => 定義` の組で返します -| `findByTag(string $tag)` | そのタグを持つサービスを `名前 => タグの値` の組で返します -| `addExcludedClasses(array $types)` | クラスとインターフェースをオートワイヤリングから除外します - -便利な決まり文句が、`getByType()` でサービスがそもそも存在するかを調べることです。たとえばアプリケーションにロガーがあるときだけ、それに結びつける場合です。 - -```php -if ($builder->getByType(Psr\Log\LoggerInterface::class)) { - $builder->getDefinition($this->prefix('articles')) - ->addSetup('setLogger'); -} -``` - - -定義の種類 ------ - -`add*Definition()` の各メソッドは、それぞれ違う種類の定義を返します。どれも共通の祖先 `Nette\DI\Definitions\Definition` を継承しています。 - -- **`ServiceDefinition`** - 通常のサービス。`setType()`、`setFactory()`、`addSetup()`、`addTag()`、`setAutowired()` で設定します -- **`FactoryDefinition`** - [生成されるファクトリ |factory]。呼ぶたびに新しいオブジェクトを返す `create()` メソッドを持つインターフェースです -- **`AccessorDefinition`** - [生成されるアクセサ |factory#アクセサ]。既存のサービスを返す `get()` メソッドを持つインターフェースです -- **`LocatorDefinition`** - 複数のファクトリやアクセサをひとつのインターフェースにまとめた[マルチファクトリ/ロケーター |factory#マルチファクトリ/アクセサ] -- **`ImportedDefinition`** - コンテナが自分で作らず、実行時に外部から受け取るサービス - -`getDefinition()` は、その名前の下にある種類の定義をそのまま返すことを覚えておいてください。生成されるファクトリに出会う可能性があるコードでは、まず型を調べ、作られるオブジェクトを `getResultDefinition()` で設定してください。 - -```php -$def = $builder->getDefinition($name); -if ($def instanceof Nette\DI\Definitions\FactoryDefinition) { - $def = $def->getResultDefinition(); -} -$def->addSetup('setLogger'); -``` - - -ヒントと落とし穴 -======== - - -コンパイル時と実行時 ----------- - -最もよくある混乱のもとです。拡張のコードは、アプリケーションがリクエストを処理するときではなく、コンテナが**コンパイルされる**ときに走ります。実際上これは次のことを意味します。 - -- 拡張はサービスのインスタンスを扱いません。まだ存在しないからです。`new` でサービスを作らず、定義を登録してコンテナに作らせてください。 -- すべての設定値は生成されるコードに焼き込まれます。環境によって変わり得る値(パス、`getenv()` から得るパスワード)は[動的 |application:bootstrapping#動的パラメータ]と印を付けなければ、コンパイル時に固定されてしまいます。 -- `$this->initialization->addBody()` に渡す文字列は、今実行されるのではありません。コンテナに書き出される PHP のコードで、リクエストごとに実行されます。 - - -ファイルの依存関係 ---------- - -設定ファイルや拡張のクラスが変わると、コンテナは再コンパイルされます。しかし拡張がほかのファイル、たとえばエンティティの一覧やライブラリの XML 設定を読んでいる場合、コンテナはそれを知る術がありません。そうしたファイルは次のように登録してください。 - -```php -$builder->addDependency($file); -``` - -さもないと、典型的な謎に悩まされます。ファイルを編集したのにアプリケーションは古いままの振る舞いを続け、変更は何かほかの理由でコンテナが再構築されたときにようやく現れるのです。(`loadFromFile()` で読んだファイルは自動的に追跡されます。) - - -条件つきの登録 -------- - -拡張は環境に合わせて振る舞いを変えられます。任意の統合はふつう `class_exists()` で守ります。 - -```php -if (class_exists(Symfony\Component\Console\Command\Command::class)) { - $builder->addDefinition($this->prefix('command')) - ->setFactory(Blog\Console\SitemapCommand::class); -} -``` - -そして `%debugMode%` のような値は、拡張のコンストラクタで渡すのが最善です。 - -```neon -extensions: - blog: BlogExtension(%debugMode%) -``` - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function __construct( - private bool $debugMode = false, - ) {} -} -``` - -典型的な用途は、開発モードのときだけ Tracy のパネルを登録することです。 - - -複雑な引数 ------ - -ファクトリや setup の呼び出しの引数が、素の値、クラス名、`@service` の参照のいずれでもないことがあります。そうした場合のために次のものがあります。 - -- `new Nette\DI\Definitions\Statement(Blog\Panel::class, [$args])` - その場で作られるオブジェクト。引数として使う「無名のサービス」です -- `new Nette\DI\Definitions\Reference('blog.articles')` - サービスへの参照。`@name` という文字列のオブジェクト版です -- `$builder::literal('PHP_SAPI')` - 生成されるコンテナにそのまま挿入される生の PHP コード - -例として、Tracy のパネルを登録してみましょう。 - -```php -$builder->getDefinition($this->prefix('articles')) - ->addSetup('@Tracy\Bar::addPanel', [ - new Nette\DI\Definitions\Statement(Blog\ArticlesPanel::class), - ]); -``` - - -書き出されるタグと型 ----------- - -[メタデータの書き出し |configuration#メタデータの書き出し]は設定で制限でき、コンパイル済みのコンテナがアプリケーションの実際に使うタグとオートワイヤリングの型だけを保つようにできます。あなたの拡張が実行時に `$container->findByTag()` や `$container->getByType()` でサービスを取得しているなら、その制限が、まさに頼りにしているメタデータを取り除いてしまうかもしれません。 - -それを防ぐには、常に書き出されるべきタグと型をコンパイラに伝えます。 - -```php -public function loadConfiguration(): void -{ - // このタグは、書き出しが制限されていても常に書き出されます - $this->compiler->addExportedTag('event.subscriber'); - - // この型は常に getByType() で使えます - $this->compiler->addExportedType(Nette\Database\Connection::class); -} -``` - -どちらのメソッドも書き出されるメタデータに足すだけで、アプリケーションの `di › export` の設定を上書きすることはありません。ですからアプリケーションが書き出しを一覧に制限しても、拡張が必要とするタグと型は含まれたままです。タグの書き出しを完全に切った場合(`tags: false`)にだけ、ほかのすべてとともに捨てられます。 diff --git a/dependency-injection/ja/factory.texy b/dependency-injection/ja/factory.texy deleted file mode 100644 index 4dec2f51be..0000000000 --- a/dependency-injection/ja/factory.texy +++ /dev/null @@ -1,233 +0,0 @@ -生成されるファクトリ -********** - -.[perex] -Nette DI はインターフェースにもとづいてファクトリのコードを自動生成できるので、あなたがコードを書く手間が省けます。 - -ファクトリとは、オブジェクトを作り、その依存関係を渡す役目を持つクラスです。*factory method* デザインパターンと混同しないでください。あちらはファクトリの特定の使い方を述べたもので、この話題とは関係がありません。 - -そうしたファクトリがどんな形になるかは、[はじめの章 |introduction#ファクトリ]で示しました。 - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Nette DI はファクトリのコードを自動生成できます。あなたはインターフェースを作るだけで、実装は Nette DI が生成します。インターフェースには `create` という名前のメソッドがちょうどひとつあり、戻り値の型が宣言されている必要があります。 - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -つまりファクトリ `ArticleFactory` には、`Article` オブジェクトを作る `create` メソッドがあります。`Article` クラスはたとえば次のようなものです。 - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } -} -``` - -ファクトリを設定ファイルに足します。 - -```neon -services: - - ArticleFactory -``` - -Nette DI が対応するファクトリの実装を生成します。 - -ファクトリを使うコードでは、インターフェースでそのオブジェクトを要求すれば、Nette DI が生成された実装を渡してくれます。 - -```php -class UserController -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function foo() - { - // ファクトリにオブジェクトを作らせます - $article = $this->articleFactory->create(); - } -} -``` - - -パラメータ付きのファクトリ -============= - -ファクトリのメソッド `create` はパラメータを受け取り、それをコンストラクタに渡せます。たとえば `Article` クラスに記事の著者の ID を足してみましょう。 - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - private int $authorId, - ) { - } -} -``` - -ファクトリにもパラメータを足します。 - -```php -interface ArticleFactory -{ - function create(int $authorId): Article; -} -``` - -コンストラクタのパラメータ名(`$authorId`)とファクトリのメソッドのパラメータ名が一致しているので、Nette DI が自動的に渡します。 - - -高度な定義 -===== - -定義は `implement` キーを使って複数行の形でも書けます。 - -```neon -services: - articleFactory: - implement: ArticleFactory -``` - -この長い形を使うと、通常のサービスの定義と同じように、`arguments` キーでコンストラクタの追加の引数を指定したり、`setup` でさらに設定したりできます。 - -例: `create()` メソッドが `$authorId` パラメータを受け取らない場合、`Article` のコンストラクタに渡す固定の値を設定で与えられます。 - -```neon -services: - articleFactory: - implement: ArticleFactory - arguments: - authorId: 123 -``` - -逆に `create()` が `$authorId` を受け取るものの、それがコンストラクタの一部ではなく `Article::setAuthorId()` のようなメソッドで渡される場合は、`setup` セクションでそのパラメータを参照します。 - -```neon -services: - articleFactory: - implement: ArticleFactory - setup: - - setAuthorId($authorId) -``` - - -アクセサ -==== - -ファクトリのほかに、Nette はいわゆるアクセサも生成できます。これは `get()` メソッドを持つオブジェクトで、DI コンテナから特定のサービスを返します。`get()` を繰り返し呼んでも常に同じインスタンスが返ります。 - -アクセサは依存関係の遅延読み込みを可能にします。エラーを専用のデータベースに記録するクラスを考えてみてください。このクラスがコンストラクタでデータベース接続を注入されるなら、エラーがめったに起きず接続がほとんど使われない場合でも、接続は常に確立されてしまいます。代わりにアクセサを受け取れば、データベースのオブジェクト(接続)はアクセサの `get()` メソッドが最初に呼ばれたときにはじめて作られます。 - -アクセサはどう作るのでしょうか。インターフェースを書くだけで、実装は Nette DI が生成します。インターフェースには `get` という名前のメソッドがちょうどひとつあり、パラメータを取らず、戻り値の型が宣言されている必要があります。 - -```php -interface PDOAccessor -{ - function get(): PDO; -} -``` - -アクセサを、それが返すべきサービスの定義とともに設定ファイルに足します。 - -```neon -services: - - PDOAccessor - - PDO(%dsn%, %user%, %password%) -``` - -アクセサは `PDO` サービスを返し、設定にはそのサービスがひとつしか定義されていないので、アクセサはそのサービスを返します。その型のサービスが複数ある場合は、アクセサがどれを返すべきかを名前で指定してください。たとえば `- PDOAccessor(@db1)` です。 - - -マルチファクトリ/アクセサ -============= - -ここまでのファクトリとアクセサは、1 種類のオブジェクトしか作れず、また返せませんでした。しかしファクトリとアクセサの機能を組み合わせたマルチファクトリも簡単に作れます。そうした部品のインターフェースには、`create<Name>()` と `get<Name>()` という名前のメソッドを複数書けます。たとえば次のようにです。 - -```php -interface MultiFactory -{ - function createArticle(): Article; - function getDb(): PDO; -} -``` - -つまり個々のファクトリやアクセサをいくつも注入する代わりに、ひとつのより包括的な部品を注入できます。 - -複数のメソッドの代わりに、パラメータ付きの `get()` を使うこともできます。 - -```php -interface MultiFactoryAlt -{ - function get($name): PDO; -} -``` - -このとき `MultiFactory::getDb()` は `MultiFactoryAlt::get('db')` と同じことをします。ただしこの書き方には、`$name` に渡せる値がインターフェースのシグネチャから明らかでないという欠点があります。さらに、`$name` の値ごとに異なる戻り値の型をインターフェースで定義することもできません。 - -`get($name)` の代わりに、インターフェースは `create($name)` を宣言することもでき、こちらは呼ぶたびに新しいインスタンスを返します(`get()` は共有されたものを返します)。インターフェースに書けるパラメータ付きのメソッドはひとつだけです。メソッドの戻り値の型が nullable(たとえば `?PDO`)なら、未知の `$name` に対して例外を投げる代わりに `null` を返します。 - - -リストによる定義 --------- -設定では、サービスをその場に書くリストの形でマルチファクトリを定義できます。 .{data-version:3.2.0} - -```neon -services: - - MultiFactory( - article: Article() # createArticle() を定義します - db: PDO(%dsn%, %user%, %password%) # getDb() を定義します - ) -``` - -あるいは、マルチファクトリの定義の中で参照を使って既存のサービスを指すこともできます。 - -```neon -services: - article: Article - - PDO(%dsn%, %user%, %password%) - - MultiFactory( - article: @article # createArticle() を定義します - db: @\PDO # getDb() を定義します - ) -``` - - -タグによる定義 -------- - -マルチファクトリを定義するもうひとつの方法は[タグ |services#タグ]を使うことです。タグの値が対応するメソッドの名前を決めます。 - -```neon -services: - article: - create: Article - tags: {multi: article} # createArticle() を定義します - db: - create: PDO(%dsn%, %user%, %password%) - tags: {multi: db} # getDb() を定義します - - - MultiFactory(tagged: multi) -``` diff --git a/dependency-injection/ja/faq.texy b/dependency-injection/ja/faq.texy deleted file mode 100644 index c67766703e..0000000000 --- a/dependency-injection/ja/faq.texy +++ /dev/null @@ -1,123 +0,0 @@ -DI のよくある質問(FAQ) -*************** - - -DI は IoC の別名ですか? ----------------- - -*制御の反転*(Inversion of Control、IoC)は、プログラムの中で制御がどう流れるかを述べる原則です。あなたのコードが外部のコードを呼ぶのか、それとも(フレームワークのような)外部のコードがあなたのコードを呼ぶのか、という話です。IoC は幅広い概念で、[イベント |nette:glossary#イベント]、いわゆる[ハリウッドの原則 |application:components#ハリウッド流]などを含みます。[原則 3: ファクトリに任せる |introduction#原則 3: ファクトリに任せる]で扱うファクトリもこの概念に含まれ、`new` 演算子の反転にあたります。 - -*依存性注入*(Dependency Injection、DI)は、オブジェクトがどうやって依存関係(つまり一緒に働く必要のあるほかのオブジェクト)を手に入れるかに焦点を当てます。オブジェクト自身に作らせたり探させたりするのではなく、依存関係を明示的に渡すことを勧めるデザインパターンです。 - -ですから DI は IoC の特定の形と見なせます。ただし IoC のすべての形がきれいなコードを促すわけではありません。たとえば[グローバル状態|global-state]や [Service Locator |#Service Locator とは何ですか?]パターンに頼る手法は、アンチパターンです。 - - -Service Locator とは何ですか? ------------------------ - -依存性注入に代わる方法です。中心となるオブジェクト(ロケーター)があり、利用できるすべてのサービス(依存関係)がそこに登録されます。オブジェクトが依存関係を必要とすると、Service Locator に要求します。 - -しかし DI と比べると透明性に欠けます。依存関係が API(コンストラクタやメソッド)に現れず、オブジェクトのコードの中(ロケーターの呼び出し)に隠れてしまうので、つながりを知るにはコードを読まなければなりません。テストも複雑になります。オブジェクトを作るときに単にモックの依存関係を渡すことができず、Service Locator 自体を操作する必要がしばしば出てくるからです。さらに Service Locator は不要な依存関係を持ち込みます。DI ではオブジェクトが理想的にはコンテナの存在を知らずに済むのに対し、こちらではオブジェクトがロケーターと結びついてしまいます。 - - -DI を使わないほうがよいのはどんなときですか? ------------------------- - -依存性注入のデザインパターンを正しく使うことに、知られた大きな欠点はありません。むしろ、グローバルにアクセスできる場所(静的プロパティやシングルトンなど)から依存関係を得ることは[数々の面倒|global-state]につながりますし、Service Locator を使うのも同様です。ですから DI を使うことは一般に常におすすめできます。これは教条ではありません。単に、依存関係をきれいに管理するこれより良い代案が広く受け入れられていない、というだけのことです。 - -とはいえ、オブジェクトにグローバルにアクセスしてもよい、限られた特定の場面はあります。たとえばデバッグ中に、変数の値をダンプしたり、実行時間を測ったり、ある地点でメッセージを記録したりする場合です。あとでコードから取り除く一時的な処置であれば、グローバルにアクセスできるダンパー、タイマー、ロガーを使うのは正当です。これらの道具はアプリケーションの設計の中核ではありません。 - - -DI を使うことに欠点はありますか? ------------------- - -依存性注入を使うと、コードを書く手間が増えたり性能が落ちたりといった不利益はあるのでしょうか。DI に沿ってコードを書き始めると、私たちは何を失うのでしょうか。 - -DI 自体が実行時の性能やメモリ使用量に与える影響はごくわずかです。DI コンテナの性能は要因になり得ますが、[Nette DI |nette-container]はコンテナを素の PHP コードにコンパイルするので、アプリケーションの実行中のオーバーヘッドは事実上ゼロです。 - -DI の原則に従ってコードを書くと、依存関係を受け取るコンストラクタを作る必要が出てきます。かつては面倒に思えたかもしれませんが、現代の IDE や PHP 8 の[コンストラクタのプロパティ昇格 |https://blog.nette.org/en/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]といった機能のおかげで、とても速く書けます。ファクトリは Nette DI がしばしば自動生成してくれるので、定型コードはさらに減ります。その一方で、シングルトンや静的なアクセサを書く必要はなくなります。 - -全体として、DI を使ったよく設計されたアプリケーションは、シングルトンやグローバルアクセスに頼るものと比べて、目立って短くも長くもなりません。依存関係の生成と結びつけに関わるコードが、個々のクラスから専用の場所、つまり DI コンテナの設定とファクトリに移るだけです。 - - -古いアプリケーションを DI に書き換えるには? ------------------------- - -古いアプリケーションから依存性注入への移行は、とくに大きく複雑なアプリケーションでは骨の折れる作業になり得ます。この過程には体系的に取り組むことが大切です。 - -- 依存性注入に移るときは、使う原則と作法をチームの全員が理解していることが大切です。 -- まず既存のアプリケーションを分析し、主要な部品とその依存関係を洗い出します。どの部分をどの順序で書き換えるかの計画を立てます。 -- DI コンテナを実装するか、できれば Nette DI のような既存のライブラリを使いましょう。 -- アプリケーションの各部分を、依存性注入を使うよう少しずつ書き換えます。コンストラクタやメソッドを、依存関係をパラメータとして受け取る形に変えることになるでしょう。 -- オブジェクトを生成しているコードを、コンテナから取得するか、コンテナが提供するファクトリを使うよう更新します。 - -依存性注入への移行は、コードの品質とアプリケーションの長期的な保守性への投資であることを覚えておいてください。これらの変更は大変かもしれませんが、その結果はよりきれいで、モジュール化され、テストしやすく、将来の拡張と保守に備えたコードになるはずです。 - - -なぜ継承よりコンポジションが好まれるのですか? ------------------------ -コードの再利用には、[継承 |nette:introduction-to-object-oriented-programming#合成]より[コンポジション |nette:introduction-to-object-oriented-programming#継承]が一般に好まれます。結びつきがより緩やかになるからです。コンポジションなら、基底クラスの変更が依存するサブクラスを壊す問題に出会いにくくなります。典型的な例が、[コンストラクタ地獄 |passing-dependencies#コンストラクタ地獄]と呼ばれる状況です。 - - -Nette DI Container は Nette の外でも使えますか? -------------------------------------- - -もちろんです。Nette DI Container は Nette の一部ですが、フレームワークのほかの部分と切り離して使える独立したライブラリとして設計されています。Composer でインストールし、サービスを定義する設定ファイルを作り、数行の PHP コードで DI コンテナを作るだけです。すぐにあなたのプロジェクトで依存性注入の恩恵を受け始められます。 - -[Nette DI Container |nette-container]の章で、コードの例とともに具体的な使い方を説明しています。 - - -なぜ設定は NEON ファイルなのですか? ---------------------- - -NEON は、アプリケーション、サービス、その依存関係を設定するために Nette の中で生まれた、単純で読みやすい設定言語です。JSON や YAML と比べて、この目的にははるかに直感的で柔軟な選択肢を提供します。NEON なら、JSON や YAML では同じくらい明快に表すのが難しい、あるいは不可能なサービスの定義や関係を、自然に書けます。 - - -NEON ファイルの解析はアプリケーションを遅くしませんか? ------------------------------- - -NEON ファイルの解析はとても速いのですが、その速さは本番ではほとんど関係ありません。設定ファイルが解析されるのは、アプリケーションが最初に動くとき(または設定が変わったとき)の一度きりだからです。解析後は DI コンテナのコードが生成されてキャッシュされ(ディスクに保存され)、以降のリクエストではこのコンパイル済みの PHP コードが実行されるので、それ以上の解析は不要です。 - -これが本番環境での動きです。開発中は、内容が変わるたびに NEON ファイルが解析されるので、開発者は常に最新の DI コンテナを手にできます。すでに述べたとおり、解析そのものはとても高速です。 - - -設定ファイルのパラメータにクラスからアクセスするには? ---------------------------- - -[原則 1: 渡してもらう |introduction#原則 1: 渡してもらう]を思い出してください。クラスが設定ファイルの情報を必要とするなら、そのクラスがどうやってそれを*取ってくる*かを考えてはいけません。代わりに、たとえばクラスのコンストラクタでそれを要求するだけです。そして設定ファイルでその値を与えます。 - -次の例では、`%myParameter%` が `myParameter` パラメータの値のプレースホルダーで、`MyClass` のコンストラクタに渡されます。 - -```neon -# config.neon -parameters: - myParameter: Some value - -services: - - MyClass(%myParameter%) -``` - -複数のパラメータを渡したい場合やオートワイヤリングを使いたい場合は、[パラメータをオブジェクトにまとめる |best-practices:passing-settings-to-presenters]と便利です。 - - -Nette は PSR-11 Container インターフェースに対応していますか? -------------------------------------------- - -[Nette DI Container |api:Nette\DI\Container]は PSR-11 に直接は対応していません。ただし Nette DI Container と、PSR-11 の Container インターフェースを期待するライブラリやフレームワークとの相互運用が必要なら、Nette DI Container と PSR-11 の橋渡しをする[簡単なアダプタ |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f]を作れます。 - - -container、compiler、definition などの用語は何を意味しますか? ---------------------------------------------- - -Nette DI のまわりで繰り返し登場する言葉、そのほとんどは[拡張を書くとき |extensions]に出てくるものですが、その短い用語集です。 - -- **Container** - サービスを必要に応じて作り、実行時に保持するコンパイル済みのオブジェクト(`Nette\DI\Container`)。最適化された PHP コードとして一度だけ生成されます。 -- **Compiler** - 設定ファイルと拡張を、そのコンテナのクラスに変える仕組み。 -- **ContainerBuilder** - コンパイル中に使われる、変更可能なコンテナのモデル。実際のサービスがまだ存在しない段階でサービスの定義を保持します。[拡張の作成 |extensions#ContainerBuilder]をご覧ください。 -- **Service** - コンテナが管理するオブジェクト。ふつう一度だけ作られて共有されます(シングルトン)。データベース接続、メーラー、ロガーなどです。 -- **Definition** - サービスのレシピ。その型、作り方、作ったあとに何をするか。Nette は定義をコンテナのファクトリメソッドに変えます。いくつかの種類があります([定義の種類 |extensions#定義の種類]をご覧ください)。 -- **Type** - サービスのクラスやインターフェース。オートワイヤリングが、それを必要とする場所にサービスを結びつけるために使います。 -- **Autowiring** - 型にもとづいてサービスをコンストラクタやメソッドへ自動的に渡すこと。依存関係を手で結びつけずに済みます。 -- **Tag** - 定義に付けるラベル(値を伴うこともあります)。拡張は `findByTag()` で、それを持つすべてのサービスを見つけられます。 -- **Setup** - サービスが作られた直後に行われる追加の呼び出し。メソッドの呼び出しやプロパティへの代入で、`addSetup()` で足します。 -- **Alias** - 既存のサービスの別名。 diff --git a/dependency-injection/ja/global-state.texy b/dependency-injection/ja/global-state.texy deleted file mode 100644 index 80decd04ad..0000000000 --- a/dependency-injection/ja/global-state.texy +++ /dev/null @@ -1,294 +0,0 @@ -グローバル状態とシングルトン -************** - -.[perex] -注意: 次のような書き方は、設計のよくないコードの症状です。 - -- `Foo::getInstance()` -- `DB::insert(...)` -- `Article::setDb($db)` -- `ClassName::$var` または `static::$var` - -あなたのコードにこうした書き方はありますか。もしあるなら、改善の余地があります。よくある書き方で、さまざまなライブラリやフレームワークの解決例でも見かける、と思うかもしれません。もしそうなら、それらのコードの設計には欠陥があります。 - -ここで学問的な純粋さの話をしているのではありません。これらの書き方にはひとつの共通点があります。グローバル状態を使っていることです。そしてグローバル状態はコードの品質に有害な影響を与えます。クラスは自分の依存関係について嘘をつくようになります。コードは予測できなくなります。開発者を混乱させ、その効率を下げます。 - -この章では、なぜそうなるのか、そしてどうやってグローバル状態を避けるのかを説明します。 - - -グローバルな絡み合い ----------- - -理想の世界では、オブジェクトは[直接渡された |passing-dependencies]オブジェクトとだけやり取りするはずです。オブジェクト `A` と `B` を作り、両者のあいだで参照を一度も渡さなければ、`A` も `B` も互いの状態にアクセスしたり変えたりできません。これはコードにとって非常に望ましい性質です。電池と電球のようなもので、電球は導線で電池につながないと光りません。 - -しかしグローバル(静的)変数やシングルトンでは、そうはいきません。オブジェクト `A` は参照を渡されなくても、`C::changeSomething()` を呼ぶことで*無線で*オブジェクト `C` にアクセスして変更できます。オブジェクト `B` もグローバルな `C` に手を伸ばせば、`A` と `B` は `C` を通じて互いに影響し合えてしまいます。 - -グローバル変数を使うと、外からは見えない*無線の*結合という新しい形が持ち込まれます。それは煙幕を作り、コードの理解と利用を難しくします。依存関係を本当に把握するには、開発者はクラスのインターフェースを頼りにするのではなく、ソースコードの一行一行を読まなければなりません。しかもこの結合はまったく不要なものです。グローバル状態が使われるのは、どこからでも簡単にアクセスでき、たとえばグローバル(静的)メソッド `DB::insert()` でデータベースに書き込めるからです。しかしこれから示すとおり、その手軽さは、それがもたらす深刻な面倒に比べればごくわずかなものです。 - -.[note] -振る舞いという点で、グローバル変数と静的変数に違いはありません。どちらも同じくらい有害です。 - - -遠隔からの不気味な作用 ------------ - -「遠隔からの不気味な作用」。アルベルト・アインシュタインが、量子物理学のある現象を指してそう呼んだのは有名です。彼はそれに不気味さを感じていました。 -それは量子もつれのことで、一方の粒子の性質を測ると、たとえ何百万光年離れていても、もつれたもう一方の粒子に瞬時に影響します。光より速く伝わるものはないという宇宙の基本法則に、一見反しているように見えます。 - -ソフトウェアの世界では、「遠隔からの不気味な作用」は次のような状況を指します。(依存関係を明示的に渡していないので)独立していると思っていた処理を実行したのに、システムの遠い場所で思いがけないやり取りと状態の変化が、こちらの知らないうちに起きるのです。これはグローバル状態を通じてしか起こりません。 - -大きく成熟したコードベースを持つプロジェクトの開発チームに加わったところを想像してください。新しいリーダーから機能をひとつ実装するよう頼まれ、良い開発者らしく、まずテストを書き始めます。ただしプロジェクトに慣れていないので、「このメソッドを呼んだら何が起こるのか」といった探りのテストをたくさん書きます。そして次のようなテストを書いてみます。 - -```php -function testCreditCardCharge() -{ - $cc = new CreditCard('1234567890123456', 5, 2028); // あなたのカード番号 - $cc->charge(100); -} -``` - -コードを何度か実行し、しばらくすると携帯に銀行の通知が届きます。実行のたびにクレジットカードから 100 ドルが引き落とされていたのです。🤦‍♂️ - -いったいどうしてテストが実際の請求を引き起こせるのでしょうか。クレジットカードを扱うのは単純ではありません。第三者のウェブサービスとやり取りし、その URL を知り、認証する必要があります。そうした情報はテストのどこにも書かれていません。さらに悪いことに、その情報がどこにあるかも分からないので、外部の依存関係をモックに置き換えて、テストのたびに 100 ドル取られるのを防ぐこともできません。新入りの開発者に、これからやろうとしていることが 100 ドルの損失につながると、どうやって分かれというのでしょうか。 - -これが遠隔からの不気味な作用です。 - -あなたは大量のソースコードをふるいにかけ、先輩に相談してプロジェクトの絡み合いを理解するはめになります。この困難が生じるのは、`CreditCard` クラスのインターフェースが、必要なグローバル状態の初期化を明かさないからです。クラスのソースコードを見ても、どの初期化メソッドを呼ぶべきか分からないかもしれません。よくても、アクセスされているグローバル変数を見つけて、その初期化の仕方を推し量ろうとするくらいです。 - -こうしたプロジェクトのクラスは、病的な嘘つきです。`CreditCard` クラスは、ただインスタンス化して `charge()` メソッドを呼べばよいかのように振る舞います。しかし裏では、決済ゲートウェイを表す別のクラス `PaymentGateway` とやり取りしています。`PaymentGateway` のインターフェースも独立して初期化できるように見えるかもしれませんが、実際には設定ファイルから認証情報を引っ張ってきていたりします。もとの開発者たちは `CreditCard` が `PaymentGateway` を必要とすることを分かっています。そういうコードを書いたのですから。しかし新しく来た人にとっては完全な謎で、学ぶことにも貢献することにも立ちはだかります。 - -この状況をどう直せばよいでしょうか。簡単です。**API に依存関係を宣言させるのです。** - -```php -function testCreditCardCharge() -{ - $gateway = new PaymentGateway(/* ... */); - $cc = new CreditCard('1234567890123456', 5, 2028); - $cc->charge($gateway, 100); -} -``` - -コードの中の相互依存が一目で分かるようになったことに注目してください。`charge()` メソッドが `PaymentGateway` を必要とすると宣言しているので、この依存関係を推し量ったり人に聞いたりする必要はもうありません。インスタンスを作る必要があると分かり、その過程で必要なアクセス情報にも行き当たります。それがなければコードはそもそも動きません。 - -そして何より、決済ゲートウェイをモックにできるので、テストを走らせるたびに 100 ドル取られることもなくなります。 - -グローバル状態は、API に宣言されていない依存関係にオブジェクトがこっそりアクセスすることを許し、あなたの API を病的な嘘つきに変えてしまいます。 - -これまでそう考えたことはなかったかもしれませんが、グローバル状態を使うたびに、あなたは秘密の無線通信路を作っているのです。この遠隔からの不気味な作用は、起こり得るやり取りを理解するために開発者にコードの一行一行を読むことを強い、生産性を下げ、新しいチームメンバーを混乱させます。そのコードを書いたあなたは本当の依存関係を知っていますが、あとから来る人には何も分かりません。 - -グローバル状態に頼るコードを書くのはやめ、依存関係を明示的に渡すようにしましょう。依存性注入を受け入れてください。 - - -グローバル状態のもろさ ------------ - -グローバル状態やシングルトンを使ったコードでは、その状態がいつ誰によって変えられたのか、決して確信が持てません。この危険は初期化のときにも現れます。次のコードはデータベース接続を作って決済ゲートウェイを初期化しようとしていますが、何度も例外を投げ、その原因を追うのはきわめて骨が折れます。 - -```php -PaymentGateway::init(); -DB::init('mysql:', 'user', 'password'); -``` - -コードを丹念に追ってようやく、`PaymentGateway` オブジェクトが無線でほかのオブジェクトにアクセスしていて、その一部がデータベース接続を必要とすることが分かります。ですからデータベースは `PaymentGateway` より先に初期化しなければなりません。しかしグローバル状態の煙幕がそれを隠します。これらのクラスの API が正直で依存関係を宣言していたら、どれだけの時間が節約できたでしょうか。 - -```php -$db = new DB('mysql:', 'user', 'password'); -$gateway = new PaymentGateway($db, /* ... */); -``` - -データベース接続にグローバルにアクセスする場合も、同じような問題が起こります。 - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public function save(): void - { - DB::insert(/* ... */); - } -} -``` - -`save()` メソッドを呼ぶとき、データベース接続が確立されているのか、誰がそれを確立する責任を持つのかは分かりません。(テストなどのために)データベース接続を動的に変える必要が出てきたら、`DB::reconnect(...)` や `DB::reconnectForTest()` のようなメソッドを足すことになるでしょう。 - -例を見てみましょう。 - -```php -$article = new Article; -// ... -DB::reconnectForTest(); -Foo::doSomething(); -$article->save(); -``` - -`$article->save()` が呼ばれたとき、本当にテスト用のデータベースが使われていると、どうして確信できるでしょうか。`Foo::doSomething()` メソッドがグローバルなデータベース接続を変えていたら? それを確かめるには `Foo` の、そしておそらくほかの多くのクラスのソースコードを調べる必要があります。しかもその調査は一時的な答えしかもたらしません。状況はあとで変わり得るからです。 - -データベース接続を `Article` クラスの中の静的変数に移したらどうでしょうか。 - -```php -class Article -{ - private static DB $db; - - public static function setDb(DB $db): void - { - self::$db = $db; - } - - public function save(): void - { - self::$db->insert(/* ... */); - } -} -``` - -何ひとつ変わりません。問題はグローバル状態そのものであって、それがどのクラスの中に隠れているかは関係ありません。この場合も先ほどと同じく、`$article->save()` が呼ばれたとき、データがどのデータベースに書き込まれるのか確信が持てません。アプリケーションのどこにいる誰でも、`Article::setDb()` を使っていつでもデータベースを変えられたのです。私たちの知らないうちに。 - -グローバル状態は、アプリケーションを**きわめてもろく**します。 - -しかしこの問題に対処する簡単な方法があります。正しく機能するために必要なものを、API に依存関係として宣言させればよいのです。 - -```php -class Article -{ - public function __construct( - private DB $db, - ) { - } - - public function save(): void - { - $this->db->insert(/* ... */); - } -} - -$article = new Article($db); -// ... -Foo::doSomething(); -$article->save(); -``` - -この方法なら、データベース接続が隠れて思いがけず変わる心配がなくなります。記事がどこに保存されるのかを確信でき、無関係なクラスでの変更がそれに影響することもありません。コードはもろさを失い、安定します。 - -グローバル状態に頼るコードを書くのはやめ、依存関係を明示的に渡すようにしましょう。依存性注入を受け入れてください。 - - -シングルトン ------- - -シングルトンはデザインパターンのひとつで、有名な Gang of Four の本による[定義 |https://ja.wikipedia.org/wiki/Singleton_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3]によれば、クラスのインスタンスをひとつに制限し、それへのグローバルなアクセスを提供します。このパターンの実装は、ふつう次のようなコードになります。 - -```php -class Singleton -{ - private static self $instance; - - public static function getInstance(): self - { - self::$instance ??= new self; - return self::$instance; - } - - // そしてクラスの機能を果たすほかのメソッド -} -``` - -残念ながら、シングルトンはアプリケーションにグローバル状態を持ち込みます。そして上で示したとおり、グローバル状態は望ましくありません。だからこそシングルトンはアンチパターンと見なされます。 - -コードでシングルトンを使わず、ほかのしくみで置き換えてください。本当にシングルトンは必要ありません。ただし、アプリケーション全体でクラスのインスタンスがひとつだけであることを保証したいなら、その責任は [DI コンテナ |container]に委ねましょう。こうしてアプリケーションのスコープを持つシングルトン、一般にサービスと呼ばれるものができます。クラス自身は自分の唯一性を管理する務めから解放され(つまり `getInstance()` メソッドも静的なインスタンスのプロパティも持たなくなり)、自分の責務だけに集中できます。こうして単一責任の原則を破らずに済むようになります。 - - -グローバル状態とテスト ------------ - -テストを書くとき、私たちは理想的には、各テストが独立した単位であり、外部の状態が入ることも出ることもないと想定します。テストが終われば、それに関わる状態はガベージコレクタが自動的に片づけるはずです。これがテストを独立させます。ですからテストはどんな順序でも実行できます。 - -しかしグローバル状態やシングルトンがあると、この都合のよい前提は崩れます。状態はテストに漏れ込み、また漏れ出します。とたんに、テストの順序が問題になり得ます。 - -シングルトンを含むコードをテストできるようにするためだけに、開発者はしばしばその健全さを妥協せざるを得ません。たとえばシングルトンのインスタンスを差し替えられるようにする、といった具合です。そうした解はよくても場当たりで、保守と理解が難しいコードにつながります。グローバル状態を変えるテスト(やその `tearDown()` メソッド)は、その変更を丹念に戻さなければなりません。 - -グローバル状態は、ユニットテストにおける最大の頭痛の種です。 - -どう直せばよいでしょうか。簡単です。シングルトンを使うコードを書くのはやめ、依存関係を明示的に渡すようにしましょう。依存性注入を受け入れてください。 - - -グローバル定数 -------- - -グローバル状態はシングルトンや静的変数の利用に限りません。グローバル定数にも当てはまります。 - -普遍的な事実を表す値(`M_PI`)や、それ自体で完結した情報を与える定数(`PREG_BACKTRACK_LIMIT_ERROR`)は、一般に問題ありません。逆に、情報をコードに*無線で*注入する手段として使われる定数は、実質的に隠れた依存関係です。次の例の `LOG_FILE` がそうです。`FILE_APPEND` 定数の使い方はまったく正しいものです。 - -```php -const LOG_FILE = '...'; - -class Foo -{ - public function doSomething() - { - // ... - file_put_contents(LOG_FILE, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -代わりに、ログファイルのパスを `Foo` クラスのコンストラクタのパラメータとして宣言し、その API の明示的な一部にすべきです。 - -```php -class Foo -{ - public function __construct( - private string $logFile, - ) { - } - - public function doSomething() - { - // ... - file_put_contents($this->logFile, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -これでログファイルへのパスを明示的に渡すようになりました。必要に応じて簡単に変えられ、テストもコードの保守も楽になります。 - - -グローバル関数と静的メソッド --------------- - -強調しておきたいのは、静的メソッドやグローバル関数を使うこと自体は本質的に問題ではない、という点です。`DB::insert()` のようなメソッドの問題を説明しましたが、核心はいつも、その背後にあるグローバル状態、たいていは静的変数に保存された状態でした。`DB::insert()` メソッドは、データベース接続を保持する静的変数に頼っています。その変数がなければ、このメソッドは実装できません。 - -`Closure::fromCallable()`、`strlen()` など、決定的な静的メソッドや関数を使うことは、依存性注入と完全に両立します。これらの関数は、同じ入力に対して常に同じ結果を返すので予測できます。グローバル状態をまったく使いません。 - -とはいえ PHP には決定的でない関数もあります。たとえば `htmlspecialchars()` 関数です。第 3 パラメータの `$encoding` を省くと、設定オプション `default_charset` の値(`ini_get('default_charset')`)が既定になります。ですから予測できない振る舞いを防ぐため、このパラメータは常に指定することをおすすめします。Nette は一貫してそうしています。 - -`strtolower()` や `strtoupper()` のような一部の関数は、少し前まではロケールの設定(`setlocale()`)に応じて決定的でない振る舞いをしていました。これは多くの面倒を生み、とりわけトルコ語を扱うときに問題になりました。トルコ語では小文字にも大文字にも、点のある I と点のない I の区別があるからです。その結果 `strtolower('I')` は `ı`(点のない小文字の i)を返し、`strtoupper('i')` は `İ`(点のある大文字の I)を返して、原因不明のアプリケーションのエラーを数多く引き起こしました。ただしこの問題は PHP 8.2 で修正され、これらの関数はもうロケールに依存しません。 - -これは、グローバル状態(ロケールの設定)が世界中の何千もの開発者を悩ませた良い例です。最終的な解は、関数をロケールに依存しないものにすること、つまり隠れた依存関係を取り除くことでした。 - - -グローバル状態を使ってもよいのはどんなときか ----------------------- - -グローバル状態を使ってもよい、限られた特定の場面はあります。たとえばデバッグ中に、変数の値をダンプしたり、あるコードの実行時間を測ったりする場合です。あとでコードから取り除く一時的な処置であれば、グローバルにアクセスできるダンパーやタイマーを使うのは正当です。これらの道具はアプリケーションの設計の中核ではありません。 - -もうひとつの例が PHP の正規表現の関数(`preg_*`)で、コンパイル済みの正規表現を内部で静的メモリにキャッシュしています。コードの中で同じ正規表現の関数を何度も呼んでも、その式がコンパイルされるのは一度きりです。このキャッシュは性能を高め、利用者からはまったく見えないので、こうした内部の静的な状態の使い方は一般に問題ありません。 - - -まとめ ---- - -なぜ次のことに意味があるのかを見てきました。 - -1) コードから変更可能な静的プロパティ(グローバル状態)をすべて取り除く -2) 依存関係を明示的に宣言する -3) そして依存性注入を活用する - -コードを設計するときは、変更可能な `static $foo` のひとつひとつが問題の種になり得ることを思い出してください。DI と相性のよい環境を作るには、グローバル状態を完全に取り除き、依存性注入に置き換えることが決定的に重要です。 - -その過程で、複数の責務を持つクラスを分ける必要に気づくかもしれません。ためらわずにそうしてください。単一責任の原則を目指しましょう。 - -*[Flaw: Brittle Global State & Singletons |https://web.archive.org/web/20230321084133/http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/]などの記事がこの章の土台になっている Miško Hevery 氏に感謝します。* diff --git a/dependency-injection/ja/introduction.texy b/dependency-injection/ja/introduction.texy deleted file mode 100644 index b4e76fb999..0000000000 --- a/dependency-injection/ja/introduction.texy +++ /dev/null @@ -1,525 +0,0 @@ -Dependency Injection とは? -************************ - -.[perex] -この章では、どんなアプリケーションを書くときにも従うべき基本的なプログラミングの作法を紹介します。きれいで分かりやすく、保守しやすいコードを書くために欠かせない土台です。 - -これらの規則を身につけて守れば、Nette はあらゆる段階であなたを支えます。決まりきった作業を代わりにこなし、最大限の快適さを提供するので、あなたはロジックそのものに集中できます。 - -ここで示す原則はとても単純です。恐れることは何もありません。 - - -最初に書いたプログラムを覚えていますか -------------------- - -どの言語で書いたかは分かりませんが、もし PHP だったなら、おそらくこんな形だったでしょう。 - -```php -function addition(float $a, float $b): float -{ - return $a + $b; -} - -echo addition(23, 1); // 24 を出力 -``` - -ほんの数行のありふれたコードですが、そこには多くの重要な考え方が隠れています。変数が存在すること。コードが関数のような小さな単位に分けられること。そこに入力の引数を渡すと、結果が返ってくること。足りないのは条件とループくらいです。 - -関数にデータを渡すと結果が返ってくるというのは、数学など他の分野でも使われる、まったく理解しやすい考え方です。 - -関数にはシグネチャがあり、名前、パラメータとその型の並び、そして戻り値の型から成ります。利用者として関心があるのはシグネチャで、内部の実装について知る必要はふつうありません。 - -では、関数のシグネチャが次のようだったらどうでしょうか。 - -```php -function addition(float $x): float -``` - -パラメータがひとつの足し算? 妙ですね……。ではこれは? - -```php -function addition(): float -``` - -これは本当に妙ですね。この関数はどう使うのでしょうか。 - -```php -echo addition(); // 何を出力する? -``` - -こんなコードを見たら困惑するでしょう。初心者どころか、熟練したプログラマーでも理解できません。 - -こうした関数は中身がどうなっているのか気になりますか。足す数はどこから手に入れるのでしょうか。おそらく*どうにかして*自分で調達しているはずです。たとえば次のように。 - -```php -function addition(): float -{ - $a = Input::get('a'); - $b = Input::get('b'); - return $a + $b; -} -``` - -関数の本体で、ほかのグローバル関数や静的メソッドへの隠れた依存関係が見つかりました。数が実際にどこから来るのかを知るには、さらに調べる必要があります。 - - -こうしてはいけない ---------- - -いま見せた設計は、多くの悪い性質の元になっています。 - -- 関数のシグネチャは、足す数が要らないかのように装っていて、私たちを混乱させた。 -- この関数に別の 2 つの数を足させる方法が分からない。 -- 数がどこから来るのかを知るためにコードを覗く必要があった。 -- 隠れた依存関係が見つかった。 -- 完全に理解するには、その依存関係も調べなければならない。 - -そもそも入力を手に入れることは、足し算の関数の仕事でしょうか。もちろん違います。その責務は足し算そのものだけです。 - - -こんなコードには出会いたくありませんし、まして書きたくもありません。直し方は簡単です。基本に戻って、素直にパラメータを使うのです。 - - -```php -function addition(float $a, float $b): float -{ - return $a + $b; -} -``` - - -原則 1: 渡してもらう ------------- - -最も大切な原則はこれです。**関数やクラスが必要とするデータは、すべてそこへ渡さなければならない**。 - -データを手に入れる隠れた道を考え出す代わりに、素直にパラメータで渡しましょう。コードを何ひとつ良くしない隠し通路を考える時間を節約できます。 - -この原則をいつでもどこでも守れば、隠れた依存関係のないコードへの道を歩むことになります。作者だけでなく、あとから読む誰にとっても分かりやすいコードへ。すべてが関数やクラスのシグネチャから読み取れ、実装の中に隠れた詳細を探す必要のないコードへ。 - -この手法は専門的には **Dependency Injection(依存性注入)**と呼ばれます。そしてそのデータを**依存関係**と呼びます。単なるパラメータの受け渡しであって、それ以上のものではありません。 - -.[note] -デザインパターンである Dependency Injection と、道具である「Dependency Injection コンテナ」を混同しないでください。両者は根本的に異なるものです。コンテナについてはあとで扱います。 - - -関数からクラスへ --------- - -これはクラスにはどう当てはまるのでしょうか。クラスは単純な関数より複雑な存在ですが、原則 1 はここでもそのまま当てはまります。ただ[引数の渡し方が増える |passing-dependencies]だけです。たとえば、関数の場合とよく似た形にできます。 - -```php -class Math -{ - public function sum(float $a, float $b): float - { - return $a + $b; - } -} - -$math = new Math; -echo $math->sum(23, 1); // 24 -``` - -あるいはほかのメソッドや、コンストラクタを直接使う形でも。 - -```php -class Sum -{ - public function __construct( - private float $a, - private float $b, - ) { - } - - public function calculate(): float - { - return $this->a + $this->b; - } -} - -$sum = new Sum(23, 1); -echo $sum->calculate(); // 24 -``` - -どちらの例も Dependency Injection に完全に沿っています。 - - -現実の例 ----- - -現実の世界では、数を足すクラスを書くことはないでしょう。実践的な例に移りましょう。 - -ブログの記事を表す `Article` クラスがあるとします。 - -```php -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - // 記事をデータベースに保存します - } -} -``` - -使い方は次のようになります。 - -```php -$article = new Article; -$article->title = 'ダイエットについて知っておくべき 10 のこと'; -$article->content = '毎年、何百万人もの人々が ...'; -$article->save(); -``` - -`save()` メソッドは記事をデータベースのテーブルに保存します。[Nette Database |database:]で実装するのは簡単なはずですが、ひとつ引っかかることがあります。`Article` はデータベース接続、つまり `Nette\Database\Connection` クラスのオブジェクトをどこから手に入れるのでしょうか。 - -選択肢はたくさんありそうです。静的変数から取ってくることもできます。データベース接続を提供するクラスを継承することも。[シングルトン |global-state#シングルトン]を使うことも。あるいは Laravel で使われているような、いわゆるファサードも。 - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - DB::insert( - 'INSERT INTO articles (title, content) VALUES (?, ?)', - [$this->title, $this->content], - ); - } -} -``` - -素晴らしい、問題が解けました。 - -本当にそうでしょうか。 - -[#原則 1: 渡してもらう]を思い出しましょう。クラスが必要とするすべての依存関係は、そこへ渡さなければなりません。この原則を破れば、隠れた依存関係だらけで見通しの悪いコードへの道に踏み出すことになり、その結果は保守と発展が困難なアプリケーションです。 - -`Article` クラスの利用者には、`save()` メソッドが記事をどこに保存するのか分かりません。データベースのテーブル? どのデータベース、本番用かテスト用か? そしてそれはどう変えられるのでしょうか。 - -利用者は `save()` メソッドの実装を見て、`DB::insert()` メソッドが使われていることに気づきます。そこで、このメソッドがデータベース接続をどう手に入れるのかをさらに調べなければなりません。隠れた依存関係はかなり長い連鎖を作り得ます。 - -きれいでよく設計されたコードには、隠れた依存関係も、Laravel のファサードも、静的変数もありません。きれいでよく設計されたコードでは、引数が渡されます。 - -```php -class Article -{ - public function save(Nette\Database\Connection $db): void - { - $db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -あとで見るように、コンストラクタを使うほうがさらに実用的です。 - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function save(): void - { - $this->db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -.[note] -経験を積んだプログラマーなら、そもそも `Article` に `save()` メソッドがあるべきではなく、純粋にデータ構造を表し、保存は別のリポジトリが担うべきだと考えるかもしれません。それはもっともです。しかしそれは、この記事の主題である Dependency Injection と、単純な例を示すという目的をはるかに超えてしまいます。 - -たとえばデータベースを必要とするクラスを書くなら、それをどこから手に入れるかを考え出すのではなく、渡してもらってください。コンストラクタやほかのメソッドのパラメータとして渡すのがよいでしょう。依存関係を認めてください。クラスの API でそれを認めてください。分かりやすく予測できるコードが手に入ります。 - -では、エラーメッセージを記録する次のクラスはどうでしょうか。 - -```php -class Logger -{ - public function log(string $message): void - { - $file = LOG_DIR . '/log.txt'; - file_put_contents($file, $message . "\n", FILE_APPEND); - } -} -``` - -どう思いますか。[#原則 1: 渡してもらう]を守っているでしょうか。 - -守っていません。 - -肝心の情報、つまりログファイルのあるディレクトリを、*クラス自身が*定数から取ってきています。 - -使い方の例を見てください。 - -```php -$logger = new Logger; -$logger->log('気温は 23 °C です'); -$logger->log('気温は 10 °C です'); -``` - -実装を知らずに、メッセージがどこに書かれるか答えられるでしょうか。動作に `LOG_DIR` 定数の存在が必要だと思い当たるでしょうか。そして別の場所に書き込む 2 つめのインスタンスを作れるでしょうか。まず無理です。 - -クラスを直しましょう。 - -```php -class Logger -{ - public function __construct( - private string $file, - ) { - } - - public function log(string $message): void - { - file_put_contents($this->file, $message . "\n", FILE_APPEND); - } -} -``` - -これでクラスはずっと分かりやすく、設定しやすく、そして役に立つものになりました。 - -```php -$logger = new Logger('/path/to/log.txt'); -$logger->log('気温は 15 °C です'); -``` - - -でも、そんなの気にしたくない --------------- - -*「Article オブジェクトを作って save() を呼ぶとき、データベースのことなんて考えたくない。設定したところに保存されてほしいだけだ。」* - -*「Logger を使うとき、メッセージが書かれてほしいだけで、どこに書かれるかは考えたくない。グローバルな設定を使ってくれればいい。」* - -もっともな言い分です。 - -例として、ニュースレターを配信して結果を記録するクラスを見てみましょう。 - -```php -class NewsletterDistributor -{ - public function distribute(): void - { - $logger = new Logger(/* ... */); - try { - $this->sendEmails(); - $logger->log('メールを送信しました'); - - } catch (Exception $e) { - $logger->log('送信中にエラーが発生しました'); - throw $e; - } - } -} -``` - -改良された `Logger` は `LOG_DIR` 定数を使わなくなり、コンストラクタでファイルのパスを求めます。これはどう解けばよいでしょうか。`NewsletterDistributor` クラスはメッセージがどこに書かれるかに関心がなく、ただ記録したいだけです。 - -答えはまたも [#原則 1: 渡してもらう]です。クラスが必要とするデータをすべて渡します。 - -では、ログのパスをコンストラクタで渡して、それを `Logger` オブジェクトを作るときに使う、ということでしょうか。 - -```php -class NewsletterDistributor -{ - public function __construct( - private string $file, // ⛔ こうではありません! - ) { - } - - public function distribute(): void - { - $logger = new Logger($this->file); -``` - -こうではありません。そのパスは `NewsletterDistributor` クラスが必要とするデータでは**なく**、`Logger` が必要とするデータだからです。違いが分かりますか。`NewsletterDistributor` クラスが必要とするのはロガーそのものです。ですからロガーそのものを渡します。 - -```php -class NewsletterDistributor -{ - public function __construct( - private Logger $logger, // ✅ - ) { - } - - public function distribute(): void - { - try { - $this->sendEmails(); - $this->logger->log('メールを送信しました'); - - } catch (Exception $e) { - $this->logger->log('送信中にエラーが発生しました'); - throw $e; - } - } -} -``` - -これで `NewsletterDistributor` クラスのシグネチャから、ログの記録がその機能の一部だと分かるようになりました。しかもテストなどのためにロガーを別のものに差し替えるのは、まったく簡単です。さらに `Logger` クラスのコンストラクタが変わっても、私たちのクラスには何の影響もありません。 - - -原則 2: 自分のものだけ受け取る ------------------ - -惑わされず、自分の依存関係の依存関係を受け取らないでください。受け取るのは自分自身の依存関係だけです。 - -おかげで、ほかのオブジェクトを使うコードは、そのコンストラクタの変更からまったく独立でいられます。API はより正確になります。そして何より、その依存関係を別のものに差し替えるのが簡単になります。 - - -家族に新しい仲間 --------- - -開発チームは、データベースに書き込む 2 つめのロガーを作ることにしました。そこで `DatabaseLogger` クラスを作ります。これで `Logger` と `DatabaseLogger` の 2 つのクラスができ、一方はファイルに、もう一方はデータベースに書きます……名前づけが少し変ではないでしょうか。`Logger` を `FileLogger` に改名したほうがよくないでしょうか。もちろんそうです。 - -ただし賢くやりましょう。もとの名前でインターフェースを作ります。 - -```php -interface Logger -{ - function log(string $message): void; -} -``` - -……そして両方のロガーがそれを実装します。 - -```php -class FileLogger implements Logger -// ... - -class DatabaseLogger implements Logger -// ... -``` - -おかげで、ロガーを使っているコードのほかの部分を変える必要はまったくありません。たとえば `NewsletterDistributor` クラスのコンストラクタは、引き続き `Logger` をパラメータとして求めるだけで済みます。どのインスタンスを渡すかは私たち次第です。 - -**だからこそ、インターフェース名に `Interface` の接尾辞や `I` の接頭辞を付けないのです。** さもないと、コードをこれほど優雅に拡張できなくなります。 - - -ヒューストン、問題が発生した --------------- - -ファイル用でもデータベース用でも、アプリケーション全体でロガーのインスタンスはひとつあれば足り、記録が必要なところに渡せばよいのに対し、`Article` クラスの事情はかなり違います。そのインスタンスは必要に応じて、何度でも作られます。コンストラクタのデータベース依存はどう扱えばよいでしょうか。 - -例として、フォームの送信後に記事をデータベースに保存すべきコントローラを考えてみましょう。 - -```php -class EditController extends Controller -{ - public function formSubmitted($data) - { - $article = new Article(/* ... */); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -答えは明らかに見えます。データベースのオブジェクトをコンストラクタ経由で `EditController` に渡して、`$article = new Article($this->db)` とすればよさそうです。 - -しかし `Logger` とファイルのパスの先ほどの場合と同じく、これは正しいやり方ではありません。データベースは `EditController` の依存関係ではなく、`Article` の依存関係です。ですからデータベースを渡すことは[原則 2: 自分のものだけ受け取る |#原則 2: 自分のものだけ受け取る]に反します。`Article` クラスのコンストラクタが変われば(新しいパラメータが加われば)、インスタンスを作っているすべての場所のコードを直す必要があります。まいりましたね。 - -ヒューストン、どうすればいい? - - -原則 3: ファクトリに任せる ---------------- - -隠れた依存関係をなくし、すべての依存関係を引数として渡すことで、より設定しやすく柔軟なクラスが手に入りました。ですから、その柔軟なクラスを作って設定してくれる何かがさらに必要になります。それをファクトリと呼びます。 - -原則はこうです。クラスが依存関係を持つなら、そのインスタンスの生成をファクトリに任せる。 - -ファクトリは、Dependency Injection の世界における `new` 演算子の賢い代替です。 - -.[note] -*factory method* デザインパターンと混同しないでください。あちらはファクトリの特定の使い方を述べたもので、この話題とは関係がありません。 - - -ファクトリ ------ - -ファクトリとは、オブジェクトを作って設定するメソッドやクラスです。`Article` を作るクラスを `ArticleFactory` と呼ぶことにすると、次のような形になります。 - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -コントローラでの使い方は次のようになります。 - -```php -class EditController extends Controller -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function formSubmitted($data) - { - // ファクトリにオブジェクトを作らせます - $article = $this->articleFactory->create(); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -これで `Article` クラスのコンストラクタのシグネチャが変わっても、対応が必要なコードは `ArticleFactory` だけです。`EditController` など `Article` オブジェクトを扱うほかのコードは、まったく影響を受けません。 - -本当に事態は良くなったのか、と首をかしげているかもしれません。コードの量は増え、全体がやけに複雑に見え始めました。 - -心配は要りません。もうすぐ Nette の DI コンテナに行き着きます。そこには、Dependency Injection を使ったアプリケーションの構築を大きく単純にしてくれる仕掛けがいくつもあります。たとえば `ArticleFactory` クラスの代わりに、[インターフェースを書くだけ |factory]で済むようになります。 - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -とはいえ先を急ぎすぎました。どうぞこのまま :-) - - -まとめ ---- - -この章のはじめに、きれいなコードを設計する道筋を示すと約束しました。クラスについて次のことを守るだけです。 - -1) [必要とする依存関係を渡してもらう |#原則 1: 渡してもらう] -2) [逆に、直接必要としないものは渡されない |#原則 2: 自分のものだけ受け取る] -3) [そして依存関係を持つオブジェクトはファクトリで作るのが最善 |#原則 3: ファクトリに任せる] - -一見そうは見えないかもしれませんが、この 3 つの原則は遠くまで届く結果をもたらします。コードの設計に対するまったく違う見方につながるのです。その価値はあるでしょうか。古い習慣を捨てて Dependency Injection を一貫して使い始めたプログラマーは、この一歩を職業人生の節目と見なします。彼らには、明快で保守しやすいアプリケーションの世界が開けたのです。 - -では、コードが Dependency Injection を一貫して使っていなかったら? 静的メソッドやシングルトンの上に築かれていたら? それは問題につながるでしょうか。[はい、それも非常に大きな問題に |global-state]。 diff --git a/dependency-injection/ja/nette-container.texy b/dependency-injection/ja/nette-container.texy deleted file mode 100644 index 8dad1cc7eb..0000000000 --- a/dependency-injection/ja/nette-container.texy +++ /dev/null @@ -1,117 +0,0 @@ -Nette DI Container -****************** - -.[perex] -Nette DI は Nette の最も興味深いライブラリのひとつです。きわめて高速で、驚くほど設定しやすいコンパイル済みの DI コンテナを生成し、自動的に更新できます。 - -DI コンテナが作るべきサービスの形は、ふつう [NEON 形式|neon:format]の設定ファイルで定義します。[前の章|container]で手作りしたコンテナは、次のように書けます。 - -```neon -parameters: - db: - dsn: 'mysql:' - user: root - password: '***' - -services: - - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - - ArticleFactory - - EditController -``` - -構文はとても簡潔です。 - -`ArticleFactory` と `EditController` クラスのコンストラクタで宣言されたすべての依存関係は、いわゆる[オートワイヤリング|autowiring]のおかげで Nette DI が見つけて自動的に渡すので、設定ファイルに何も書く必要はありません。ですからパラメータが変わっても、設定を変える必要はありません。開発中は Nette がコンテナを自動的に作り直します。あなたはアプリケーションの開発だけに集中できます。 - -セッターで依存関係を渡したい場合は、そのために [setup |services#Setup]セクションを使います。 - -Nette DI はコンテナの PHP コードを直接生成します。ですから結果は `.php` ファイルで、開いて中を確かめられます。コンテナがどう機能するかを正確に見られるわけです。IDE でデバッグして、実行を追うこともできます。そして何より、生成された PHP コードはきわめて高速です。 - -Nette DI は、与えられたインターフェースにもとづいて[ファクトリ|factory]のコードを生成することもできます。ですから `ArticleFactory` クラスの代わりに、アプリケーションではインターフェースを作るだけで済みます。 - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -完全な例は [GitHub|https://github.com/nette-examples/di-example-doc]にあります。 - - -単体での利用 ------- - -Nette DI ライブラリをアプリケーションに組み込むのはとても簡単です。まず Composer でインストールします(zip ファイルをダウンロードするのは、もう時代遅れですから)。 - -```shell -composer require nette/di -``` - -次のコードは [Compiler |api:Nette\DI\Compiler]を使い、`config.neon` ファイルに書かれた設定に従って DI コンテナのインスタンスを作ります。 - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp'); -$class = $loader->load(function ($compiler) { - $compiler->loadConfig(__DIR__ . '/config.neon'); -}); -$container = new $class; -``` - -コンテナが生成されるのは一度きりで、そのコードはキャッシュ(`__DIR__ . '/temp'` ディレクトリ)に書かれ、以降のリクエストではそこから読み込まれるだけです。 - -`Compiler` は単体では、設定の `services` と `parameters` セクションだけを有効にします。`search`、`decorator`、`di`、`inject` などほかのセクションを使うには、まずその拡張を登録してください。そして設定の `extensions` セクションから拡張を登録できるようにするには、`ExtensionsExtension` を足します。 - -```php -$compiler->addExtension('search', new Nette\DI\Extensions\SearchExtension($tempDir)); -$compiler->addExtension('extensions', new Nette\DI\Extensions\ExtensionsExtension); -``` - -完全な Nette のアプリケーションで使われる [Configurator |application:bootstrapping]は、これらをすべて自動的に登録します。 - -同じキャッシュディレクトリに複数の異なるコンテナを置く場合は、`load()` の第 2 引数に渡すキーで区別してください。それは生成されるクラス名の一部になります。 - -```php -$class = $loader->load( - fn($compiler) => $compiler->loadConfig(__DIR__ . '/config.neon'), - 'my-key', -); -``` - -サービスの生成と取得には `getService()` や `getByType()` メソッドを使います。`EditController` オブジェクトはこうして作ります。 - -```php -$controller = $container->getByType(EditController::class); -$controller->someMethod(); -``` - -開発中は自動リフレッシュのモードを有効にすると便利です。クラスや設定ファイルが変わると、コンテナが自動的に作り直されます。[ContainerLoader |api:Nette\DI\ContainerLoader]のコンストラクタの第 2 引数に `true` を渡すだけです。 - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp', true); -``` - - -コンテナを扱う -------- - -`getService()` と `getByType()` のほかにも、コンテナのオブジェクトには便利なメソッドがいくつかあります。 - -- `getByType(string $type, bool $throw = true): ?object` は指定した型のサービスを返します。第 2 引数に `false` を渡すと、そのサービスがない場合に例外を投げる代わりに `null` を返します。 -- `hasService(string $name): bool` と `isCreated(string $name): bool` は、サービスが定義されているか、そしてすでに生成されているかを教えてくれます。 -- `getParameters(): array` はコンテナのすべてのパラメータを、`getParameter($key)` はひとつのパラメータを返します。 -- `createInstance(string $class, array $args = []): object` は指定したクラスの新しいインスタンスを作り、そのコンストラクタの依存関係をオートワイヤリングで渡します。 -- `callMethod(callable $function, array $args = []): mixed` は指定した callable を呼び、その引数をオートワイヤリングで渡します。 -- `callInjects(object $service): void` は指定したオブジェクトのすべての `inject*()` メソッドを呼び、依存関係を渡します。 - -コンテナのコンストラクタは、設定で定義されたものを補うパラメータの配列も受け取れます。 - -```php -$container = new $class(['host' => 'localhost']); -``` - - -Nette Framework との併用 --------------------- - -ここまで見てきたとおり、Nette DI の利用は Nette Framework で作られたアプリケーションに限られません。3 行のコードでどこにでも組み込めます。とはいえ Nette Framework でアプリケーションを開発しているなら、コンテナの設定と生成は [Bootstrap |application:bootstrapping#DI コンテナの設定]が引き受けます。 diff --git a/dependency-injection/ja/passing-dependencies.texy b/dependency-injection/ja/passing-dependencies.texy deleted file mode 100644 index ffe6167e6f..0000000000 --- a/dependency-injection/ja/passing-dependencies.texy +++ /dev/null @@ -1,215 +0,0 @@ -依存関係の受け渡し -********* - -<div class=perex> - -引数、DI の用語でいう「依存関係」は、次の主な方法でクラスに渡せます。 - -* コンストラクタインジェクション -* メソッドインジェクション(いわゆるセッターインジェクション) -* プロパティインジェクション -* `inject*()` メソッドまたは `#[Inject]` アトリビュートの利用 - -</div> - -それぞれの形を具体的な例で見ていきましょう。 - - -コンストラクタインジェクション -=============== - -依存関係は、オブジェクトが生成されるときにコンストラクタの引数として渡されます。 - -```php -class MyClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -$obj = new MyClass($cache); -``` - -この方法は、クラスが動くのに絶対に必要な依存関係に向いています。それがなければインスタンスを作れないからです。 - -PHP 8.0 以降は、より短い書き方([コンストラクタのプロパティ昇格 |https://blog.nette.org/en/php-8-0-complete-overview-of-news#toc-constructor-property-promotion])が使え、機能は同じです。 - -```php -// PHP 8.0 -class MyClass -{ - public function __construct( - private Cache $cache, - ) { - } -} -``` - -PHP 8.1 以降はプロパティに `readonly` フラグを付けられ、初期化のあとに値が変わらないことを宣言できます。 - -```php -// PHP 8.1 -class MyClass -{ - public function __construct( - private readonly Cache $cache, - ) { - } -} -``` - -DI コンテナは[オートワイヤリング |autowiring]を使って、依存関係をコンストラクタに自動的に渡します。この方法で渡せない引数(文字列、数値、真偽値など)は[設定で指定します |services#引数]。 - - -コンストラクタ地獄 ---------- - -*コンストラクタ地獄*という言葉は、依存関係を必要とするコンストラクタを持つ親クラスを子クラスが継承し、その子クラスも依存関係を必要とする状況を指します。子クラスは親の依存関係も受け取って渡さなければなりません。 - -```php -abstract class BaseClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass extends BaseClass -{ - private Database $db; - - // ⛔ コンストラクタ地獄 - public function __construct(Cache $cache, Database $db) - { - parent::__construct($cache); - $this->db = $db; - } -} -``` - -問題が起こるのは、たとえば新しい依存関係が加わって `BaseClass` のコンストラクタを変えたくなったときです。すると子クラスのコンストラクタもすべて直す必要が出てきます。こうしてその変更は地獄になります。 - -どうすれば防げるでしょうか。答えは**[継承よりコンポジションを優先する |faq#なぜ継承よりコンポジションが好まれるのですか?]**ことです。 - -そこでコードの設計を変えます。[抽象 |nette:introduction-to-object-oriented-programming#抽象クラス]の `Base*` クラスを避けるのです。`MyClass` が `BaseClass` を継承して機能を得る代わりに、その機能を依存関係として渡します。 - -```php -final class SomeFunctionality -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass -{ - private SomeFunctionality $sf; - private Database $db; - - public function __construct(SomeFunctionality $sf, Database $db) // ✅ - { - $this->sf = $sf; - $this->db = $db; - } -} -``` - - -セッターインジェクション -============ - -依存関係は、それを private なプロパティに保存するメソッドを呼ぶことで渡されます。こうしたメソッドの一般的な命名は `set*()` の形なのでセッターと呼ばれますが、もちろん別の名前でもかまいません。 - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - $this->cache = $cache; - } -} - -$obj = new MyClass; -$obj->setCache($cache); -``` - -この方法は、クラスの動作に不可欠でない省略可能な依存関係に向いています。オブジェクトが実際にその依存関係を受け取る保証(つまり呼び出し側がそのメソッドを呼ぶ保証)がないからです。 - -同時にこの方法では、セッターを繰り返し呼んで依存関係を変えられます。それが望ましくないなら、メソッドの中にチェックを足すか、PHP 8.1 以降なら `$cache` プロパティに `readonly` フラグを付けてください。 - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - if (isset($this->cache)) { - throw new RuntimeException('The dependency has already been set'); - } - $this->cache = $cache; - } -} -``` - -セッターの呼び出しは、DI コンテナの設定の [setup キー |services#Setup]で定義します。ここでもオートワイヤリングによる依存関係の自動的な受け渡しが使われます。 - -```neon -services: - - create: MyClass - setup: - - setCache -``` - - -プロパティインジェクション -============= - -依存関係は、メンバーのプロパティに直接書き込むことで渡されます。 - -```php -class MyClass -{ - public Cache $cache; -} - -$obj = new MyClass; -$obj->cache = $cache; -``` - -この方法は不適切と見なされます。メンバーのプロパティを `public` として宣言しなければならないからです。その結果、渡された依存関係が本当に求める型かどうかを保証する制御を失い(これは PHP 7.4 でプロパティの型宣言が入る前にはとくに当てはまりました)、新しく代入された依存関係に対して独自のロジックで反応する、たとえばそのあとの変更を防ぐ、といったこともできなくなります。同時にそのプロパティはクラスの公開 API の一部になってしまい、それは意図しないことかもしれません。 - -プロパティへの代入は、DI コンテナの設定の [setup セクション |services#Setup]で定義します。 - -```neon -services: - - create: MyClass - setup: - - $cache = @\Cache -``` - - -Inject -====== - -ここまでの 3 つの方法はあらゆるオブジェクト指向言語に一般に当てはまりますが、`inject*()` メソッドや `#[Inject]` アトリビュートによる注入は、ふつう Nette のプレゼンターで使われ、そこでは既定で有効です。ほかのサービスも [`inject: true` |services#Inject モード]で使えるようにできます。これらは[別の章 |best-practices:inject-method-attribute]で扱います。 - - -どの方法を選ぶか -======== - -- コンストラクタは、クラスが動くのに絶対に必要な依存関係に向いています。 -- 逆にセッターは、省略可能な依存関係や、あとで変える必要があるかもしれない依存関係に向いています。 -- 公開プロパティは一般におすすめしません。 diff --git a/dependency-injection/ja/services.texy b/dependency-injection/ja/services.texy deleted file mode 100644 index dffad9eb64..0000000000 --- a/dependency-injection/ja/services.texy +++ /dev/null @@ -1,480 +0,0 @@ -サービスの定義 -******* - -.[perex] -設定は、個々のサービスをどう作り、依存関係とどう結びつけるかを DI コンテナに指示する場所です。Nette はそのための、とても明快で優雅な方法を提供します。 - -NEON の設定ファイルの `services` セクションで、独自のサービスとその設定を定義します。`PDO` クラスのインスタンスを表す `database` というサービスを定義する簡単な例を見てみましょう。 - -```neon -services: - database: PDO('sqlite::memory:') -``` - -上の設定から、[DI コンテナ|container]に次のファクトリメソッドが作られます。 - -```php -public function createServiceDatabase(): PDO -{ - return new PDO('sqlite::memory:'); -} -``` - -サービス名を付けると、設定ファイルのほかの場所から `@serviceName` の形で参照できます。サービスに名前を付ける必要がなければ、単に箇条書きの記号(`-`)を使えます。 - -```neon -services: - - PDO('sqlite::memory:') -``` - -DI コンテナからサービスを取り出すには、サービス名をパラメータに取る `getService()` メソッドか、サービスの型を取る `getByType()` メソッドを使います。 - -```php -$database = $container->getService('database'); -$database = $container->getByType(PDO::class); -``` - - -サービスの生成 -======= - -ふつうサービスは、特定のクラスをインスタンス化するだけで作ります。たとえば次のようにです。 - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -キーを足して設定を広げたい場合は、定義を複数行に分けられます。 - -```neon -services: - database: - create: PDO('sqlite::memory:') - setup: ... -``` - -`create` キーには `factory` という別名があり、どちらの書き方もよく使われます。ただし `create` の利用をおすすめします。 - -コンストラクタやファクトリメソッドの引数は、`arguments` キーで指定することもできます。 - -```neon -services: - database: - create: PDO - arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] -``` - -サービスは必ずしも単純なクラスのインスタンス化で作る必要はなく、静的メソッドやほかのサービスのメソッドを呼んだ結果でもかまいません。 - -```neon -services: - database: DatabaseFactory::create() - router: @routerFactory::create() -``` - -分かりやすさのために `->` の代わりに `::` を使っていることに注意してください。[#式の言語]をご覧ください。次のファクトリメソッドが生成されます。 - -```php -public function createServiceDatabase(): PDO -{ - return DatabaseFactory::create(); -} - -public function createServiceRouter(): RouteList -{ - return $this->getService('routerFactory')->create(); -} -``` - -DI コンテナは、作られるサービスの型を知る必要があります。戻り値の型が指定されていないメソッドでサービスを作る場合は、その型を設定で明示的に宣言しなければなりません。 - -```neon -services: - database: - create: DatabaseFactory::create() - type: PDO -``` - - -引数 -=== - -コンストラクタやメソッドへの引数は、PHP 自体とよく似たやり方で渡します。 - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -読みやすさのために、引数を別々の行に並べることもできます。その場合、カンマは省略できます。 - -```neon -services: - database: PDO( - 'mysql:host=127.0.0.1;dbname=test' - root - secret - ) -``` - -引数に名前を付ければ、順序を気にする必要もなくなります。 - -```neon -services: - database: PDO( - username: root - password: secret - dsn: 'mysql:host=127.0.0.1;dbname=test' - ) -``` - -一部の引数を省いて既定値を使いたい場合や、[オートワイヤリング|autowiring]でサービスを注入させたい場合は、アンダースコア(`_`)を使います。 - -```neon -services: - foo: Foo(_, %appDir%) -``` - -引数にはサービスやパラメータなど、さまざまなものを書けます。[#式の言語]をご覧ください。 - - -Setup -===== - -`setup` セクションでは、サービスの生成時に呼ぶべきメソッドを定義します。 - -```neon -services: - database: - create: PDO(%dsn%, %user%, %password%) - setup: - - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) -``` - -PHP では次のようになります。 - -```php -public function createServiceDatabase(): PDO -{ - $service = new PDO('...', '...', '...'); - $service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - return $service; -} -``` - -メソッドの呼び出しのほかに、プロパティへの値の代入もできます。配列への要素の追加にも対応していますが、NEON の構文との衝突を避けるため、配列アクセスを引用符で囲む必要があります。 - -```neon -services: - foo: - create: Foo - setup: - - $value = 123 - - '$onClick[]' = [@bar, clickHandler] -``` - -PHP のコードでは次のようになります。 - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - $service->value = 123; - $service->onClick[] = [$this->getService('bar'), 'clickHandler']; - return $service; -} -``` - -さらに setup では、静的メソッドやほかのサービスのメソッドも呼べます。現在のサービス自身を引数として渡す必要があるなら、`@self` で参照します。 - -```neon -services: - foo: - create: Foo - setup: - - My\Helpers::initializeFoo(@self) - - @anotherService::setFoo(@self) -``` - -分かりやすさのために `->` の代わりに `::` を使っていることに注意してください。[#式の言語]をご覧ください。次のファクトリメソッドが生成されます。 - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - My\Helpers::initializeFoo($service); - $this->getService('anotherService')->setFoo($service); - return $service; -} -``` - - -式の言語 -==== - -Nette DI はきわめて豊かな式の言語を備えていて、ほとんど何でも定義できます。設定ファイルでは[パラメータ |configuration#パラメータ]が使えます。 - -```neon -# パラメータ -%wwwDir% - -# キーの下のパラメータの値 -%mailer.user% - -# 文字列の中のパラメータ -'%wwwDir%/images' -``` - -さらにオブジェクトの生成、メソッドや関数の呼び出しもできます。 - -```neon -# オブジェクトの生成 -DateTime() - -# 静的メソッドの呼び出し -Collator::create(%locale%) - -# PHP の関数の呼び出し -::getenv(DB_USER) -``` - -サービスは名前でも型でも参照できます。 - -```neon -# 名前によるサービス -@database - -# 型によるサービス -@Nette\Database\Connection -``` - -ファーストクラス callable 構文も使えます。 .{data-version:3.2.0} - -```neon -# コールバックの生成。[@user, logout] と同じです -@user::logout(...) -``` - -定数も使えます。 - -```neon -# クラス定数 -FilesystemIterator::SKIP_DOTS - -# PHP の関数 constant() でグローバル定数を取得 -::constant(\PHP_VERSION) -``` - -サービスの公開プロパティと定数には `@service::member` でアクセスします。その名前がプロパティを指すのか定数を指すのかは最初の文字で決まります。小文字で始まれば公開プロパティ、大文字で始まれば定数です。 - -```neon -# サービスの公開プロパティ(小文字で始まります) -@settings::apiUrl - -# サービスのクラス定数(大文字で始まります) -@settings::Version -``` - -メソッドの呼び出しは PHP と同じように連ねられます。分かりやすさのために `->` の代わりに `::` を使います。 - -```neon -DateTime()::format('Y-m-d') -# PHP: (new DateTime())->format('Y-m-d') - -@http.request::getUrl()::getHost() -# PHP: $this->getService('http.request')->getUrl()->getHost() -``` - -これらの式は、[サービスの生成 |#サービスの生成]、[引数 |#引数]、[setup |#Setup]セクション、[パラメータ |configuration#パラメータ]など、どこでも使えます。 - -```neon -parameters: - ipAddress: @http.request::getRemoteAddress() - -services: - database: - create: DatabaseFactory::create( @anotherService::getDsn() ) - setup: - - initialize( ::getenv('DB_USER') ) -``` - - -特別な関数 ------ - -設定ファイルでは次の特別な関数が使えます。 - -- `not()` は値を反転します -- `bool()`、`int()`、`float()`、`string()` は指定した型への損失のないキャストです .{data-version:3.0.5} -- `typed()` は指定した型のすべてのサービスの配列を作ります -- `tagged()` は指定したタグを持つすべてのサービスの配列を作ります - -```neon -services: - - Foo( - id: int(::getenv('ProjectId')) - productionMode: not(%debugMode%) - ) -``` - -`(int)` のような標準の PHP のキャストと違い、損失のないキャストは数値でない値に対して例外を投げます。 - -`typed()` 関数は、指定した型(クラスまたはインターフェース)のすべてのサービスの配列を作ります。オートワイヤリングが無効にされたサービスは除かれます。カンマで区切って複数の型を指定することもできます。 - -```neon -services: - - BarsDependent( typed(Bar) ) -``` - -ある型のサービスの配列は、[オートワイヤリング |autowiring#サービスのコレクション]で自動的に引数として渡すこともできます。 - -`tagged()` 関数は、特定のタグを持つすべてのサービスの配列を作ります。ここでもカンマで区切って複数のタグを指定できます。 - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - - -オートワイヤリング -========= - -`autowired` キーを使うと、特定のサービスのオートワイヤリングの振る舞いを変えられます。詳しくは[オートワイヤリングの章|autowiring]をご覧ください。 - -```neon -services: - foo: - create: Foo - autowired: false # foo サービスはオートワイヤリングから除かれます -``` - - -遅延サービス .{data-version:3.2.4} -============================ - -遅延読み込みは、サービスの生成を実際に必要になるまで先延ばしにする手法です。グローバルな設定では、すべてのサービスについて[遅延生成を有効に |configuration#遅延サービス]できます。個々のサービスでは、その振る舞いを上書きできます。 - -```neon -services: - foo: - create: Foo - lazy: false -``` - -サービスが遅延として定義されていると、DI コンテナからそれを要求したとき、特別なプロキシオブジェクトを受け取ります。このプロキシは実際のサービスと見た目も振る舞いも同じですが、本当の初期化(コンストラクタの呼び出しと setup の実行)は、そのメソッドやプロパティに最初にアクセスしたときにはじめて起こります。 - -サービスが遅れて作られるので、設定の誤りも遅れて現れることを覚えておいてください。たとえばデータベースの認証情報の誤りは、アプリケーションの起動時ではなく、最初のクエリのときにはじめて明らかになります。 - -遅延生成は循環依存、つまりサービス A がサービス B を必要とし、同時に B が A を必要とする状況も和らげます。遅延生成がなければ、コンテナは `Circular reference detected` のエラーを報告します。遅延プロキシがあれば、サービス A は B のプロキシだけを受け取り、それが実際に使われるとき、つまり A がすでに存在する時点で自分を初期化します。とはいえ循環依存は設計の欠陥の兆しなので、取り除くほうがよいでしょう。 - -.[note] -遅延読み込みには PHP 8.4 以上が必要で、クラスを直接インスタンス化して作られるサービス(`create: Foo` など)にだけ働き、ファクトリメソッドで作られるサービスには働きません。最終的に PHP の内部クラスを継承するクラスにも使えません。遅延読み込みを適用できない場合、`lazy: true` のフラグは黙って無視されます。 - - -タグ -=== - -タグはサービスに補足の情報を足すためのものです。サービスにはひとつ以上のタグを割り当てられます。 - -```neon -services: - foo: - create: Foo - tags: - - cached -``` - -タグは値を持つこともできます。 - -```neon -services: - foo: - create: Foo - tags: - logger: monolog.logger.event -``` - -特定のタグを持つすべてのサービスを取得するには、`tagged()` 関数が使えます。 - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - -DI コンテナの中では、`findByTag()` メソッドで特定のタグを持つすべてのサービスの名前を取得できます。 - -```php -$names = $container->findByTag('logger'); -// $names はサービス名をキー、タグの値を値とする配列です -// たとえば ['foo' => 'monolog.logger.event', ...] -``` - - -Inject モード -========== - -`inject: true` フラグを使うと、[Inject |best-practices:inject-method-attribute#Inject アトリビュート]アトリビュートを付けた公開プロパティと [inject*() |best-practices:inject-method-attribute#inject*() メソッド]メソッドによる依存性注入が有効になります。 - -```neon -services: - articles: - create: App\Model\Articles - inject: true -``` - -既定では、`inject` モードはプレゼンターでのみ有効です。 - - -サービスの変更 -======= - -DI コンテナには、組み込みの拡張や[ユーザーの拡張|extensions]によって追加された多くのサービスが入っています。こうした既存のサービスの定義は、設定で直接変更できます。たとえば `application.application` サービスのクラスは既定で `Nette\Application\Application` ですが、別のものに変えられます。 - -```neon -services: - application.application: - create: MyApplication - alteration: true -``` - -`alteration` フラグは、既存のサービスを変更しているだけであることを示します。同時に安全装置としても働き、変更しようとしたサービスが存在しなければ、コンパイルは例外で失敗します。 - -setup を足すこともできます。 - -```neon -services: - application.application: - create: MyApplication - alteration: true - setup: - - '$onStartup[]' = [@resource, init] -``` - -サービスは内部の名前で特定しなくてもよく、型で参照することもできます。先ほどの例は次のようにも書けます。 - -```neon -services: - @Nette\Application\Application: - create: MyApplication -``` - -サービスを変更するとき、もとの引数、setup の項目、タグを取り除きたいことがあります。そのときは `reset` キーを使います。 - -```neon -services: - application.application: - create: MyApplication - alteration: true - reset: - arguments: true - setup: true - tags: true -``` - -拡張が追加したサービスを取り除きたい場合は、次のようにします。 - -```neon -services: - cache.journal: false -``` diff --git a/dependency-injection/ja/upgrading.texy b/dependency-injection/ja/upgrading.texy deleted file mode 100644 index ee16a755a8..0000000000 --- a/dependency-injection/ja/upgrading.texy +++ /dev/null @@ -1,49 +0,0 @@ -アップグレード -******* - - -バージョン 3.1 へのアップグレード -=================== - -- オートワイヤリングは、既定値のない nullable なパラメータに `null` を渡さなくなりました。引数を明示的に渡すか、パラメータに既定値を与えてください -- `@return` アノテーションのサポートはなくなりました。戻り値の型を使うか、サービスの定義で `type:` を使って型を指定してください -- キー `dynamic` は `imported` に、`class` は `type` に改名されました -- 省略した引数を表す記号が `...` から `_` に変わりました。たとえば `MyService(_, 123)` です -- NEON ファイルでは、文字列の先頭の `@` をエスケープする必要がなくなりました -- 生成されるファクトリの定義の中の `parameters` キーは非推奨です -- `Nette\DI\Config\Loader::save()` メソッドは非推奨です。設定の書き出しには `Nette\DI\Config\Adapters\NeonAdapter::dump()` を使ってください - -バージョン 3.1 は移行のためのリリースです。新機能はもたらしませんが、のちに動きが変わるものすべてについて notice で警告します。記事 [Nette DI 3.1: transition release |https://blog.nette.org/en/nette-di-3-1-transition-release]をご覧ください。 - - -バージョン 3.0 へのアップグレード -=================== - -- INI ファイルのサポートは削除されました -- 疑問符を使って設定に PHP のコードを直接書く方法(`"$service->onError[] = ?"(...)` など)は削除されました。代わりに配列の記法 `'$onError[]' = [...]` を使ってください -- 設定ファイルでは `class: PDO(...)` ではなく `factory: PDO(...)` を使ってください -- プレゼンターに `nette.presenter` タグは使わなくなりました - - -コンパイラ拡張の作者へ ------------ - -Nette 2.4 は内部ですべてのサービスを `Nette\DI\ServiceDefinition` として表していましたが、現在は定義の型がいくつかあります。取り込まれた(動的な)サービスには `Nette\DI\Definitions\ImportedDefinition`、インターフェースにもとづいて生成されるファクトリには `Nette\DI\Definitions\FactoryDefinition`、生成されるアクセサには `Nette\DI\Definitions\AccessorDefinition`、通常のサービスには `Nette\DI\Definitions\ServiceDefinition` です。 - -そのため、新しい定義を作るメソッドも `ContainerBuilder::addDefinition()` のほかにいくつかあります。`addFactoryDefinition()`、`addAccessorDefinition()`、`addImportedDefinition()` です。 - - -バージョン 2.4 へのアップグレード -=================== - -- ひとつの設定ファイルの中の設定セクション(production、development など)は非推奨です。`config.neon` と `config.local.neon` の 2 つのファイルを使ってください -- サービス定義の継承は非推奨です -- `Statement::setEntity()` は非推奨です - - -バージョン 2.3 へのアップグレード -=================== - -- 設定ファイルの拡張のセクションの中にサービスを置くサポートは削除されました -- 動的に追加される拡張のサポートは削除されました -- サービスを動的に置き換える場合(`removeService()`、`addService()` を使う場合)、新しいサービスはもとのものと同じインターフェースやクラスのインスタンスでなければなりません diff --git a/dependency-injection/meta.json b/dependency-injection/meta.json deleted file mode 100644 index bd9ae96928..0000000000 --- a/dependency-injection/meta.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": "3.x", - "repo": "nette/di", - "composer": "nette/di", - "api": "https://api.nette.org/di/" -} diff --git a/dependency-injection/pl/@home.texy b/dependency-injection/pl/@home.texy deleted file mode 100644 index 0a0bfe4739..0000000000 --- a/dependency-injection/pl/@home.texy +++ /dev/null @@ -1,22 +0,0 @@ -Nette DI -******** - -.[perex] -Wstrzykiwanie zależności to wzorzec projektowy, który zasadniczo zmieni sposób, w jaki patrzysz na kod i tworzenie aplikacji. Otwiera drogę do świata czysto zaprojektowanych i utrzymywalnych aplikacji. - -- [Czym jest wstrzykiwanie zależności? |introduction] -- [Stan globalny i singletony |global-state] -- [Przekazywanie zależności |passing-dependencies] -- [Czym jest kontener DI? |container] -- [Często zadawane pytania |faq] - - -Pakiet `nette/di` dostarcza wyjątkowo zaawansowany, kompilowany kontener DI dla PHP. - -- [Nette DI Container |nette-container] -- [Konfiguracja |configuration] -- [Definiowanie usług |services] -- [Autowiring |autowiring] -- [Generowane fabryki |factory] -- [Tworzenie rozszerzeń dla Nette DI |extensions] -- [Kompilacja w szczegółach |compilation-internals] diff --git a/dependency-injection/pl/@left-menu.texy b/dependency-injection/pl/@left-menu.texy deleted file mode 100644 index 4701baf249..0000000000 --- a/dependency-injection/pl/@left-menu.texy +++ /dev/null @@ -1,28 +0,0 @@ -Dependency Injection -******************** -- [Czym jest DI? |introduction] -- [Stan globalny i singletony |global-state] -- [Przekazywanie zależności |passing-dependencies] -- [Czym jest kontener DI? |container] -- [Często zadawane pytania |faq] - - -Nette DI --------- -- [Nette DI Container |nette-container] -- [Konfiguracja |configuration] -- [Definiowanie usług |services] -- [Autowiring |autowiring] -- [Generowane fabryki |factory] -- [Tworzenie rozszerzeń dla Nette DI |extensions] -- [Kompilacja w szczegółach |compilation-internals] -- [Aktualizacja|upgrading] - - -Dalsza lektura -************** -- [Dokumentacja Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Dobre praktyki |best-practices:] -- [Rozwiązywanie problemów |nette:troubleshooting] diff --git a/dependency-injection/pl/@meta.texy b/dependency-injection/pl/@meta.texy deleted file mode 100644 index 61ac92d1af..0000000000 --- a/dependency-injection/pl/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Dokumentacja Nette}} diff --git a/dependency-injection/pl/autowiring.texy b/dependency-injection/pl/autowiring.texy deleted file mode 100644 index 1b9cdd29b4..0000000000 --- a/dependency-injection/pl/autowiring.texy +++ /dev/null @@ -1,279 +0,0 @@ -Autowiring -********** - -.[perex] -Autowiring to świetna funkcja, która automatycznie przekazuje do konstruktora i innych metod potrzebne usługi, dzięki czemu nie musimy ich jawnie podawać. Oszczędza mnóstwo czasu. - -Dzięki temu przy pisaniu definicji usług możemy pominąć zdecydowaną większość argumentów. Zamiast: - -```neon -services: - articles: Model\ArticleRepository(@database, @cache.storage) -``` - -wystarczy napisać: - -```neon -services: - articles: Model\ArticleRepository -``` - -Autowiring kieruje się typami, więc aby zadziałał, klasa `ArticleRepository` musi być zdefiniowana mniej więcej tak: - -```php -namespace Model; - -class ArticleRepository -{ - public function __construct(\PDO $db, \Nette\Caching\Storage $storage) - {} -} -``` - -Autowiring nigdy nie używa nazw usług. Kieruje się wyłącznie systemem typów PHP, więc wie też, że klasa spełnia interfejsy, które implementuje, i klasy, po których dziedziczy. Dzięki temu nazwa usługi jest tylko pomocniczym identyfikatorem, a zmiana jej nazwy niczego w aplikacji nie zepsuje. - -Aby móc używać autowiringu, w kontenerze musi istnieć **dokładnie jedna usługa** każdego typu. Gdyby było ich więcej, autowiring nie wiedziałby, którą przekazać, i zgłosiłby wyjątek: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - tempDb: PDO('sqlite::memory:') - articles: Model\ArticleRepository # ZGŁASZA WYJĄTEK, pasują zarówno mainDb, jak i tempDb -``` - -Jednym z rozwiązań jest obejście autowiringu i jawne podanie nazwy usługi (np. `articles: Model\ArticleRepository(@mainDb)`). Wygodniejszym podejściem jest jednak albo [wyłączenie |#Wyłączenie autowiringu] autowiringu dla jednej z usług, albo [wskazanie |#Preferowanie w autowiringu] jednej usługi jako preferowanej. - - -Wyłączenie autowiringu ----------------------- - -Autowiring dla usługi możemy wyłączyć opcją `autowired: false`: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - - tempDb: - create: PDO('sqlite::memory:') - autowired: false # usługa tempDb jest wyłączona z autowiringu - - articles: Model\ArticleRepository # dlatego do konstruktora trafia mainDb -``` - -Usługa `articles` nie zgłosi wyjątku o dwóch pasujących usługach `PDO` (`mainDb` i `tempDb`) dostępnych dla konstruktora, bo bierze pod uwagę tylko usługę `mainDb`. - -Autowiring można też wyłączyć globalnie dla całych typów, opcją konfiguracyjną [`di › excluded` |configuration#DI], która wymienia typy (i ich potomków), które nigdy nie mają być autowirowane. - -.[note] -Konfiguracja autowiringu w Nette różni się od Symfony. W Symfony `autowire: false` oznacza, że autowiring nie ma być używany dla argumentów konstruktora usługi. W Nette autowiring dotyczy argumentów konstruktora i wszelkich innych metod wywoływanych przez kontener (jak setter injection). Opcja `autowired: false` uniemożliwia kontenerowi automatyczne przekazywanie tej instancji usługi jako zależności do innych usług. - - -Preferowanie w autowiringu --------------------------- - -Jeśli mamy kilka usług tego samego typu i dla jednej z nich podamy opcję `autowired`, usługa ta staje się preferowana: - -```neon -services: - mainDb: - create: PDO(%dsn%, %user%, %password%) - autowired: PDO # staje się preferowana - - tempDb: - create: PDO('sqlite::memory:') - - articles: Model\ArticleRepository -``` - -Usługa `articles` nie zgłosi wyjątku o kilku pasujących usługach `PDO` (`mainDb` i `tempDb`), lecz użyje preferowanej, czyli `mainDb`. - - -Kolekcja usług --------------- - -Autowiring potrafi przekazywać również tablice usług określonego typu. Ponieważ PHP natywnie nie obsługuje podawania typu elementów tablicy w deklaracjach typów, musisz uzupełnić deklarację typu `array` komentarzem phpDoc podającym typ elementu, w rodzaju `ClassName[]`: - -```php -namespace Model; - -class ShipManager -{ - /** - * @param Shipper[] $shippers - */ - public function __construct(array $shippers) - {} -} -``` - -Kontener DI przekazuje wtedy automatycznie tablicę usług odpowiadających podanemu typowi. Pomija usługi z [wyłączonym autowiringiem |#Wyłączenie autowiringu] i nigdy nie umieszcza aktualnie tworzonej usługi w jej własnej kolekcji. W odróżnieniu od przekazywania pojedynczej usługi [zawężenie |#Zawężanie autowiringu] autowiringu do konkretnego typu albo oznaczenie usługi jako [preferowanej |#Preferowanie w autowiringu] nie ma tu żadnego znaczenia - tablica zawiera zawsze wszystkie usługi danego typu. - -Typ w komentarzu może mieć również postać `array<int, Class>` albo `list<Class>`. Jeśli nie masz kontroli nad postacią komentarza phpDoc, możesz przekazać tablicę usług bezpośrednio w konfiguracji za pomocą [`typed()` |services#Funkcje specjalne]. - - -Argumenty skalarne ------------------- - -Autowiring działa tylko dla obiektów i tablic obiektów. Argumenty skalarne (np. stringi, liczby, wartości logiczne) trzeba [podać w konfiguracji |services#Argumenty]. Alternatywą jest utworzenie [obiektu ustawień|best-practices:passing-settings-to-presenters], który zamyka wartość skalarną (albo wiele wartości). Taki obiekt można potem przekazywać autowiringiem. - -```php -class MySettings -{ - public function __construct( - // readonly można używać od PHP 8.1 - public readonly bool $value, - ) - {} -} -``` - -Rejestrujesz go jako usługę, dodając do konfiguracji: - -```neon -services: - - MySettings('any value') -``` - -Inne klasy mogą go potem otrzymać przez autowiring. - - -Zależności opcjonalne ---------------------- - -Jeśli parametr konstruktora albo metody ma wartość domyślną, a w kontenerze nie ma usługi wymaganego typu, autowiring nie zgłasza wyjątku - po prostu pomija argument, więc użyta zostaje wartość domyślna. Tak deklaruje się zależności opcjonalne: - -```php -class Foo -{ - public function __construct( - private ?Logger $logger = null, - ) {} -} -``` - -Odwrotnie, przy parametrze bez wartości domyślnej brak usługi zawsze powoduje wyjątek. - - -Zawężanie autowiringu ---------------------- - -Dla poszczególnych usług autowiring można zawęzić do konkretnych klas albo interfejsów. - -Normalnie autowiring przekazuje usługę do każdego parametru metody, którego typowi usługa odpowiada. Zawężenie oznacza, że ustalamy warunki, jakie muszą spełniać typy podane dla parametrów metod, aby usługa została do nich przekazana. - -Weźmy przykład: - -```php -class ParentClass -{} - -class ChildClass extends ParentClass -{} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Gdybyśmy zarejestrowali je wszystkie jako usługi, autowiring by zawiódł: - -```neon -services: - parent: ParentClass - child: ChildClass - parentDep: ParentDependent # ZGŁASZA WYJĄTEK, pasują zarówno usługa parent, jak i child - childDep: ChildDependent # autowiring przekazuje do konstruktora usługę child -``` - -Usługa `parentDep` zgłasza wyjątek `Multiple services of type ParentClass found: child, parent`, bo do jej konstruktora pasują zarówno usługa `parent`, jak i `child`, a autowiring nie potrafi zdecydować, którą wybrać. - -Dla usługi `child` możemy więc zawęzić jej autowiring do typu `ChildClass`: - -```neon -services: - parent: ParentClass - child: - create: ChildClass - autowired: ChildClass # można też zapisać jako 'autowired: self' - - parentDep: ParentDependent # autowiring przekazuje do konstruktora usługę parent - childDep: ChildDependent # autowiring przekazuje do konstruktora usługę child -``` - -Teraz do konstruktora usługi `parentDep` przekazywana jest usługa `parent`, bo jest jedynym pasującym obiektem. Usługa `child` nie jest już tam przekazywana przez autowiring. Tak, usługa `child` nadal jest typu `ParentClass`, ale warunek zawężający `autowired: ChildClass` oznacza, że zostanie przekazana tylko do parametrów jawnie otypowanych jako `ChildClass` (albo jej podtypy). Ponieważ `ParentDependent` wymaga `ParentClass`, usługa `child` nie jest już tam brana pod uwagę jako kandydat do autowiringu. - -Dla usługi `child` `autowired: ChildClass` można też zapisać jako `autowired: self`, bo `self` jest symbolem zastępczym klasy bieżącej usługi. - -W kluczu `autowired` można też podać kilka klas albo interfejsów jako tablicę: - -```neon -autowired: [ParentClass, FooInterface] -``` - -Spróbujmy dodać do przykładu interfejsy: - -```php -interface FooInterface -{} - -interface BarInterface -{} - -class ParentClass implements FooInterface -{} - -class ChildClass extends ParentClass implements BarInterface -{} - -class FooDependent -{ - function __construct(FooInterface $obj) - {} -} - -class BarDependent -{ - function __construct(BarInterface $obj) - {} -} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Jeśli w żaden sposób nie ograniczymy usługi `child`, będzie pasować do konstruktorów wszystkich klas `FooDependent`, `BarDependent`, `ParentDependent` i `ChildDependent`, a autowiring wszędzie ją przekaże. - -Jeśli jednak zawęzimy jej autowiring do `ChildClass` przez `autowired: ChildClass` (albo `self`), autowiring przekaże ją tylko do konstruktora `ChildDependent`, bo wymaga on argumentu typu `ChildClass`, a zachodzi, że `ChildClass` *jest typu* `ChildClass`. Żaden z pozostałych wymaganych typów parametrów nie jest `ChildClass` ani jej podtypem, więc usługa nie jest do nich przekazywana. - -Jeśli ograniczymy ją do `ParentClass` przez `autowired: ParentClass`, autowiring znów przekaże ją do konstruktora `ChildDependent` (bo wymagany `ChildClass` jest podtypem `ParentClass`), a teraz również do konstruktora `ParentDependent`, bo wymagany typ `ParentClass` również pasuje. - -Jeśli ograniczymy ją do `FooInterface`, nadal będzie autowirowana do `ParentDependent` (wymagany `ParentClass` jest podtypem `FooInterface`) i `ChildDependent`, a dodatkowo do konstruktora `FooDependent`, ale nie do `BarDependent`, bo `BarInterface` nie jest podtypem `FooInterface`. - -```neon -services: - child: - create: ChildClass - autowired: FooInterface - - fooDep: FooDependent # autowiring przekazuje do konstruktora usługę child - barDep: BarDependent # ZGŁASZA WYJĄTEK, żadna usługa nie pasuje - parentDep: ParentDependent # autowiring przekazuje do konstruktora usługę child - childDep: ChildDependent # autowiring przekazuje do konstruktora usługę child -``` diff --git a/dependency-injection/pl/compilation-internals.texy b/dependency-injection/pl/compilation-internals.texy deleted file mode 100644 index f89d5478db..0000000000 --- a/dependency-injection/pl/compilation-internals.texy +++ /dev/null @@ -1,222 +0,0 @@ -Kompilacja w szczegółach -************************ - -.[perex] -Ta strona otwiera kompilację kontenera: fazy, przez które przechodzi, kiedy rozwijane są parametry konfiguracji, kiedy stringi `@service` zamieniają się w prawdziwe referencje i - to pytanie autorzy rozszerzeń zadają najczęściej - w której fazie można bezpiecznie wyszukiwać usługi po typie. To głębszy towarzysz strony [Tworzenie rozszerzeń |extensions]. - -Nic z tego nie jest potrzebne do napisania zwykłej aplikacji ani nawet zwykłego rozszerzenia. Gdy jednak Twoje rozszerzenie zaczyna badać albo przekształcać graf usług, wszystkim staje się moment: to samo wywołanie `getByType()` daje w jednej fazie wiarygodną odpowiedź, a w innej mylącą. Ta strona wyjaśnia dlaczego, abyś zawsze wiedział, gdzie Twój kod należy. - - -Dwa światy: kompilacja kontra czas działania -============================================ - -Najważniejsze do zrozumienia jest to, że kontener Nette **nie jest składany przy każdym żądaniu**. Budowany jest raz do zoptymalizowanej klasy PHP, klasa ta zapisywana jest na dysku, a każde kolejne żądanie jedynie `include`'uje gotowy plik. Cała maszyneria opisana poniżej - rozszerzenia, resolvery, generator kodu - działa **wyłącznie podczas (re)kompilacji**. - -Dzieli to świat na dwie reprezentacje, które nigdy nie współistnieją: - -| | podczas kompilacji | w czasie działania -|---|---|--- -| Co istnieje | **definicje** (przepisy) w `ContainerBuilder` | **instancje** usług w `Container` -| Kluczowe klasy | `Compiler`, `ContainerBuilder`, `Resolver`, `PhpGenerator` | `Container` (rodzic wygenerowanej klasy) -| `%param%`, `@service` | markery tekstowe, wciąż tłumaczone | już przetłumaczone / wpieczone w kod - -Wygenerowana klasa rozszerza `Nette\DI\Container` i ma metodę `createServiceXxx()` dla każdej usługi. Jej parametry i metadane autowiringu są wyliczone z góry, więc w czasie działania nie ma już nic do rozwiązania - trzeba tylko na żądanie tworzyć instancje usług. - -.[note] -W trybie deweloperskim kontener przebudowywany jest automatycznie za każdym razem, gdy zmieni się plik konfiguracyjny albo klasa rozszerzenia; oba są śledzone jako zależności. Na produkcji kompilowany jest raz i nigdy więcej sprawdzany, i stąd bierze się szybkość. - - -Fazy w skrócie -============== - -Kompilacją dyryguje `Compiler::compile()` i sprowadza się ona do trzech kroków: - -```php -public function compile(): string -{ - $this->processExtensions(); // FAZA A: schematy + loadConfiguration() - $this->processBeforeCompile(); // FAZA B: resolve + beforeCompile() + complete - return $this->generateCode(); // FAZA C: generowanie kodu + afterCompile() -} -``` - -Cały model myślowy mieści się w jednej idei - **każda faza wie więcej niż poprzednia:** - -- **Faza A** wypełnia graf definicjami. Typy usług **nie są jeszcze wiarygodnie znane**, bo typ może pochodzić z wartości zwracanej fabryki, do której nikt jeszcze nie zajrzał. -- **Faza B** najpierw rozwiązuje wszystkie typy (`resolve`), potem pozwala rozszerzeniom przekształcić graf (`beforeCompile`), a na końcu [autowiruje |autowiring] argumenty (`complete`). -- **Faza C** zamienia gotowy graf w PHP i pozwala rozszerzeniom dotknąć wygenerowanego kodu. - -Ta rosnąca wiedza jest dokładnie powodem, dla którego ta sama operacja jest w jednej fazie bezpieczna, a w innej niewiarygodna. Reszta tej strony przechodzi przez fazy z tą ideą w tle. - - -Faza A: rejestrowanie definicji -=============================== - -W tej fazie Nette wywołuje na każdym rozszerzeniu trzy metody - `getConfigSchema()`, potem `setConfig()`, potem `loadConfiguration()` - ale w **starannie kontrolowanej kolejności**, bo tutaj kolejność naprawdę ma znaczenie. - - -Dlaczego kolejność ma znaczenie -------------------------------- - -- **`ParametersExtension` i `ExtensionsExtension` idą pierwsze.** Pierwsze musi zadziałać przed wszystkim innym, aby móc rozwinąć `%param%` w całej konfiguracji - każde kolejne rozszerzenie otrzymuje wtedy swoją sekcję z już wypełnionymi wartościami. Drugie rejestruje kolejne rozszerzenia wymienione w sekcji `extensions:`, więc również musi istnieć, zanim przetworzone zostaną pozostałe. -- **`ServicesExtension` idzie ostatnie.** Sekcja `services:` użytkownika ma więc zawsze ostatnie słowo i może nadpisać wszystko, co ustawiły rozszerzenia. -- **`InjectExtension` przesunięte jest na sam koniec**, aby jego praca widziała setupy dodane przez wszystkie pozostałe rozszerzenia. - -Wniosek dla Ciebie: w chwili, gdy działa `loadConfiguration()` Twojego rozszerzenia, parametry są już rozwinięte, ale usług użytkownika jeszcze tam nie ma. Ten jeden fakt napędza większość poniższych reguł dotyczących momentu. - - -Zamiana services: w definicje ------------------------------ - -Sekcja `services:` użytkownika zamieniana jest na [obiekty definicji |extensions#Typy definicji] tutaj, w ostatnim kroku fazy A. Każdy wpis NEON jest normalizowany (zapisy skrócone są ujednolicane), rozpoznawany jest jego rodzaj (zwykła usługa, fabryka, akcesor, ...) i w builderze tworzona jest odpowiadająca definicja. To również pierwszy moment, w którym proste argumenty `@name` / `@Type` stają się referencjami - zobacz [niżej |#Referencje: kiedy @service staje się referencją]. - -Na końcu fazy A wszystkie definicje są obecne - każde rozszerzenie i użytkownik zarejestrowali, co chcieli - ale obraz nie jest jeszcze ostry: - -- **typy nie są rozwiązane** dla definicji, których typ pochodzi z wartości zwracanej fabryki, -- **argumenty nie są autowirowane**, -- część referencji `@service` to wciąż zwykłe stringi. - -Właśnie dlatego wyszukiwanie po typie jest tu niewiarygodne - więcej [niżej |#Badanie ContainerBuildera: kiedy jest bezpieczne]. - - -Parametry: kiedy rozwijane jest %param% -======================================= - -Jedno z dwóch sztandarowych pytań. Odpowiedź jest krótka: **raz, na samym początku fazy A, w całym drzewie konfiguracji.** - -`ParametersExtension` działa pierwsze, a jedną z pierwszych rzeczy, które robi, jest rozwinięcie symboli `%param%` - najpierw wewnątrz samych parametrów (parametr może odwoływać się do innego), potem w całej reszcie konfiguracji. Zanim więc jakiekolwiek inne rozszerzenie, w tym `ServicesExtension`, otrzyma swoją sekcję, symboli już nie ma. Rozszerzenia pracują z konkretnymi wartościami, nigdy z `%...%`. - -Gdy symbol stanowi cały string, jego wartość zwracana jest *taka, jaka jest* - łącznie z tablicami i obiektami - więc `%mailer%` może rozwinąć się w całą tablicę. Gdziekolwiek indziej sklejany jest w string, a zapis z kropką `%foo.bar%` sięga do zagnieżdżonych tablic. - - -Parametry statyczne kontra dynamiczne -------------------------------------- - -Nie każdą wartość da się wpiec w kod. Parametr, którego wartość różni się w zależności od środowiska - zmienna środowiskowa, `baseUrl` wyprowadzony z żądania - musi pozostać **dynamiczny**. Takie parametry deklarujesz przez `setDynamicParameterNames()` albo `Expect::...->dynamic()` w schemacie; więcej w [Parametry dynamiczne |application:bootstrapping#Parametry dynamiczne]. - -Parametr dynamiczny nie jest zastępowany wartością, lecz wyrażeniem, które odczyta ją *w czasie działania*. `%env.DB_HOST%` nie zamarza więc w string; staje się odczytem w czasie działania w wygenerowanym kontenerze. Wszystko inne jest statyczne i zamarza w czasie kompilacji - i stąd bierze się zwykle zaskoczenie "moja wartość z `getenv()` jest w każdym środowisku taka sama": parametr był po prostu statyczny. - -Operacją odwrotną jest **escapowanie**: aby dosłowny `%` albo `@` nie został zinterpretowany, podwaja się go (`%%`, `@@`). Nette robi to automatycznie dla parametrów, które wstrzykuje za Ciebie, więc ich wartości nigdy nie są mylone z symbolami zastępczymi ani referencjami. - - -Referencje: kiedy @service staje się referencją -=============================================== - -Drugie sztandarowe pytanie. Tłumaczenie `@service` odbywa się **w kilku krokach, w różnych fazach**, zależnie od tego, jak złożony jest string. Rzadko trzeba to prześledzić ręcznie, ale znajomość kroków wyjaśnia, dlaczego niektóre referencje rozwiązywane są wcześniej niż inne. - -- **Parsowanie (wczytanie konfiguracji).** `@service` użyty *jako encja* - czyli to, co tworzy usługę, jak w `Foo(@bar)` - staje się referencją natychmiast. `@service` użyty *jako argument* pozostaje na razie zwykłym stringiem. `@` w cudzysłowie escapowany jest do `@@`, więc liczy się jako dosłowny tekst, a nie referencja. -- **Faza A (`loadConfiguration`).** Przy przetwarzaniu definicji czysty argument `@name` albo `@Type` zamieniany jest w obiekt `Reference`. Wyłapuje to tylko proste postacie; `@service::CONST` albo `@` wewnątrz większego wyrażenia zostawiane są na później. -- **Faza B (`complete`).** Tutaj odbywa się prawdziwe "sprytne" tłumaczenie: `@service` → referencja, `@service::CONSTANT` → dosłowna stała klasowa, `@service::property` → odczyt tej właściwości, `@@x` → dosłowny tekst `@x`. - -W samym słowie *referencja* kryje się drugie tłumaczenie. `Reference` może wskazywać albo po **nazwie**, albo po **typie** (`@Namespace\Type`). Referencja po typie **nie jest jeszcze nazwą usługi** - do konkretnej nazwy rozwiązuje ją autowiring, a to dzieje się dopiero w kroku **complete**, gdy zbudowany jest indeks autowiringu. To pomost do kolejnej sekcji: wyszukiwania autowiringu są celowo odkładane, dopóki indeks nie jest gotowy. - -| Postać | Staje się referencją/wyrażeniem w | Rozwiązywana do konkretnej usługi w -|---|---|--- -| encja (`@foo` jako fabryka) | parsowaniu | complete -| argument `@foo`, `@Type` | fazie A | complete -| `@foo::CONST`, `@foo::prop` | fazie B | complete -| referencja po typie `@Type` | fazie A/B | complete (autowiring) - - -Badanie ContainerBuildera: kiedy jest bezpieczne -================================================ - -Teraz pytanie, które autorzy rozszerzeń zadają najczęściej: **w której metodzie mogę wyszukiwać usługi po typie?** Odpowiedź wynika z jednej prostej reguły dotyczącej tego, jak builder śledzi swój własny stan. - -Wyszukiwanie **po typie** (`getByType()`, `getDefinitionByType()`, `findByType()`) wymaga, aby graf usług był *rozwiązany* - każdy typ znany, indeks autowiringu zbudowany. Ilekroć więc wywołasz którąś z tych metod, a graf zmienił się od ostatniego rozwiązania, builder **rozwiązuje cały znany graf na miejscu**. Podczas samego rozwiązywania jakiekolwiek wyszukiwanie po typie jest zabronione i zgłasza `NotAllowedDuringResolvingException`. - -Wyszukiwanie **po tagu** (`findByTag()`) nie ma takiego wymogu - tagi nie zależą od typów, więc działa w **każdej fazie**. - -Faza po fazie: - -- **`loadConfiguration()` (faza A) - wyszukiwanie po typie jest niewiarygodne.** Graf jest niekompletny: rozszerzenia działające później nie zarejestrowały jeszcze swoich usług, a przede wszystkim nie ma tam `services:` użytkownika (które działa ostatnie). Wywołanie `getByType()` wprawdzie działa - wyzwala wczesne rozwiązanie częściowego grafu - ale odpowiedź pochodzi z niekompletnego obrazu, a przedwczesne rozwiązanie marnuje wysiłek. Reguła kciuka: **w `loadConfiguration()` tylko rejestruj definicje; nie wyszukuj po typie.** `findByTag()` jest w porządku. -- **`beforeCompile()` (faza B) - właściwe miejsce na badanie.** Do tej chwili istnieją **wszystkie** definicje (łącznie z tymi użytkownika), **typy są rozwiązane**, a **indeks autowiringu zbudowany**, więc `getByType()`, `findByType()` i `findByTag()` zwracają **wiarygodne** odpowiedzi. Argumenty *nie* są jeszcze autowirowane - to następny krok (`complete`), po wszystkich wywołaniach `beforeCompile()`. Gdy zmodyfikujesz tu definicję, kolejne `getByType()` przezroczyście rozwiąże graf ponownie, więc możesz swobodnie przeplatać edycje i zapytania. -- **`afterCompile()` (faza C) - tylko kod.** Działa nad wygenerowaną klasą, nie nad builderem. Graf jest gotowy; tutaj kształtujesz wynikowy PHP. - -| Chcę... | Faza -|---|--- -| zarejestrować usługę | `loadConfiguration()` -| wyszukiwać po **tagu** i modyfikować definicje | `loadConfiguration()` albo `beforeCompile()` -| wyszukiwać po **typie** (`getByType`/`findByType`) | **`beforeCompile()`** -| polegać na tym, które usługi autowiring wybrał do argumentów | nie w czasie kompilacji - zbadaj to w czasie działania -| dotknąć wygenerowanego kodu | `afterCompile()` -| uruchomić kod po starcie kontenera | [kod inicjalizacyjny |extensions#Kod inicjalizacyjny] - - -Wewnątrz fazy B: resolve i complete -=================================== - -Faza B to dwa przebiegi z wywołaniami `beforeCompile()` wciśniętymi między nie: - -```php -$this->builder->resolve(); // typy rozwiązane, indeks autowiringu zbudowany -foreach ($this->extensions as $extension) { - $extension->beforeCompile(); -} -$this->builder->complete(); // DOPIERO TERAZ autowirowane są argumenty -``` - -**`resolve()`** ustala typ każdej usługi - wzięty z zadeklarowanego `type` albo wywnioskowany z jej fabryki: typ zwracany metody fabrycznej, klasa, której instancję tworzy, albo usługa, na którą wskazuje referencja - a następnie buduje indeks autowiringu mapujący każdy typ (klasę wraz z jej rodzicami i interfejsami) na nazwę usługi. Usługa oznaczona `autowired: false` jest z indeksu pomijana; `autowired: [A, B]` zawęża typy, pod którymi jest widoczna. Co kluczowe, resolve ustala *typy*, nie *argumenty* - autowirowanie argumentów wymagałoby gotowego indeksu, który istnieje dopiero po tym przebiegu. - -**`complete()`** to miejsce, w którym faktycznie odbywa się autowirowanie argumentów. Dla każdej definicji uzupełnia brakujące argumenty konstruktora i setupu, wyszukując ich typy w gotowym już indeksie. Dlatego referencje po typie pozostawiono nierozwiązane podczas resolve: wyszukiwanie należy tutaj, gdy jest już wiarygodny indeks, w którym można szukać. - - -Faza C: generowanie kodu -======================== - -`generateCode()` przekazuje gotowy graf do `PhpGenerator`, który produkuje klasę rozszerzającą `Container`, z metodą `createServiceXxx()` na każdą usługę, plus wyliczone z góry metadane `aliases`, `tags` i `wiring`. Każdy `Statement` staje się tekstem PHP (`new Foo(...)`, wywołania metod, dostęp do właściwości), a każdy `Reference` staje się wywołaniem `$this->getService(...)`. - -Rozszerzenia dostają następnie ostatni przebieg `afterCompile()` nad wygenerowaną klasą - to tutaj emitowane są na przykład gettery parametrów statycznych i dynamicznych - a także szansę na dodanie [kodu inicjalizacyjnego |extensions#Kod inicjalizacyjny], który wykonuje się przy każdym żądaniu. - - -Oś czasu na jednym obrazku -========================== - -``` -KOMPILACJA (raz, do cache) -│ -├─ wczytanie plików konfiguracyjnych NEON -> Statement/tablica; scalenie plików -│ @ w cudzysłowie -> @@ ; encje -> Statement -│ -▼ Compiler::compile() -│ -├─ FAZA A processExtensions() -│ ├─ ParametersExtension (PIERWSZE) ── %param% ROZWINIĘTE w całej konfiguracji -│ │ dynamiczne -> wyrażenie w czasie działania -│ ├─ ExtensionsExtension (PIERWSZE) ── rejestruje kolejne rozszerzenia -│ ├─ ...pozostałe rozszerzenia... ── loadConfiguration(): tylko rejestracja definicji -│ └─ ServicesExtension (OSTATNIE) ── services: -> obiekty Definition -│ @name/@Type -> Reference -│ [graf kompletny co do liczby; TYPY i ARGUMENTY jeszcze nie; wyszukiwanie po typie niewiarygodne] -│ -├─ FAZA B processBeforeCompile() -│ ├─ builder.resolve() ── rozwiązanie wszystkich typów; budowa indeksu autowiringu -│ │ [typy gotowe; indeks gotowy] -│ ├─ beforeCompile() rozszerzeń ── tutaj getByType/findByType/findByTag są BEZPIECZNE -│ │ (argumenty jeszcze nie autowirowane) -│ └─ builder.complete() ── autowirowanie ARGUMENTÓW; dokończenie tłumaczenia referencji -│ referencje po typie -> nazwy usług -│ -└─ FAZA C generateCode() - ├─ PhpGenerator.generate() ── Statement -> PHP; metody createServiceXxx() - ├─ afterCompile() rozszerzeń ── poprawki kodu; emisja getterów parametrów - └─ toString() ── ostateczny kod PHP -> cache - -──────────────────────────────────────────────────────────── - -CZAS DZIAŁANIA (każde żądanie) -│ -├─ new Container($dynamicParams) -├─ initialize() ── kod startowy rozszerzeń (sesja, nagłówki, walidacja) -└─ getService()/getByType() ── leniwe instancje z wyliczonych metadanych -``` - - -Częste nieporozumienia -====================== - -- "W `loadConfiguration()` wyszukam usługi po typie." Nie - graf jest niekompletny (`services:` użytkownika działa po Tobie), a `getByType()` wyzwala przedwczesne rozwiązanie częściowego grafu. Przenieś to do `beforeCompile()`. `findByTag()` jest w porządku nawet tutaj. -- "Wartość z `getenv()` w parametrze będzie inna w każdym środowisku." Tylko jeśli parametr jest dynamiczny. W przeciwnym razie zostaje wpieczona w czasie kompilacji i wszędzie jest taka sama. -- "Referencja `@Type` jest już nazwą usługi." Nie jest - to referencja po typie, rozwiązywana do konkretnej nazwy przez autowiring dopiero w kroku complete. -- "Moje rozszerzenie czyta plik pomocniczy, ale zmiany się nie pojawiają." Zarejestruj go przez `$builder->addDependency($file)`, w przeciwnym razie cache o nim nie wie i nie przebuduje się. -- "Podczas `resolve()` mogę wywołać `getByType()`." Nie - zgłasza `NotAllowedDuringResolvingException`. Wyszukiwanie po typie należy do `beforeCompile()` albo później, nigdy w środku rozwiązywania. diff --git a/dependency-injection/pl/configuration.texy b/dependency-injection/pl/configuration.texy deleted file mode 100644 index dbd47e6fe8..0000000000 --- a/dependency-injection/pl/configuration.texy +++ /dev/null @@ -1,335 +0,0 @@ -Konfiguracja kontenera DI -************************* - -.[perex] -Przegląd opcji konfiguracyjnych kontenera Nette DI. - - -Plik konfiguracyjny -=================== - -Kontenerem Nette DI łatwo steruje się za pomocą plików konfiguracyjnych. Zapisywane są one zwykle w [formacie NEON|neon:format]. Zalecamy używanie [edytorów z jego obsługą |tools:ide]. - -<pre> -"decorator .[prism-token prism-atrule]":[#Dekorator]: "Dekorator .[prism-token prism-comment]"<br> -"di .[prism-token prism-atrule]":[#DI]: "Kontener DI .[prism-token prism-comment]"<br> -"extensions .[prism-token prism-atrule]":[#Rozszerzenia]: "Instalacja dodatkowych rozszerzeń DI .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#Dołączanie plików]: "Dołączanie plików .[prism-token prism-comment]"<br> -"parameters .[prism-token prism-atrule]":[#Parametry]: "Parametry .[prism-token prism-comment]"<br> -"search .[prism-token prism-atrule]":[#Search]: "Automatyczna rejestracja usług .[prism-token prism-comment]"<br> -"services .[prism-token prism-atrule]":[services]: "Usługi .[prism-token prism-comment]" -</pre> - -.[note] -Aby zapisać string zawierający znak `%`, musisz go zescapować, podwajając na `%%`. - - -Parametry -========= - -W konfiguracji możesz zdefiniować parametry, których można potem używać jako części definicji usług. Pozwala to uczynić konfigurację czytelniejszą albo scentralizować wartości, które mogą się zmieniać. - -```neon -parameters: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: secret -``` - -Do parametru `dsn` odwołujemy się w dowolnym miejscu konfiguracji zapisem `%dsn%`. Parametrów można używać również wewnątrz stringów, jak `'%wwwDir%/images'`. - -Parametry nie muszą być tylko stringami czy liczbami, mogą zawierać również tablice: - -```neon -parameters: - mailer: - host: smtp.example.com - secure: ssl - user: franta@gmail.com - languages: [cs, en, de] -``` - -Do konkretnego klucza odwołujemy się jako `%mailer.user%`. - -Jeśli Twój kod (np. klasa) potrzebuje wartości parametru, przekaż mu ją. Na przykład w konstruktorze. Nie istnieje globalny obiekt konfiguracji, którego klasy mogłyby pytać o wartości parametrów. Byłoby to złamanie zasady wstrzykiwania zależności. - - -Usługi -====== - -Zobacz [osobny rozdział|services]. - - -Dekorator -========= - -Jak zmodyfikować naraz wiele usług określonego typu? Na przykład jak wywołać konkretną metodę na wszystkich presenterach dziedziczących po określonej klasie bazowej? Od tego jest dekorator. - -```neon -decorator: - # dla wszystkich usług będących instancjami tej klasy albo interfejsu - App\Presentation\BasePresenter: - setup: - - setProjectId(10) # wywołaj tę metodę - - $absoluteUrls = true # i ustaw zmienną -``` - -Dekoratora można też używać do ustawiania [tagów |services#Tagi] albo włączania [trybu inject |services#Tryb inject]. - -```neon -decorator: - InjectableInterface: - tags: [mytag: 1] - inject: true -``` - - -DI -=== - -Ustawienia techniczne kontenera DI. - -```neon -di: - # pokazywać DIC w pasku Tracy? - debugger: ... # (bool) domyślnie autodetekcja (włączone, gdy Tracy jest obecne) - - # typy parametrów, których nigdy nie autowirujesz - excluded: ... # (string[]) - - # włączyć leniwe tworzenie usług? - lazy: ... # (bool) domyślnie false - - # klasa, po której dziedziczy kontener DI - parentClass: ... # (string) domyślnie Nette\DI\Container -``` - - -Usługi leniwe .{data-version:3.2.4} ------------------------------------ - -Ustawienie `lazy: true` aktywuje leniwe (odroczone) tworzenie usług. Oznacza to, że usługi nie są faktycznie tworzone w chwili, gdy prosisz o nie kontener DI, lecz dopiero przy ich pierwszym użyciu. Może to przyspieszyć start aplikacji i zmniejszyć zużycie pamięci, bo tworzone są tylko usługi faktycznie potrzebne dla danego żądania. - -Dla konkretnej usługi leniwe tworzenie można [dostosować |services#Usługi leniwe]. - -.[note] -Obiektów leniwych można używać tylko dla klas zdefiniowanych przez użytkownika, nie dla wewnętrznych klas PHP. Wymaga PHP 8.4 lub nowszego. - - -Eksport metadanych ------------------- - -Klasa kontenera DI zawiera również sporo metadanych. Możesz zmniejszyć jej rozmiar, ograniczając eksport metadanych. - -```neon -di: - export: - # eksportować parametry? - parameters: false # (bool) domyślnie true - - # eksportować tagi i które? - tags: # (string[]|bool) domyślnie wszystkie - - event.subscriber - - # eksportować dane do autowiringu i które? - types: # (string[]|bool) domyślnie wszystkie - - Nette\Database\Connection - - Symfony\Component\Console\Application -``` - -Jeśli nie używasz `$container->getParameters()`, możesz wyłączyć eksport parametrów. Ponadto możesz eksportować tylko te tagi, których faktycznie używasz do pobierania usług przez `$container->findByTag(...)`. Jeśli w ogóle nie wywołujesz tej metody, możesz całkowicie wyłączyć eksport tagów przez `false`. - -Możesz znacząco ograniczyć metadane dla [autowiringu|autowiring], wymieniając tylko te klasy, o które faktycznie prosisz przez `$container->getByType()`. Znów: jeśli nie wywołujesz tej metody (albo wywołujesz ją tylko w pliku [bootstrap|application:bootstrapping], np. aby uzyskać `Nette\Application\Application`), możesz całkowicie wyłączyć eksport typów przez `false`. - - -Rozszerzenia -============ - -Rejestracja dodatkowych rozszerzeń DI. Tak dodasz na przykład rozszerzenie DI `Dibi\Bridges\Nette\DibiExtension3` pod nazwą `dibi`: - -```neon -extensions: - dibi: Dibi\Bridges\Nette\DibiExtension3 -``` - -Konfigurujesz je potem w sekcji `dibi`: - -```neon -dibi: - host: localhost -``` - -Jako rozszerzenie możesz też dodać klasę z parametrami: - -```neon -extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, [%appDir%], %tempDir%/cache) -``` - - -Dołączanie plików -================= - -Kolejne pliki konfiguracyjne można dołączyć w sekcji `includes`: - -```neon -includes: - - parameters.php - - services.neon - - presenters.neon -``` - -Nazwa `parameters.php` nie jest literówką; konfigurację można zapisać także w pliku PHP, który zwraca ją jako tablicę: - -```php -<?php -return [ - 'database' => [ - 'main' => [ - 'dsn' => 'sqlite::memory:', - ], - ], -]; -``` - -Jeśli elementy o tych samych kluczach pojawią się w kilku plikach konfiguracyjnych, zostaną nadpisane albo, w przypadku tablic, [scalone |#Scalanie]. Plik dołączony później ma wyższy priorytet niż poprzedni. Plik, w którym wymieniona jest sekcja `includes`, ma wyższy priorytet niż pliki w nim dołączone. - - -Search -====== - -Automatyczna rejestracja usług w kontenerze DI znacząco upraszcza pracę. Nette automatycznie dodaje do kontenera presentery, ale równie łatwo dodasz dowolne inne klasy. - -Wystarczy podać, w których katalogach (i podkatalogach) mają być wyszukiwane klasy: - -```neon -search: - - in: %appDir%/Forms - - in: %appDir%/Model -``` - -Jeśli potrzebujesz tylko jednej reguły wyszukiwania, możesz pominąć listę i zapisać jej klucze bezpośrednio pod `search`: - -```neon -search: - in: %appDir% -``` - -Zwykle jednak nie chcemy dodawać absolutnie wszystkich klas i interfejsów, więc możemy je filtrować: - -```neon -search: - - in: %appDir%/Forms - - # filtrowanie po nazwie pliku (string|string[]) - files: - - *Factory.php - - # filtrowanie po nazwie klasy (string|string[]) - classes: - - *Factory -``` - -Albo możemy wybrać klasy, które dziedziczą po co najmniej jednej z wymienionych klas albo ją implementują: - - -```neon -search: - - in: %appDir% - extends: - - App\*Form - implements: - - App\*FormInterface -``` - -Możesz też zdefiniować reguły wykluczające za pomocą masek nazw klas albo przodków. Jeśli klasa pasuje do reguły wykluczającej, nie zostanie dodana do kontenera DI: - -```neon -search: - - in: %appDir% - exclude: - files: ... - classes: ... - extends: ... - implements: ... -``` - -Wszystkim automatycznie zarejestrowanym usługom można przypisać tagi: - -```neon -search: - - in: %appDir% - tags: ... -``` - -Poza klasami search rejestruje również interfejsy mające jedną metodę `create()` albo `get()` - jako [generowane fabryki albo akcesory |factory]. Klasy, dla których w kontenerze zarejestrowana jest już usługa tego samego typu, są pomijane, więc nie powstają duplikaty. - - -Scalanie -======== - -Jeśli elementy o tych samych kluczach pojawią się w kilku plikach konfiguracyjnych, zostaną nadpisane albo, w przypadku tablic, scalone. Plik dołączony później ma wyższy priorytet niż poprzedni. - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>wynik</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> - <td> -```neon -items: - - 1 - - 2 - - 3 -``` - </td> -</tr> -</table> - -Dla tablic scalaniu można zapobiec, dodając po nazwie klucza wykrzyknik: - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>wynik</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items!: - - 3 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> -</tr> -</table> - -{{maintitle: Konfiguracja wstrzykiwania zależności}} diff --git a/dependency-injection/pl/container.texy b/dependency-injection/pl/container.texy deleted file mode 100644 index 0a2b376a75..0000000000 --- a/dependency-injection/pl/container.texy +++ /dev/null @@ -1,142 +0,0 @@ -Czym jest kontener DI? -********************** - -.[perex] -Kontener wstrzykiwania zależności (DIC albo kontener DI) to obiekt odpowiedzialny za tworzenie instancji i konfigurowanie innych obiektów (nazywanych usługami). - -Może Cię to zaskoczyć, ale w wielu przypadkach nie potrzebujesz kontenera wstrzykiwania zależności, aby korzystać z zalet wstrzykiwania zależności (w skrócie DI). Przecież nawet w [rozdziale wprowadzającym|introduction] pokazywaliśmy konkretne przykłady DI i żaden kontener nie był potrzebny. - -Gdy jednak zarządzasz dużą liczbą obiektów o złożonych zależnościach, kontener DI staje się bardzo przydatny. Tak bywa często w aplikacjach webowych zbudowanych na frameworku. - -W poprzednim rozdziale przedstawiliśmy klasy `Article` i `EditController`. Obie mają zależności, mianowicie bazę danych i fabrykę `ArticleFactory`. I dla tych klas utworzymy teraz kontener. Oczywiście tworzenie kontenera dla tak prostego przykładu to przesada. Utworzymy go jednak, aby pokazać, jak wygląda i jak działa. - -Oto prosty, zapisany na sztywno kontener dla powyższego przykładu: - -```php -class Container -{ - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection('mysql:', 'root', '***'); - } - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->createDatabase()); - } - - public function createEditController(): EditController - { - return new EditController($this->createArticleFactory()); - } -} -``` - -Użycie wyglądałoby tak: - -```php -$container = new Container; -$controller = $container->createEditController(); -``` - -Po prostu prosimy kontener o obiekt, nie musząc wiedzieć, jak go utworzyć ani jakie ma zależności; kontener zajmuje się tym wszystkim. Zależności wstrzykiwane są przez kontener automatycznie. Na tym polega jego siła. - -Na razie kontener ma wszystkie informacje zapisane na sztywno. Zróbmy więc kolejny krok i dodajmy parametry, aby kontener stał się naprawdę użyteczny: - -```php -class Container -{ - public function __construct( - private array $parameters, - ) { - } - - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection( - $this->parameters['db.dsn'], - $this->parameters['db.user'], - $this->parameters['db.password'], - ); - } - - // ... -} - -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); -``` - -Spostrzegawczy czytelnicy mogą zauważyć problem. Za każdym razem, gdy pobieramy obiekt `EditController`, tworzone są również nowe instancje `ArticleFactory` i połączenia z bazą danych. Zdecydowanie tego nie chcemy. - -Dodamy więc metodę `getService()`, która zawsze zwróci te same instancje: - -```php -class Container -{ - private array $services = []; - - public function __construct( - private array $parameters, - ) { - } - - public function getService(string $name): object - { - if (!isset($this->services[$name])) { - // getService('Database') wywoła createDatabase() - $method = 'create' . $name; - $this->services[$name] = $this->$method(); - } - return $this->services[$name]; - } - - // ... -} -``` - -Przy pierwszym wywołaniu, na przykład `$container->getService('Database')`, wywoła `createDatabase()`, aby utworzyć obiekt bazy danych, zapisze go do tablicy `$services` i zwróci. Przy kolejnych wywołaniach zwraca bezpośrednio już zapisaną instancję. - -Modyfikujemy również resztę kontenera, aby używała `getService()`: - -```php -class Container -{ - // ... - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->getService('Database')); - } - - public function createEditController(): EditController - { - return new EditController($this->getService('ArticleFactory')); - } -} -``` - -Nawiasem mówiąc, terminem usługa określamy dowolny obiekt zarządzany przez kontener. Stąd nazwa metody `getService()`. - -Gotowe. Mamy w pełni działający kontener DI! I możemy go używać: - -```php -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); - -$controller = $container->getService('EditController'); -$database = $container->getService('Database'); -``` - -Jak widzisz, napisanie DIC nie jest trudne. Warto zauważyć, że same obiekty nie wiedzą, że tworzy je kontener. W konsekwencji można w ten sposób tworzyć dowolny obiekt PHP bez modyfikowania jego kodu źródłowego. - -Ręczne tworzenie i utrzymywanie klasy kontenera może szybko stać się koszmarem. Dlatego w kolejnym rozdziale omówimy [Nette DI Container|nette-container], który potrafi generować się i aktualizować niemal automatycznie. - - -{{maintitle: Czym jest kontener wstrzykiwania zależności?}} diff --git a/dependency-injection/pl/extensions.texy b/dependency-injection/pl/extensions.texy deleted file mode 100644 index 48d28d5991..0000000000 --- a/dependency-injection/pl/extensions.texy +++ /dev/null @@ -1,443 +0,0 @@ -Tworzenie rozszerzeń dla Nette DI -********************************* - -.[perex] -Rozszerzenie to klasa, która wpina się w kompilację kontenera DI. Może rejestrować usługi programowo, walidować własną sekcję konfiguracji, modyfikować usługi zdefiniowane przez innych, a nawet zmieniać wygenerowany kod kontenera. Ta strona uczy, jak takie rozszerzenie napisać, co dzieje się kiedy i na co uważać. - -Rozszerzenia to natywny sposób, w jaki pakiety integrują się z Nette: używają ich wszystkie pakiety `nette/*`, a Twój też może. Typowe rozszerzenie robi jedną albo więcej z tych rzeczy: - -- **integruje bibliotekę** - rejestruje jej usługi w kontenerze i udostępnia przyjazną, walidowaną sekcję konfiguracji (stąd biorą się sekcje `mail:` czy `database:`) -- **automatyzuje rejestrację** - rejestruje wiele podobnych usług w pętli albo według reguły, gdzie wypisywanie ich w `services:` byłoby żmudne -- **wprowadza zmiany przekrojowe** - znajduje usługi zarejestrowane przez innych i je uzupełnia, np. podpina logger do każdej usługi z określonym tagiem - -Do codziennej pracy nad aplikacją rzadko go potrzebujesz - sekcja [services |services] konfiguracji wystarcza do rejestrowania i łączenia Twoich klas. Po rozszerzenie sięgnij wtedy, gdy sama konfiguracja przestaje wystarczać. - -Rozszerzenie aktywuje się w sekcji `extensions`. Tak dodasz rozszerzenie reprezentowane przez klasę `BlogExtension` pod nazwą `blog`: - -```neon -extensions: - blog: BlogExtension -``` - -Jeśli jego konstruktor przyjmuje argumenty, przekaż je od razu tam: - -```neon -extensions: - blog: BlogExtension(%debugMode%) -``` - - -Jak działa kompilacja -===================== - -Aby pewnie pisać rozszerzenia, musisz wiedzieć jedną kluczową rzecz: **kiedy Twój kod działa**. Nette nie łączy usług podczas obsługi żądań. Zamiast tego *kompiluje* kontener z wyprzedzeniem: wczytuje wszystkie pliki konfiguracyjne, pozwala rozszerzeniom wykonać swoją pracę i generuje zoptymalizowaną klasę PHP, którą zapisuje na dysku. Każde kolejne żądanie po prostu wczytuje tę gotową klasę. Kod Twojego rozszerzenia działa więc tylko wtedy, gdy kontener jest (prze)budowywany, a nie przy każdym żądaniu. - -Ma to ważną konsekwencję: podczas kompilacji nie istnieją jeszcze żadne usługi. Istnieją **definicje** - przepisy opisujące, jakiej klasy będzie każda usługa, jak ją utworzyć i co następnie na niej wywołać. Definicje żyją w obiekcie [ContainerBuilder |#ContainerBuilder]. Rozszerzenie to w istocie *skryptowalna konfiguracja*: wszystko, co możesz zadeklarować w sekcji `services:`, możesz też zbudować w PHP - warunkowo, w pętlach albo w reakcji na to, co zarejestrowali inni. - -Kompilacja przebiega w fazach, a rozszerzenie może wkroczyć w każdą z nich: - -1) walidowane są sekcje konfiguracji wszystkich rozszerzeń (`getConfigSchema()`) -2) każde rozszerzenie rejestruje swoje usługi (`loadConfiguration()`); sekcja `services:` użytkownika przetwarzana jest ostatnia, więc aplikacja ma zawsze ostatnie słowo -3) gdy wszystkie definicje są na miejscu, a typy usług rozwiązane, rozszerzenia mogą je modyfikować (`beforeCompile()`) -4) generowana jest klasa kontenera; rozszerzenia mogą jeszcze dostosować jej kod (`afterCompile()`) i wyemitować kod, który wykona się przy starcie aplikacji ([inicjalizacja |#Kod inicjalizacyjny]) - -.[note] -W trybie deweloperskim kontener rekompiluje się automatycznie, gdy tylko zmienisz plik konfiguracyjny albo samą klasę rozszerzenia - oba są śledzone jako zależności. Możesz więc rozwijać rozszerzenia, nigdy nie czyszcząc cache. - -.[tip] -Głębsze spojrzenie na to, co dzieje się w każdej fazie - kiedy rozwijane są parametry, kiedy `@service` staje się referencją i dokładnie kiedy bezpiecznie jest wyszukiwać usługi po typie - znajdziesz w [Kompilacja w szczegółach |compilation-internals]. - - -Pierwsze rozszerzenie -===================== - -Oto małe, ale kompletne rozszerzenie. Aktywujemy je i konfigurujemy w tym samym pliku: - -```neon -extensions: - blog: BlogExtension - -blog: - postsPerPage: 5 -``` - -A oto cała klasa: - -```php -use Nette\Schema\Expect; - -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function getConfigSchema(): Nette\Schema\Schema - { - return Expect::structure([ - 'postsPerPage' => Expect::int(10), - 'allowComments' => Expect::bool(true), - ]); - } - - - public function loadConfiguration(): void - { - $builder = $this->getContainerBuilder(); - - $builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class, ['postsPerPage' => $this->config->postsPerPage]); - - if ($this->config->allowComments) { - $builder->addDefinition($this->prefix('comments')) - ->setFactory(Blog\Comments::class); - } - } -} -``` - -`getConfigSchema()` opisuje, co może zawierać sekcja `blog:` (nazwana według klucza, pod którym zarejestrowaliśmy rozszerzenie), wraz z typami i wartościami domyślnymi - zwalidowane wartości dostępne są potem w `$this->config`. W `loadConfiguration()` rejestrujemy usługi. Zwróć uwagę na nazwy: `$this->prefix('articles')` daje `blog.articles`, więc usługi różnych rozszerzeń nie mogą się zderzyć. - -A ostatnie kilka wierszy pokazuje, po co w ogóle istnieją rozszerzenia: usługa `comments` rejestrowana jest tylko wtedy, gdy komentarze są włączone. Zwykły plik konfiguracyjny takich decyzji podjąć nie potrafi. - -Usługi zarejestrowane w ten sposób zachowują się dokładnie tak, jakby były zapisane w `services:` - tworzone są leniwie na żądanie, a autowiring przekazuje je wszędzie tam, gdzie zadeklarowano typ `Blog\Articles`. - -Kolejne rozdziały szczegółowo opisują cykl życia rozszerzenia, następnie API [ContainerBuildera |#ContainerBuilder], którego będziesz używać wewnątrz rozszerzenia, a na końcu [pułapki |#Wskazówki i pułapki], o których warto wiedzieć. - - -Cykl życia rozszerzenia -======================= - -Rozszerzenie dziedziczy po [api:Nette\DI\CompilerExtension] i nadpisuje niektóre z czterech metod `getConfigSchema()`, `loadConfiguration()`, `beforeCompile()` i `afterCompile()`, które kompilator wywołuje podczas kompilacji w tej właśnie kolejności. - - -getConfigSchema(): Nette\Schema\Schema .[method] ------------------------------------------------- - -Definiuje schemat sekcji konfiguracji rozszerzenia. Dzięki niemu użytkownicy dostają za darmo walidację i czytelne komunikaty o błędach: literówka albo zły typ w sekcji `blog:` zgłaszane są zrozumiałym komunikatem, bez pisania przez Ciebie choćby jednego sprawdzenia. - -Schemat opisuje się za pomocą biblioteki [Schema |schema:] i może wyrażać typy, wartości domyślne, wartości dozwolone i wiele więcej: - -```php -public function getConfigSchema(): Nette\Schema\Schema -{ - return Expect::structure([ - 'postsPerPage' => Expect::int(10), - 'storage' => Expect::anyOf('files', 'database')->firstIsDefault(), - ]); -} -``` - -Zwalidowana konfiguracja dostępna jest w `$this->config` jako obiekt `stdClass` (albo jako tablica, jeśli dopiszesz do schematu `castTo('array')`). - -Jeśli wartości opcji nie da się poznać w czasie kompilacji - bo pochodzi na przykład ze zmiennej środowiskowej - oznacz ją przez `dynamic()`, np. `Expect::int()->dynamic()`. Więcej w [parametrach dynamicznych |application:bootstrapping#Parametry dynamiczne]. - - -loadConfiguration() .[method] ------------------------------ - -Miejsce, w którym rozszerzenie rejestruje swoje usługi, używając [ContainerBuildera |#ContainerBuilder]: - -```php -public function loadConfiguration(): void -{ - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class); -} -``` - -Jeśli usługa ma być dostępna również pod krótką nazwą, dodaj alias. Zwyczajowo robi się to tylko wtedy, gdy rozszerzenie zarejestrowano pod jego zwykłą nazwą, aby kilka instancji rozszerzenia nie mogło się o nią bić: - -```php -if ($this->name === 'blog') { - $builder->addAlias('articles', $this->prefix('articles')); -} -``` - -Gdy usług jest wiele, wygodniej bywa zdefiniować je w osobnym pliku NEON, znajomą składnią [services |services]. Prefiks `@extension` odwołuje się do bieżącego rozszerzenia: - -```neon -services: - articles: - create: MyBlog\ArticlesModel(@connection) - - comments: - create: MyBlog\CommentsModel(@connection, @extension.articles) -``` - -Definicje te wczytujemy metodą `loadDefinitionsFromConfig()`; nazwy dostają prefiks automatycznie, a plik śledzony jest jako zależność, więc jego zmiana wyzwala rekompilację: - -```php -public function loadConfiguration(): void -{ - $this->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/services.neon')['services'], - ); -} -``` - - -beforeCompile() .[method] -------------------------- - -Gdy wywoływana jest ta metoda, builder zawiera już **wszystkie** definicje: Twoje, innych rozszerzeń i te z plików konfiguracyjnych użytkownika. Typy usług są też rozwiązane, więc wyszukiwanie po typie jest wiarygodne. Czyni to tę fazę idealną do badania i uzupełniania ostatecznego grafu usług. - -Typowo wyszukujesz usługi po tagu albo po typie i uzupełniasz znalezione definicje: - -```php -public function beforeCompile(): void -{ - $builder = $this->getContainerBuilder(); - - foreach ($builder->findByTag('logaware') as $name => $attrs) { - $builder->getDefinition($name)->addSetup('setLogger'); - } -} -``` - -Wywołanie `setLogger()` nie ma jawnych argumentów - dostarczy je autowiring, tak samo jak robi to w fabrykach. - -Możesz też współpracować z innymi zarejestrowanymi rozszerzeniami, pozyskanymi przez `$this->compiler->getExtensions()`, opcjonalnie przefiltrowanymi po klasie albo interfejsie: - -```php -foreach ($this->compiler->getExtensions(FooExtension::class) as $extension) { - // ... -} -``` - - -afterCompile(Nette\PhpGenerator\ClassType $class) .[method] ------------------------------------------------------------ - -W ostatniej fazie generowana jest klasa kontenera jako obiekt [ClassType |php-generator:#Klasy] z biblioteki [PHP Generator |php-generator:]. Zawiera metodę fabryczną dla każdej usługi i za chwilę zostanie zapisana do cache. Możesz jeszcze zmodyfikować jej kod: - -```php -public function afterCompile(Nette\PhpGenerator\ClassType $class): void -{ - $method = $class->getMethod('__construct'); - // ... -} -``` - -Ta faza będzie Ci potrzebna tylko rzadko. Aby dodać kod, który wykona się przy starcie aplikacji, użyj zamiast tego inicjalizacji: - - -Kod inicjalizacyjny -------------------- - -Wszystkie poprzednie fazy wpływają na to, jak kontener jest *budowany*. Rozszerzenie może ponadto wyemitować kod, który wykona się *w czasie działania*, zaraz po utworzeniu kontenera - na przykład aby wystartować sesję albo uruchomić usługi. Kod zapisuje się do obiektu `$this->initialization` jego metodą [addBody() |php-generator:#Ciała metod i funkcji]: - -```php -public function loadConfiguration(): void -{ - // usługi z tagiem 'run' muszą zostać utworzone zaraz po starcie kontenera - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $attrs) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } -} -``` - -Samo Nette używa inicjalizacji na przykład do automatycznego wystartowania sesji albo wysłania nagłówków HTTP związanych z bezpieczeństwem. I miej na uwadze: w odróżnieniu od wszystkiego innego w rozszerzeniu, ten kod wykonuje się przy **każdym żądaniu**, więc trzymaj go niewielkim. - - -ContainerBuilder -================ - -[api:Nette\DI\ContainerBuilder] to obiekt, przez który rozszerzenie rozmawia z kompilatorem. Zawiera [definicje |#Jak działa kompilacja] wszystkich usług i oferuje metody do ich dodawania, wyszukiwania i modyfikowania. Pozyskujesz go w `loadConfiguration()` i `beforeCompile()`: - -```php -$builder = $this->getContainerBuilder(); -``` - - -Dodawanie usług ---------------- - -Rejestracja usługi to to samo, co robisz w sekcji `services:` pliku NEON, tyle że zapisane w PHP. Każdemu kluczowi konfiguracji odpowiada metoda na definicji, więc te dwa zapisy są równoważne: - -```neon -services: - articles: - create: Blog\Articles(@connection) - setup: - - setLogger(@logger) - tags: [logaware] -``` - -```php -$builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class, ['@connection']) - ->addSetup('setLogger', ['@logger']) - ->addTag('logaware'); -``` - -Definicja zwracana przez `addDefinition()` to [ServiceDefinition |#Typy definicji] oferujący odpowiedniki kluczy konfiguracji: `setType()` (klasa usługi), `setFactory()` (jak ją utworzyć), `setArguments()`, `addSetup()`, `addTag()` i `setAutowired()`. - -`addSetup()` odzwierciedla listę `setup:` i przyjmuje te same postacie: wywołanie metody `addSetup('setLogger', ['@logger'])`, przypisanie do właściwości `addSetup('$cache', ['@cache'])` albo wywołanie na innej usłudze `addSetup('@Tracy\Bar::addPanel', [$panel])`. - -Poza zwykłymi usługami builder potrafi rejestrować również [generowane |factory] fabryki, akcesory i lokatory - każdy własną metodą zwracającą odpowiadający [typ definicji |#Typy definicji]: - -| Metoda | Rejestruje -|--------|---------- -| `addDefinition()` | zwykłą usługę (zwraca `ServiceDefinition`) -| `addFactoryDefinition()` | generowaną [fabrykę |factory] (interfejs z metodą `create()`) -| `addAccessorDefinition()` | generowany [akcesor |factory#Akcesor] (interfejs z metodą `get()`) -| `addLocatorDefinition()` | [multifabrykę / lokator |factory#Multifabryka/akcesor] łączący kilka fabryk -| `addImportedDefinition()` | usługę przekazywaną do kontenera z zewnątrz w czasie działania -| `addAlias()` | drugą nazwę istniejącej usługi - -Przy fabryce obiekt, który tworzy, konfigurujesz przez `getResultDefinition()`; akcesor zamiast tego wskazuje na istniejącą usługę przez `setReference()`: - -```php -$builder->addFactoryDefinition($this->prefix('latteFactory')) - ->setImplement(LatteFactory::class) - ->getResultDefinition() - ->setFactory(Latte\Engine::class) - ->addSetup('setStrictTypes', [true]); -``` - -`addLocatorDefinition()` i `addImportedDefinition()` potrzebne są rzadko - takie usługi zwykle pochodzą z kluczy `implement:` i usług importowanych w NEON, a nie z ręcznego pisania. - - -Wyszukiwanie i modyfikowanie usług ----------------------------------- - -Do wyszukiwania i przechodzenia po istniejących definicjach builder udostępnia: - -| Metoda | Opis -|--------|------------ -| `getDefinition(string $name)` | definicja o podanej nazwie (zgłasza wyjątek, gdy jej brak) -| `hasDefinition(string $name)` | czy istnieje definicja albo alias o tej nazwie -| `getDefinitions()` | wszystkie definicje -| `removeDefinition(string $name)` | usuwa definicję -| `getByType(string $type)` | nazwa autowirowanej usługi tego typu albo `null` -| `getDefinitionByType(string $type)` | autowirowana definicja tego typu -| `findByType(string $type)` | wszystkie definicje tego typu jako pary `nazwa => definicja` -| `findByTag(string $tag)` | usługi noszące tag jako pary `nazwa => wartość tagu` -| `addExcludedClasses(array $types)` | wyklucza klasy i interfejsy z autowiringu - -Poręcznym idiomem jest użycie `getByType()` do sprawdzenia, czy usługa w ogóle istnieje - na przykład aby podpiąć się do loggera tylko wtedy, gdy aplikacja go ma: - -```php -if ($builder->getByType(Psr\Log\LoggerInterface::class)) { - $builder->getDefinition($this->prefix('articles')) - ->addSetup('setLogger'); -} -``` - - -Typy definicji --------------- - -Każda metoda `add*Definition()` zwraca inny rodzaj definicji. Wszystkie rozszerzają wspólnego przodka `Nette\DI\Definitions\Definition`: - -- **`ServiceDefinition`** - zwykła usługa; konfigurowana przez `setType()`, `setFactory()`, `addSetup()`, `addTag()` i `setAutowired()` -- **`FactoryDefinition`** - [generowana fabryka |factory]: interfejs, którego metoda `create()` przy każdym wywołaniu zwraca nowy obiekt -- **`AccessorDefinition`** - [generowany akcesor |factory#Akcesor]: interfejs, którego metoda `get()` zwraca istniejącą usługę -- **`LocatorDefinition`** - [multifabryka / lokator |factory#Multifabryka/akcesor] łącząca kilka fabryk albo akcesorów w jednym interfejsie -- **`ImportedDefinition`** - usługa, której kontener nie tworzy sam, lecz otrzymuje ją z zewnątrz w czasie działania - -Miej na uwadze, że `getDefinition()` zwraca taki rodzaj definicji, jaki żyje pod podaną nazwą. Jeśli Twój kod może natknąć się na generowaną fabrykę, sprawdź najpierw typ i skonfiguruj produkowany obiekt przez `getResultDefinition()`: - -```php -$def = $builder->getDefinition($name); -if ($def instanceof Nette\DI\Definitions\FactoryDefinition) { - $def = $def->getResultDefinition(); -} -$def->addSetup('setLogger'); -``` - - -Wskazówki i pułapki -=================== - - -Czas kompilacji kontra czas działania -------------------------------------- - -Najczęstsze źródło nieporozumień: kod rozszerzenia działa wtedy, gdy kontener jest **kompilowany**, a nie wtedy, gdy aplikacja obsługuje żądania. W praktyce oznacza to: - -- Rozszerzenie nigdy nie pracuje z instancjami usług - one jeszcze nie istnieją. Nie twórz usług przez `new`; zarejestruj definicję i pozwól kontenerowi je utworzyć. -- Wszystkie wartości konfiguracji są wpiekane w wygenerowany kod. Wartość, która może różnić się między środowiskami (ścieżka, hasło z `getenv()`), musi być oznaczona jako [dynamiczna |application:bootstrapping#Parametry dynamiczne], w przeciwnym razie zamarza w czasie kompilacji. -- Stringi przekazywane do `$this->initialization->addBody()` nie wykonują się teraz - to kod PHP emitowany do kontenera, wykonywany przy każdym żądaniu. - - -Zależności od plików --------------------- - -Kontener rekompiluje się, gdy zmienią się pliki konfiguracyjne albo klasy rozszerzeń. Jeśli jednak Twoje rozszerzenie czyta jakikolwiek inny plik - listę encji, konfigurację XML biblioteki - kontener nie ma jak się o tym dowiedzieć. Takie pliki zarejestruj przez: - -```php -$builder->addDependency($file); -``` - -W przeciwnym razie czeka Cię klasyczna zagadka: edytujesz plik, a aplikacja dalej zachowuje się po staremu - zmiana ujawnia się dopiero wtedy, gdy kontener przebuduje się z jakiegoś innego powodu. (Pliki czytane przez `loadFromFile()` śledzone są automatycznie.) - - -Rejestracja warunkowa ---------------------- - -Rozszerzenie może dostosowywać się do swojego środowiska. Opcjonalne integracje zabezpiecza się typowo przez `class_exists()`: - -```php -if (class_exists(Symfony\Component\Console\Command\Command::class)) { - $builder->addDefinition($this->prefix('command')) - ->setFactory(Blog\Console\SitemapCommand::class); -} -``` - -A wartości w rodzaju `%debugMode%` najlepiej przekazywać przez konstruktor rozszerzenia: - -```neon -extensions: - blog: BlogExtension(%debugMode%) -``` - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function __construct( - private bool $debugMode = false, - ) {} -} -``` - -Typowym zastosowaniem jest rejestrowanie panelu Tracy tylko w trybie deweloperskim. - - -Argumenty złożone ------------------ - -Czasem argument fabryki albo wywołania w setupie nie jest zwykłą wartością, nazwą klasy ani referencją `@service`. Na takie przypadki są: - -- `new Nette\DI\Definitions\Statement(Blog\Panel::class, [$args])` - obiekt tworzony na miejscu, "anonimowa usługa" używana jako argument -- `new Nette\DI\Definitions\Reference('blog.articles')` - referencja do usługi, obiektowy odpowiednik stringa `@name` -- `$builder::literal('PHP_SAPI')` - kawałek surowego kodu PHP wstawiany bez zmian do wygenerowanego kontenera - -Przykład - rejestracja panelu Tracy: - -```php -$builder->getDefinition($this->prefix('articles')) - ->addSetup('@Tracy\Bar::addPanel', [ - new Nette\DI\Definitions\Statement(Blog\ArticlesPanel::class), - ]); -``` - - -Eksportowane tagi i typy ------------------------- - -[Eksport metadanych |configuration#Eksport metadanych] można w konfiguracji ograniczyć tak, aby skompilowany kontener zachował tylko te tagi i typy autowiringu, których aplikacja faktycznie używa. Jeśli Twoje rozszerzenie pobiera usługi w czasie działania przez `$container->findByTag()` albo `$container->getByType()`, takie ograniczenie mogłoby usunąć dokładnie te metadane, na których polegasz. - -Aby temu zapobiec, powiedz kompilatorowi, które tagi i typy muszą być zawsze eksportowane: - -```php -public function loadConfiguration(): void -{ - // ten tag będzie eksportowany zawsze, nawet gdy eksport jest ograniczony - $this->compiler->addExportedTag('event.subscriber'); - - // ten typ będzie zawsze dostępny dla getByType() - $this->compiler->addExportedType(Nette\Database\Connection::class); -} -``` - -Obie metody tylko dodają do eksportowanych metadanych; nigdy nie nadpisują konfiguracji `di › export` aplikacji. Gdy więc aplikacja ograniczy eksport do listy, tagi i typy potrzebne Twojemu rozszerzeniu pozostaną uwzględnione; dopiero całkowite wyłączenie eksportu tagów (`tags: false`) odrzuca je wraz ze wszystkim innym. diff --git a/dependency-injection/pl/factory.texy b/dependency-injection/pl/factory.texy deleted file mode 100644 index 7106fa5f68..0000000000 --- a/dependency-injection/pl/factory.texy +++ /dev/null @@ -1,233 +0,0 @@ -Generowane fabryki -****************** - -.[perex] -Nette DI potrafi automatycznie wygenerować kod fabryki na podstawie interfejsów, oszczędzając Ci pisania kodu. - -Fabryka to klasa odpowiedzialna za tworzenie obiektów i przekazywanie ich zależności. Nie myl tego ze wzorcem projektowym *factory method*, który opisuje konkretny sposób używania fabryk i nie ma z tym tematem związku. - -Jak taka fabryka wygląda, pokazaliśmy w [rozdziale wprowadzającym |introduction#Fabryka]: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Nette DI potrafi automatycznie wygenerować kod fabryki. Wystarczy, że utworzysz interfejs, a Nette DI wygeneruje implementację. Interfejs musi mieć dokładnie jedną metodę o nazwie `create` i deklarować typ zwracany: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Fabryka `ArticleFactory` ma więc metodę `create`, która tworzy obiekty `Article`. Klasa `Article` może wyglądać na przykład tak: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } -} -``` - -Dodaj fabrykę do pliku konfiguracyjnego: - -```neon -services: - - ArticleFactory -``` - -Nette DI wygeneruje odpowiadającą jej implementację fabryki. - -W kodzie używającym fabryki poproś o obiekt przez jego interfejs, a Nette DI dostarczy wygenerowaną implementację: - -```php -class UserController -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function foo() - { - // pozwalamy fabryce utworzyć obiekt - $article = $this->articleFactory->create(); - } -} -``` - - -Fabryka z parametrami -===================== - -Metoda fabryczna `create` może przyjmować parametry, które następnie przekazuje do konstruktora. Dodajmy na przykład do klasy `Article` ID autora artykułu: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - private int $authorId, - ) { - } -} -``` - -Parametr dodamy również do fabryki: - -```php -interface ArticleFactory -{ - function create(int $authorId): Article; -} -``` - -Ponieważ nazwa parametru w konstruktorze (`$authorId`) zgadza się z nazwą parametru w metodzie fabrycznej, Nette DI przekazuje go automatycznie. - - -Definicja zaawansowana -====================== - -Definicję można zapisać również w postaci wielowierszowej, używając klucza `implement`: - -```neon -services: - articleFactory: - implement: ArticleFactory -``` - -Użycie tej dłuższej postaci pozwala podać dodatkowe argumenty konstruktora kluczem `arguments` i dalszą konfigurację przez `setup`, podobnie jak przy zwykłych definicjach usług. - -Przykład: gdyby metoda `create()` nie przyjmowała parametru `$authorId`, moglibyśmy podać w konfiguracji stałą wartość, która zostanie przekazana do konstruktora `Article`: - -```neon -services: - articleFactory: - implement: ArticleFactory - arguments: - authorId: 123 -``` - -Odwrotnie, gdyby `create()` przyjmowało `$authorId`, ale nie byłby on częścią konstruktora, tylko przekazywany metodą w rodzaju `Article::setAuthorId()`, odwołalibyśmy się do parametru w sekcji `setup`: - -```neon -services: - articleFactory: - implement: ArticleFactory - setup: - - setAuthorId($authorId) -``` - - -Akcesor -======= - -Poza fabrykami Nette potrafi generować również tak zwane akcesory. To obiekty z metodą `get()`, która zwraca konkretną usługę z kontenera DI. Powtarzane wywołania `get()` zawsze zwracają tę samą instancję. - -Akcesory zapewniają leniwe ładowanie zależności. Rozważ klasę, która loguje błędy do dedykowanej bazy danych. Gdyby klasa ta otrzymywała połączenie z bazą danych przez wstrzykiwanie w konstruktorze, połączenie nawiązywałoby się zawsze, nawet jeśli błędy występują rzadko, a połączenie przez większość czasu pozostaje niewykorzystane. Zamiast tego klasa może otrzymać akcesor. Obiekt bazy danych (połączenie) tworzony jest dopiero wtedy, gdy metoda `get()` akcesora zostanie wywołana po raz pierwszy. - -Jak utworzyć akcesor? Wystarczy napisać interfejs, a Nette DI wygeneruje implementację. Interfejs musi mieć dokładnie jedną metodę o nazwie `get`, która nie przyjmuje parametrów i deklaruje typ zwracany: - -```php -interface PDOAccessor -{ - function get(): PDO; -} -``` - -Dodaj akcesor do pliku konfiguracyjnego wraz z definicją usługi, którą ma zwracać: - -```neon -services: - - PDOAccessor - - PDO(%dsn%, %user%, %password%) -``` - -Ponieważ akcesor zwraca usługę `PDO`, a w konfiguracji zdefiniowana jest tylko jedna taka usługa, akcesor zwróci właśnie ją. Jeśli usług tego typu jest więcej, podaj po nazwie, którą akcesor ma zwracać, np. `- PDOAccessor(@db1)`. - - -Multifabryka/akcesor -==================== - -Jak dotąd nasze fabryki i akcesory potrafiły tworzyć albo zwracać tylko jeden typ obiektu. Możesz jednak łatwo utworzyć multifabryki, które łączą cechy fabryk i akcesorów. Interfejs takiego komponentu może zawierać wiele metod o nazwach `create<Nazwa>()` i `get<Nazwa>()`, na przykład: - -```php -interface MultiFactory -{ - function createArticle(): Article; - function getDb(): PDO; -} -``` - -Zamiast wstrzykiwać wiele osobnych fabryk i akcesorów, możesz więc wstrzyknąć jeden, bardziej kompleksowy komponent. - -Alternatywnie zamiast wielu metod można użyć `get()` z parametrem: - -```php -interface MultiFactoryAlt -{ - function get($name): PDO; -} -``` - -`MultiFactory::getDb()` robi wtedy to samo co `MultiFactoryAlt::get('db')`. Ten alternatywny zapis ma jednak tę wadę, że obsługiwane wartości `$name` nie wynikają jawnie z sygnatury interfejsu. Ponadto nie da się zdefiniować w interfejsie różnych typów zwracanych dla różnych wartości `$name`. - -Zamiast `get($name)` interfejs może deklarować `create($name)`, który przy każdym wywołaniu zwraca nową instancję (podczas gdy `get()` zwraca współdzieloną). Interfejs może zawierać tylko jedną taką metodę z parametrem. Jeśli typ zwracany metody jest nullable (np. `?PDO`), dla nieznanego `$name` zwraca `null` zamiast zgłaszać wyjątek. - - -Definicja z listą ------------------ -Multifabrykę możesz zdefiniować w konfiguracji za pomocą listy, z usługami zapisanymi w linii: .{data-version:3.2.0} - -```neon -services: - - MultiFactory( - article: Article() # definiuje createArticle() - db: PDO(%dsn%, %user%, %password%) # definiuje getDb() - ) -``` - -Alternatywnie możesz odwołać się w definicji multifabryki do istniejących usług za pomocą referencji: - -```neon -services: - article: Article - - PDO(%dsn%, %user%, %password%) - - MultiFactory( - article: @article # definiuje createArticle() - db: @\PDO # definiuje getDb() - ) -``` - - -Definicja z tagami ------------------- - -Innym sposobem zdefiniowania multifabryki jest użycie [tagów |services#Tagi]. Wartość tagu określa nazwę odpowiadającej metody: - -```neon -services: - article: - create: Article - tags: {multi: article} # definiuje createArticle() - db: - create: PDO(%dsn%, %user%, %password%) - tags: {multi: db} # definiuje getDb() - - - MultiFactory(tagged: multi) -``` diff --git a/dependency-injection/pl/faq.texy b/dependency-injection/pl/faq.texy deleted file mode 100644 index 7013fb6240..0000000000 --- a/dependency-injection/pl/faq.texy +++ /dev/null @@ -1,123 +0,0 @@ -Często zadawane pytania o DI (FAQ) -********************************** - - -Czy DI to inna nazwa IoC? -------------------------- - -*Inversion of Control* (IoC) to zasada opisująca przepływ sterowania w programie: czy Twój kod wywołuje kod zewnętrzny, czy też kod zewnętrzny (jak framework) wywołuje Twój kod? IoC to szerokie pojęcie obejmujące [zdarzenia |nette:glossary#Zdarzenia], tak zwaną [zasadę hollywoodzką |application:components#Styl hollywoodzki] i inne aspekty. Pojęcie to obejmuje również fabryki, omówione w [zasadzie nr 3: niech zajmie się tym fabryka |introduction#Zasada nr 3: niech zajmie się tym fabryka], które stanowią odwrócenie operatora `new`. - -*Dependency Injection* (DI) skupia się na tym, jak obiekty pozyskują swoje zależności (czyli inne obiekty, z którymi muszą pracować). To wzorzec projektowy zalecający jawne przekazywanie zależności obiektom, zamiast pozwalać im tworzyć je albo odnajdywać. - -DI można więc uznać za konkretną formę IoC. Nie wszystkie formy IoC sprzyjają jednak czystemu kodowi. Antywzorcami są na przykład techniki opierające się na [stanie globalnym|global-state] albo na wzorcu [Service Locator |#Czym jest Service Locator?]. - - -Czym jest Service Locator? --------------------------- - -To alternatywne podejście do wstrzykiwania zależności. Polega na istnieniu centralnego obiektu (lokatora), w którym rejestrowane są wszystkie dostępne usługi (zależności). Gdy obiekt potrzebuje zależności, prosi o nią Service Locator. - -W porównaniu z DI brakuje mu jednak przejrzystości. Zależności są ukryte w kodzie obiektu (wywołania lokatora), zamiast być jawne w jego API (konstruktorze albo metodach), więc zrozumienie powiązań wymaga zajrzenia do kodu. Trudniejsze jest też testowanie, bo nie da się po prostu przekazać atrap zależności przy tworzeniu instancji obiektu; często trzeba manipulować samym Service Locatorem. Ponadto Service Locator wprowadza zbędną zależność: obiekty stają się powiązane z lokatorem, inaczej niż przy DI, gdzie obiekty najlepiej w ogóle nie wiedzą o kontenerze. - - -Kiedy lepiej nie używać DI? ---------------------------- - -Nie są znane istotne wady poprawnego stosowania wzorca projektowego wstrzykiwania zależności. Wręcz przeciwnie, pozyskiwanie zależności z globalnie dostępnych miejsc (jak właściwości statyczne czy singletony) prowadzi do [licznych komplikacji|global-state], podobnie jak używanie Service Locatora. Dlatego używanie DI jest zwykle zawsze wskazane. To nie dogmat; po prostu żadna lepsza alternatywa czystego zarządzania zależnościami nie przyjęła się szeroko. - -Istnieją jednak konkretne, ograniczone sytuacje, w których globalny dostęp do obiektów bywa dopuszczalny. Na przykład podczas debugowania, gdy potrzebujesz zrzucić wartość zmiennej, zmierzyć czas wykonania albo zalogować komunikat w konkretnym miejscu. W tych przypadkach, dotyczących działań tymczasowych, które później zostaną z kodu usunięte, użycie globalnie dostępnego dumpera, timera albo loggera może być uzasadnione. Narzędzia te nie są częścią rdzennego projektu aplikacji. - - -Czy używanie DI ma wady? ------------------------- - -Czy używanie wstrzykiwania zależności wprowadza wady, takie jak więcej pisania kodu albo niższa wydajność? Co tracimy, gdy zaczynamy pisać kod zgodnie z DI? - -Samo DI ma pomijalny wpływ na wydajność w czasie działania i zużycie pamięci. Znaczenie może mieć wydajność kontenera DI, ale [Nette DI |nette-container] kompiluje kontener do zwykłego kodu PHP, co daje praktycznie zerowy narzut podczas działania aplikacji. - -Pisząc kod zgodnie z zasadami DI, często musisz tworzyć konstruktory przyjmujące zależności. Kiedyś mogło się to wydawać żmudne, ale nowoczesne IDE i możliwości takie jak [promocja właściwości w konstruktorze |https://blog.nette.org/pl/php-8-0-complete-overview-of-news#toc-constructor-property-promotion] z PHP 8 sprawiają, że jest to bardzo szybkie. Fabryki często potrafi wygenerować automatycznie Nette DI, co dodatkowo ogranicza powtarzalny kod. Z drugiej strony pozbywasz się konieczności pisania singletonów i statycznych akcesorów. - -Ogólnie dobrze zaprojektowana aplikacja używająca DI jest zwykle ani znacząco krótsza, ani dłuższa niż taka opierająca się na singletonach czy dostępie globalnym. Kod związany z tworzeniem i łączeniem zależności po prostu przenosi się z poszczególnych klas do dedykowanych miejsc: konfiguracji kontenera DI i fabryk. - - -Jak przepisać starą aplikację na DI? ------------------------------------- - -Migracja starej aplikacji na wstrzykiwanie zależności może być wymagającym procesem, zwłaszcza przy dużych i złożonych aplikacjach. Ważne, aby podejść do tego procesu systematycznie. - -- Przy przechodzeniu na wstrzykiwanie zależności ważne jest, aby wszyscy członkowie zespołu rozumieli używane zasady i praktyki. -- Najpierw przeanalizuj istniejącą aplikację, aby zidentyfikować kluczowe komponenty i ich zależności. Utwórz plan, które części będą refaktoryzowane i w jakiej kolejności. -- Zaimplementuj kontener DI albo, lepiej, użyj istniejącej biblioteki, takiej jak Nette DI. -- Stopniowo refaktoryzuj części aplikacji, aby używały wstrzykiwania zależności. Może to obejmować modyfikowanie konstruktorów albo metod tak, aby przyjmowały zależności jako parametry. -- Zaktualizuj kod, w którym tworzone są instancje obiektów, tak aby pobierał je z kontenera albo używał fabryk dostarczanych przez kontener. - -Pamiętaj, że przejście na wstrzykiwanie zależności to inwestycja w jakość kodu i długoterminową utrzymywalność aplikacji. Choć wprowadzenie tych zmian może być trudne, wynikiem powinien być czystszy, bardziej modułowy i łatwo testowalny kod, gotowy na przyszłe rozszerzenia i utrzymanie. - - -Dlaczego kompozycja jest preferowana nad dziedziczeniem? --------------------------------------------------------- -Używanie [kompozycji |nette:introduction-to-object-oriented-programming#Kompozycja] jest zwykle preferowane nad [dziedziczeniem |nette:introduction-to-object-oriented-programming#Dziedziczenie] przy ponownym wykorzystaniu kodu, bo prowadzi do luźniejszego powiązania. Przy kompozycji rzadziej napotkasz problemy, w których zmiana klasy bazowej psuje zależne podklasy. Typowym przykładem jest sytuacja określana jako [piekło konstruktorów |passing-dependencies#Piekło konstruktorów]. - - -Czy Nette DI Container da się używać poza Nette? ------------------------------------------------- - -Jak najbardziej. Nette DI Container jest częścią Nette, ale zaprojektowano go jako samodzielną bibliotekę, której można używać niezależnie od pozostałych części frameworka. Wystarczy zainstalować ją przez Composera, utworzyć plik konfiguracyjny definiujący Twoje usługi, a następnie kilkoma wierszami kodu PHP utworzyć kontener DI. I możesz od razu zacząć korzystać z zalet wstrzykiwania zależności w swoich projektach. - -Rozdział o [Nette DI Container |nette-container] opisuje konkretny przypadek użycia wraz z przykładami kodu. - - -Dlaczego konfiguracja jest w plikach NEON? ------------------------------------------- - -NEON to prosty i łatwy do czytania język konfiguracyjny opracowany w ramach Nette do konfigurowania aplikacji, usług i ich zależności. W porównaniu z JSON-em czy YAML-em oferuje do tego celu o wiele bardziej intuicyjne i elastyczne możliwości. W NEON-ie możesz naturalnie opisać definicje usług i relacje, których w JSON-ie czy YAML-u trudno albo w ogóle nie da się wyrazić równie przejrzyście. - - -Czy parsowanie plików NEON spowalnia aplikację? ------------------------------------------------ - -Choć pliki NEON parsują się bardzo szybko, szybkość ich parsowania jest na produkcji w zasadzie bez znaczenia. Pliki konfiguracyjne parsowane są bowiem tylko raz, przy pierwszym uruchomieniu aplikacji (albo gdy się zmienią). Po sparsowaniu generowany jest kod kontenera DI, zapisywany do cache (na dysk), a ten skompilowany kod PHP wykonywany jest przy każdym kolejnym żądaniu, co eliminuje potrzebę dalszego parsowania. - -Tak to działa w środowisku produkcyjnym. Podczas tworzenia aplikacji pliki NEON parsowane są za każdym razem, gdy zmieni się ich treść, dzięki czemu programista ma zawsze aktualny kontener DI. Jak wspomniano, samo parsowanie jest bardzo szybkie. - - -Jak sięgnąć w klasie po parametry z pliku konfiguracyjnego? ------------------------------------------------------------ - -Pamiętaj o [zasadzie nr 1: pozwól, aby Ci to przekazano |introduction#Zasada nr 1: pozwól, aby Ci to przekazano]. Jeśli klasa potrzebuje informacji z pliku konfiguracyjnego, nie kombinuj, jak może je *pobrać*. Zamiast tego po prostu o nie poproś, na przykład przez konstruktor klasy. A potem podaj tę wartość w pliku konfiguracyjnym. - -W tym przykładzie `%myParameter%` to symbol zastępczy wartości parametru `myParameter`, która zostanie przekazana do konstruktora `MyClass`: - -```neon -# config.neon -parameters: - myParameter: Some value - -services: - - MyClass(%myParameter%) -``` - -Jeśli chcesz przekazać wiele parametrów albo użyć autowiringu, warto [opakować parametry w obiekt |best-practices:passing-settings-to-presenters]. - - -Czy Nette obsługuje interfejs Container z PSR-11? -------------------------------------------------- - -[Nette DI Container |api:Nette\DI\Container] nie obsługuje PSR-11 bezpośrednio. Jeśli jednak potrzebujesz interoperacyjności między Nette DI Container a bibliotekami czy frameworkami oczekującymi interfejsu Container z PSR-11, możesz utworzyć [prosty adapter |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f], który posłuży jako most między Nette DI Container a PSR-11. - - -Co oznaczają terminy container, compiler, definition itd.? ----------------------------------------------------------- - -Krótki słowniczek słów, które nieustannie pojawiają się wokół Nette DI, w większości przy [pisaniu rozszerzeń |extensions]: - -- **Container** - skompilowany obiekt (`Nette\DI\Container`), który tworzy usługi na żądanie i trzyma je w czasie działania. Generowany jest raz jako zoptymalizowany kod PHP. -- **Compiler** - maszyneria zamieniająca pliki konfiguracyjne i rozszerzenia w tę klasę kontenera. -- **ContainerBuilder** - zmienny model kontenera używany podczas kompilacji; trzyma definicje usług, zanim powstanie jakakolwiek prawdziwa usługa. Zobacz [Tworzenie rozszerzeń |extensions#ContainerBuilder]. -- **Service** - obiekt zarządzany przez kontener, zwykle tworzony raz i współdzielony (singleton) - połączenie z bazą danych, mailer, logger. -- **Definition** - przepis na usługę: jej typ, sposób utworzenia i to, co zrobić potem. Nette zamienia definicje w metody fabryczne kontenera; istnieje kilka rodzajów (zobacz [typy definicji |extensions#Typy definicji]). -- **Type** - klasa albo interfejs usługi, używany przez autowiring do dopasowywania usług do miejsc, które ich wymagają. -- **Autowiring** - automatyczne przekazywanie usług do konstruktorów i metod według ich typu, dzięki czemu nie łączysz zależności ręcznie. -- **Tag** - etykieta dołączona do definicji (opcjonalnie z wartością); rozszerzenie może potem znaleźć wszystkie usługi ją noszące przez `findByTag()`. -- **Setup** - dodatkowe wywołania wykonywane na usłudze tuż po jej utworzeniu - wywołania metod albo przypisania do właściwości, dodawane przez `addSetup()`. -- **Alias** - alternatywna nazwa istniejącej usługi. diff --git a/dependency-injection/pl/global-state.texy b/dependency-injection/pl/global-state.texy deleted file mode 100644 index 0e478ec9d0..0000000000 --- a/dependency-injection/pl/global-state.texy +++ /dev/null @@ -1,294 +0,0 @@ -Stan globalny i singletony -************************** - -.[perex] -Ostrzeżenie: poniższe konstrukcje są objawami źle zaprojektowanego kodu: - -- `Foo::getInstance()` -- `DB::insert(...)` -- `Article::setDb($db)` -- `ClassName::$var` albo `static::$var` - -Czy któraś z tych konstrukcji pojawia się w Twoim kodzie? Jeśli tak, masz okazję do poprawy. Możesz sądzić, że to powszechne konstrukcje, widywane choćby w przykładowych rozwiązaniach rozmaitych bibliotek i frameworków. Jeśli tak, ich kod jest wadliwie zaprojektowany. - -Nie mówimy tu o jakiejś akademickiej czystości. Wszystkie te konstrukcje mają jedną wspólną cechę: korzystają ze stanu globalnego. A stan globalny fatalnie wpływa na jakość kodu. Klasy zaczynają kłamać o swoich zależnościach. Kod staje się nieprzewidywalny. Dezorientuje programistów i obniża ich efektywność. - -W tym rozdziale wyjaśnimy, dlaczego tak jest i jak unikać stanu globalnego. - - -Globalne powiązania -------------------- - -W idealnym świecie obiekt powinien komunikować się wyłącznie z obiektami, które zostały mu [bezpośrednio przekazane |passing-dependencies]. Jeśli utworzę dwa obiekty `A` i `B` i nigdy nie przekażę między nimi referencji, to ani `A`, ani `B` nie może sięgnąć po stan drugiego ani go zmienić. To wysoce pożądana właściwość kodu. To trochę jak z baterią i żarówką; żarówka nie zaświeci, dopóki nie połączysz jej z baterią przewodem. - -Nie dotyczy to jednak zmiennych globalnych (statycznych) ani singletonów. Obiekt `A` mógłby *bezprzewodowo* sięgnąć po obiekt `C` i zmienić go bez przekazywania jakiejkolwiek referencji, wywołując `C::changeSomething()`. Jeśli obiekt `B` również podłączy się do globalnego `C`, to `A` i `B` mogą wpływać na siebie przez `C`. - -Używanie zmiennych globalnych wprowadza nową formę *bezprzewodowego* powiązania, niewidocznego z zewnątrz. Tworzy zasłonę dymną, przez którą kod staje się trudniejszy do zrozumienia i użycia. Aby naprawdę pojąć zależności, programiści muszą przeczytać każdy wiersz kodu źródłowego, zamiast polegać po prostu na interfejsach klas. Co więcej, to powiązanie jest całkowicie zbędne. Stanu globalnego używa się dlatego, że jest łatwo dostępny zewsząd i pozwala na przykład zapisywać do bazy danych globalną (statyczną) metodą `DB::insert()`. Jak jednak pokażemy, ta pozorna wygoda jest znikoma w porównaniu z poważnymi komplikacjami, które wprowadza. - -.[note] -Pod względem zachowania nie ma różnicy między zmienną globalną a statyczną. Są równie szkodliwe. - - -Upiorne działanie na odległość ------------------------------- - -"Upiorne działanie na odległość" - tak Albert Einstein nazwał słynnie zjawisko fizyki kwantowej, które przyprawiało go o dreszcze. -Chodzi o splątanie kwantowe, w którym pomiar właściwości jednej cząstki natychmiast wpływa na inną, splątaną z nią cząstkę, niezależnie od dzielącej je odległości, choćby milionów lat świetlnych, co pozornie łamie fundamentalne prawo wszechświata mówiące, że nic nie może poruszać się szybciej od światła. - -W świecie oprogramowania "upiorne działanie na odległość" opisuje sytuację, w której uruchamiamy proces uznawany za odizolowany (bo żadne zależności nie zostały jawnie przekazane), a mimo to w odległych częściach systemu dochodzi, bez naszej wiedzy, do nieoczekiwanych interakcji i zmian stanu. Może to zajść wyłącznie przez stan globalny. - -Wyobraź sobie, że dołączasz do zespołu deweloperskiego przy projekcie z dużą, dojrzałą bazą kodu. Nowy szef prosi Cię o zaimplementowanie nowej funkcji, a Ty, jak dobry programista, zaczynasz od napisania testu. Ponieważ jednak jesteś w projekcie nowy, robisz sporo eksploracyjnych testów typu "co się stanie, gdy wywołam tę metodę". I próbujesz napisać taki test: - -```php -function testCreditCardCharge() -{ - $cc = new CreditCard('1234567890123456', 5, 2028); // numer Twojej karty - $cc->charge(100); -} -``` - -Uruchamiasz kod, może kilka razy, a po chwili zauważasz w telefonie powiadomienia z banku: przy każdym uruchomieniu z Twojej karty kredytowej pobrano 100 dolarów! 🤦‍♂️ - -Jakim cudem test mógł spowodować rzeczywistą płatność? Operowanie kartą kredytową nie jest proste. Trzeba porozumieć się z zewnętrzną usługą webową, znać jej URL, uwierzytelnić się itd. Żadnej z tych informacji nie ma w teście. Co gorsza, nie wiesz, gdzie te informacje się znajdują, przez co nie da się zamockować zewnętrznych zależności, aby zapobiec obciążeniu 100 dolarami przy każdym uruchomieniu testu. A skąd Ty, jako nowy programista, miałeś wiedzieć, że to, co zamierzasz zrobić, sprawi, że będziesz o 100 dolarów biedniejszy? - -To właśnie upiorne działanie na odległość! - -Jesteś zmuszony przekopywać się przez obszerny kod źródłowy i konsultować ze starszymi kolegami, aby zrozumieć powiązania w projekcie. Trudność ta bierze się stąd, że interfejs klasy `CreditCard` nie zdradza koniecznej inicjalizacji stanu globalnego. Nawet zbadanie kodu źródłowego klasy może nie ujawnić, którą metodę inicjalizującą wywołać. W najlepszym razie znajdziesz zmienną globalną, po którą sięga, i spróbujesz wydedukować, jak ją zainicjować. - -Klasy w takim projekcie są patologicznymi kłamcami. Klasa `CreditCard` udaje, że wystarczy utworzyć jej instancję i wywołać metodę `charge()`. W tajemnicy współpracuje jednak z inną klasą, `PaymentGateway`, reprezentującą bramkę płatniczą. Nawet interfejs `PaymentGateway` może sugerować niezależną inicjalizację, ale w rzeczywistości może ciągnąć poświadczenia z pliku konfiguracyjnego itd. Pierwotni programiści rozumieją, że `CreditCard` wymaga `PaymentGateway`. Sami tak ten kod napisali. Ale dla nowych osób to kompletna zagadka, która utrudnia im naukę i skuteczne włączenie się do pracy. - -Jak naprawić tę sytuację? Łatwo. **Niech API deklaruje zależności.** - -```php -function testCreditCardCharge() -{ - $gateway = new PaymentGateway(/* ... */); - $cc = new CreditCard('1234567890123456', 5, 2028); - $cc->charge($gateway, 100); -} -``` - -Zwróć uwagę, że wzajemne zależności w kodzie stają się od razu widoczne. Ponieważ metoda `charge()` deklaruje, że potrzebuje `PaymentGateway`, nie musisz już zgadywać ani pytać o tę zależność. Wiesz, że musisz utworzyć instancję, a przy okazji odkryjesz wymagane parametry dostępowe. Bez nich kod w ogóle by się nie uruchomił. - -A co najważniejsze, możesz teraz zamockować bramkę płatniczą, więc przy każdym uruchomieniu testu nie stracisz 100 dolarów. - -Stan globalny pozwala obiektom skrycie sięgać po zależności niezadeklarowane w ich API, skutecznie zamieniając Twoje API w patologicznych kłamców. - -Może nigdy tak o tym nie myślałeś, ale ilekroć używasz stanu globalnego, tworzysz tajne, bezprzewodowe kanały komunikacji. To upiorne działanie na odległość zmusza programistów do czytania każdego wiersza kodu, aby zrozumieć możliwe interakcje, obniża produktywność i dezorientuje nowych członków zespołu. Jeśli to Ty stworzyłeś ten kod, znasz prawdziwe zależności, ale każdy, kto przyjdzie po Tobie, nie ma o nich pojęcia. - -Unikaj pisania kodu opierającego się na stanie globalnym; preferuj jawne przekazywanie zależności. Przyjmij wstrzykiwanie zależności. - - -Kruchość stanu globalnego -------------------------- - -W kodzie korzystającym ze stanu globalnego i singletonów nigdy nie możesz mieć pewności, kiedy i przez kogo stan został zmieniony. Ryzyko to ujawnia się już przy inicjalizacji. Poniższy kod ma utworzyć połączenie z bazą danych i zainicjować bramkę płatniczą, ale wielokrotnie zgłasza wyjątki, a debugowanie przyczyny jest wyjątkowo żmudne: - -```php -PaymentGateway::init(); -DB::init('mysql:', 'user', 'password'); -``` - -Musisz drobiazgowo prześledzić kod, aby odkryć, że obiekt `PaymentGateway` bezprzewodowo sięga po inne obiekty, z których część wymaga połączenia z bazą danych. Bazę danych trzeba więc zainicjować przed `PaymentGateway`. Ale zasłona dymna stanu globalnego to przed Tobą ukrywa. Ile czasu by się zaoszczędziło, gdyby API tych klas było uczciwe i deklarowało swoje zależności? - -```php -$db = new DB('mysql:', 'user', 'password'); -$gateway = new PaymentGateway($db, /* ... */); -``` - -Podobny problem pojawia się przy globalnym dostępie do połączenia z bazą danych: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public function save(): void - { - DB::insert(/* ... */); - } -} -``` - -Przy wywołaniu metody `save()` nie wiadomo, czy połączenie z bazą danych zostało nawiązane ani kto odpowiada za jego nawiązanie. Jeśli potrzebujemy zmieniać połączenie z bazą danych dynamicznie (np. na potrzeby testów), możemy uciec się do dodania metod w rodzaju `DB::reconnect(...)` albo `DB::reconnectForTest()`. - -Rozważ przykład: - -```php -$article = new Article; -// ... -DB::reconnectForTest(); -Foo::doSomething(); -$article->save(); -``` - -Skąd mamy pewność, że przy wywołaniu `$article->save()` rzeczywiście używana jest testowa baza danych? Co, jeśli metoda `Foo::doSomething()` zmieniła globalne połączenie z bazą danych? Aby to ustalić, musielibyśmy zbadać kod źródłowy `Foo` i potencjalnie wielu innych klas. Takie dochodzenie dałoby tylko tymczasową odpowiedź, bo sytuacja może zmienić się później. - -A gdybyśmy przenieśli połączenie z bazą danych do zmiennej statycznej wewnątrz klasy `Article`? - -```php -class Article -{ - private static DB $db; - - public static function setDb(DB $db): void - { - self::$db = $db; - } - - public function save(): void - { - self::$db->insert(/* ... */); - } -} -``` - -Nie zmienia to absolutnie nic. Problemem jest sam stan globalny, niezależnie od tego, w której klasie jest ukryty. W tym scenariuszu, tak jak w poprzednim, przy wywołaniu `$article->save()` nie mamy pewności, do której bazy danych trafią dane. Ktokolwiek, gdziekolwiek w aplikacji, mógł w dowolnej chwili zmienić bazę danych przez `Article::setDb()`. Bez naszej wiedzy. - -Stan globalny czyni naszą aplikację **wyjątkowo kruchą**. - -Istnieje jednak prosty sposób poradzenia sobie z tym problemem. Wystarczy, aby API deklarowało zależności potrzebne do poprawnego działania. - -```php -class Article -{ - public function __construct( - private DB $db, - ) { - } - - public function save(): void - { - $this->db->insert(/* ... */); - } -} - -$article = new Article($db); -// ... -Foo::doSomething(); -$article->save(); -``` - -Takie podejście eliminuje obawy o ukryte albo nieoczekiwane zmiany połączenia z bazą danych. Mamy teraz pewność, gdzie zapisywany jest artykuł, a modyfikacje w niepowiązanych klasach nie mogą już na to wpłynąć. Kod nie jest już kruchy, lecz stabilny. - -Unikaj pisania kodu opierającego się na stanie globalnym; preferuj jawne przekazywanie zależności. Przyjmij wstrzykiwanie zależności. - - -Singleton ---------- - -Singleton to wzorzec projektowy, który zgodnie z [definicją |https://pl.wikipedia.org/wiki/Singleton_(wzorzec_projektowy)] ze słynnej publikacji Gang of Four ogranicza klasę do jednej instancji i oferuje globalny dostęp do niej. Implementacja tego wzorca zwykle przypomina poniższy kod: - -```php -class Singleton -{ - private static self $instance; - - public static function getInstance(): self - { - self::$instance ??= new self; - return self::$instance; - } - - // i inne metody pełniące funkcje klasy -} -``` - -Niestety singleton wprowadza do aplikacji stan globalny. A jak pokazaliśmy wyżej, stan globalny jest niepożądany. Dlatego singleton uznawany jest za antywzorzec. - -Nie używaj singletonów w swoim kodzie i zastąp je innymi mechanizmami. Naprawdę nie potrzebujesz singletonów. Jeśli jednak potrzebujesz zapewnić, że w całej aplikacji istnieje tylko jedna instancja klasy, przekaż tę odpowiedzialność [kontenerowi DI |container]. Powstaje w ten sposób singleton o zasięgu aplikacji, powszechnie nazywany usługą. Sama klasa zostaje wtedy uwolniona od zarządzania swoją unikalnością (czyli nie będzie miała metody `getInstance()` ani statycznej właściwości z instancją) i może skupić się wyłącznie na swoich obowiązkach. Przestanie więc łamać zasadę pojedynczej odpowiedzialności. - - -Stan globalny kontra testy --------------------------- - -Pisząc testy, zakładamy najlepiej, że każdy test jest odizolowaną jednostką, do której nie wchodzi ani z której nie wychodzi żaden zewnętrzny stan. Po zakończeniu testu jakikolwiek związany z nim stan powinien zostać automatycznie posprzątany przez garbage collector. Dzięki temu testy są odizolowane. Możemy więc uruchamiać je w dowolnej kolejności. - -Gdy jednak w grę wchodzi stan globalny albo singletony, te korzystne założenia się sypią. Stan może wyciekać do testów i z testów. Nagle kolejność testów może mieć znaczenie. - -Aby w ogóle przetestować kod z singletonami, programiści często muszą naruszyć ich integralność, na przykład pozwalając podmienić instancję singletona. Takie rozwiązania są w najlepszym razie hackami, prowadzącymi do kodu trudnego w utrzymaniu i zrozumieniu. Każdy test (albo jego metoda `tearDown()`), który modyfikuje stan globalny, musi drobiazgowo cofnąć te zmiany. - -Stan globalny to największy ból głowy przy testach jednostkowych! - -Jak to naprawić? Prosto. Unikaj pisania kodu używającego singletonów; preferuj jawne przekazywanie zależności. Przyjmij wstrzykiwanie zależności. - - -Stałe globalne --------------- - -Stan globalny nie ogranicza się do używania singletonów i zmiennych statycznych, ale może dotyczyć również stałych globalnych. - -Stałe, których wartości reprezentują uniwersalne prawdy (`M_PI`) albo dostarczają samodzielnych informacji (`PREG_BACKTRACK_LIMIT_ERROR`), są zwykle akceptowalne. Odwrotnie, stałe używane jako sposób *bezprzewodowego* wstrzykiwania informacji do kodu są w istocie ukrytymi zależnościami. Jak `LOG_FILE` w poniższym przykładzie. Użycie stałej `FILE_APPEND` jest całkowicie poprawne. - -```php -const LOG_FILE = '...'; - -class Foo -{ - public function doSomething() - { - // ... - file_put_contents(LOG_FILE, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Zamiast tego powinniśmy zadeklarować ścieżkę do pliku logu jako parametr konstruktora klasy `Foo`, czyniąc ją jawną częścią jej API: - -```php -class Foo -{ - public function __construct( - private string $logFile, - ) { - } - - public function doSomething() - { - // ... - file_put_contents($this->logFile, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Teraz jawnie przekazujemy ścieżkę do pliku logu. Możemy ją łatwo zmienić w razie potrzeby, co upraszcza testowanie i utrzymanie kodu. - - -Funkcje globalne i metody statyczne ------------------------------------ - -Chcemy podkreślić, że używanie metod statycznych i funkcji globalnych nie jest samo w sobie problematyczne. Wyjaśniliśmy problemy z metodami w rodzaju `DB::insert()`, ale sednem problemu był zawsze leżący u ich podstaw stan globalny, typowo przechowywany w zmiennej statycznej. Metoda `DB::insert()` opiera się na zmiennej statycznej trzymającej połączenie z bazą danych. Bez tej zmiennej nie dałoby się zaimplementować tej metody. - -Używanie deterministycznych metod statycznych i funkcji, takich jak `Closure::fromCallable()`, `strlen()` i wielu innych, jest w pełni zgodne ze wstrzykiwaniem zależności. Funkcje te są przewidywalne, bo dla tych samych parametrów wejściowych zawsze zwracają ten sam wynik. Nie używają żadnego stanu globalnego. - -W PHP istnieją jednak funkcje, które nie są deterministyczne. Należy do nich na przykład funkcja `htmlspecialchars()`. Jej trzeci parametr, `$encoding`, jeśli zostanie pominięty, przyjmuje domyślnie wartość opcji konfiguracyjnej `default_charset` (`ini_get('default_charset')`). Zaleca się więc zawsze podawać ten parametr, aby zapobiec potencjalnie nieprzewidywalnemu zachowaniu. Nette konsekwentnie tak robi. - -Niektóre funkcje, jak `strtolower()` i `strtoupper()`, jeszcze niedawno zachowywały się niedeterministycznie, zależnie od ustawienia locale (`setlocale()`). Powodowało to wiele komplikacji, najczęściej przy pracy z językiem tureckim. Turecki rozróżnia bowiem "I" z kropką i bez kropki, zarówno w małych, jak i wielkich literach. W konsekwencji `strtolower('I')` zwracało `ı` (małe i bez kropki), a `strtoupper('i')` zwracało `İ` (wielkie I z kropką), co prowadziło do licznych tajemniczych błędów aplikacji. Problem ten został jednak naprawiony w PHP 8.2 i funkcje nie zależą już od locale. - -To dobry przykład tego, jak stan globalny (ustawienie locale) przysporzył kłopotów tysiącom programistów na całym świecie. Ostateczne rozwiązanie polegało na uczynieniu funkcji niezależnymi od locale, czyli skutecznym usunięciu ukrytej zależności. - - -Kiedy można używać stanu globalnego? ------------------------------------- - -Istnieją konkretne, ograniczone sytuacje, w których używanie stanu globalnego bywa dopuszczalne. Na przykład podczas debugowania, gdy potrzebujesz zrzucić wartość zmiennej albo zmierzyć czas wykonania konkretnego fragmentu kodu. W tych przypadkach, dotyczących działań tymczasowych, które później zostaną z kodu usunięte, użycie globalnie dostępnego dumpera albo timera może być uzasadnione. Narzędzia te nie są częścią rdzennego projektu aplikacji. - -Innym przykładem są funkcje wyrażeń regularnych PHP (`preg_*`), które wewnętrznie cachują skompilowane wyrażenia regularne w pamięci statycznej. Gdy w swoim kodzie wielokrotnie wywołujesz funkcje z tym samym wyrażeniem regularnym, wyrażenie kompilowane jest tylko raz. Ten cache poprawia wydajność i jest dla użytkownika całkowicie niewidoczny, przez co takie użycie wewnętrznego stanu statycznego jest zwykle akceptowalne. - - -Podsumowanie ------------- - -Omówiliśmy, dlaczego warto: - -1) usunąć ze swojego kodu wszystkie zmienne właściwości statyczne (stan globalny) -2) jawnie deklarować zależności -3) i korzystać ze wstrzykiwania zależności - -Projektując swój kod, pamiętaj, że każde zmienne `static $foo` jest potencjalnym źródłem problemów. Aby stworzyć środowisko przyjazne DI, kluczowe jest całkowite wyeliminowanie stanu globalnego i zastąpienie go wstrzykiwaniem zależności. - -Podczas tego procesu możesz odkryć potrzebę rozdzielenia klas mających wiele odpowiedzialności. Nie wahaj się tego zrobić; dąż do zasady pojedynczej odpowiedzialności. - -*Chciałbym podziękować Miško Hevery'emu, którego artykuły, takie jak [Flaw: Brittle Global State & Singletons |https://web.archive.org/web/20230321084133/http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/], stanowią podstawę tego rozdziału.* diff --git a/dependency-injection/pl/introduction.texy b/dependency-injection/pl/introduction.texy deleted file mode 100644 index 37b2e8f95a..0000000000 --- a/dependency-injection/pl/introduction.texy +++ /dev/null @@ -1,525 +0,0 @@ -Czym jest wstrzykiwanie zależności? -*********************************** - -.[perex] -Ten rozdział przedstawia podstawowe praktyki programistyczne, których powinieneś trzymać się przy pisaniu każdej aplikacji. To fundamenty niezbędne do pisania czystego, zrozumiałego i utrzymywalnego kodu. - -Jeśli przyswoisz sobie te reguły i będziesz się ich trzymać, Nette wesprze Cię na każdym kroku. Załatwi za Ciebie rutynowe zadania i zapewni maksymalną wygodę, dzięki czemu będziesz mógł skupić się na samej logice. - -Zasady, które tu pokażemy, są całkiem proste. Nie ma się czego bać. - - -Pamiętasz swój pierwszy program? --------------------------------- - -Nie wiemy, w jakim języku go napisałeś, ale gdyby to było PHP, wyglądał zapewne mniej więcej tak: - -```php -function addition(float $a, float $b): float -{ - return $a + $b; -} - -echo addition(23, 1); // wypisze 24 -``` - -Kilka trywialnych wierszy kodu, a jednak kryją tyle kluczowych pojęć. Że istnieją zmienne. Że kod dzieli się na mniejsze jednostki, takie jak funkcje. Że przekazujemy im argumenty wejściowe i zwracają wyniki. Brakuje tylko warunków i pętli. - -To, że przekazujemy funkcji dane wejściowe, a ona zwraca wynik, jest koncepcją całkowicie zrozumiałą, używaną również w innych dziedzinach, jak matematyka. - -Funkcja ma swoją sygnaturę, składającą się z nazwy, listy parametrów i ich typów oraz wreszcie typu wartości zwracanej. Jako użytkowników interesuje nas sygnatura; o wewnętrznej implementacji zwykle nie musimy wiedzieć nic. - -Wyobraź sobie teraz, że sygnatura funkcji wyglądałaby tak: - -```php -function addition(float $x): float -``` - -Dodawanie z jednym parametrem? To dziwne… A co powiesz na to? - -```php -function addition(): float -``` - -To już naprawdę dziwne, prawda? Jak używa się takiej funkcji? - -```php -echo addition(); // co wypisze? -``` - -Patrząc na taki kod, bylibyśmy zdezorientowani. Nie tylko początkujący by go nie zrozumiał, ale nawet wprawny programista nie zrozumiałby takiego kodu. - -Zastanawiasz się, jak taka funkcja wyglądałaby w środku? Skąd wzięłaby liczby do dodania? Zapewne pozyskałaby je *jakoś* sama, może tak: - -```php -function addition(): float -{ - $a = Input::get('a'); - $b = Input::get('b'); - return $a + $b; -} -``` - -W ciele funkcji odkryliśmy ukryte zależności od innych funkcji globalnych albo metod statycznych. Aby dowiedzieć się, skąd tak naprawdę pochodzą liczby, musimy szukać dalej. - - -Nie tędy droga! ---------------- - -Konstrukcja, którą właśnie pokazaliśmy, jest źródłem wielu negatywnych cech: - -- sygnatura funkcji udawała, że nie potrzebuje liczb do dodania, co nas zdezorientowało -- nie mamy pojęcia, jak sprawić, aby funkcja dodała dwie inne liczby -- musieliśmy zajrzeć do kodu, aby dowiedzieć się, skąd bierze liczby -- odkryliśmy ukryte zależności -- pełne zrozumienie wymaga zbadania również tych zależności - -I czy w ogóle zadaniem funkcji dodającej jest pozyskiwanie danych wejściowych? Oczywiście nie. Jej odpowiedzialnością jest wyłącznie samo dodawanie. - - -Nie chcemy natykać się na taki kod i już na pewno nie chcemy go pisać. Naprawa jest prosta: wracamy do podstaw i po prostu używamy parametrów: - - -```php -function addition(float $a, float $b): float -{ - return $a + $b; -} -``` - - -Zasada nr 1: pozwól, aby Ci to przekazano ------------------------------------------ - -Najważniejsza zasada brzmi: **wszystkie dane, których potrzebują funkcje albo klasy, muszą zostać im dostarczone**. - -Zamiast wymyślać ukryte sposoby, w jakie mogłyby te dane pozyskać, po prostu przekaż parametry. Zaoszczędzisz czas spędzony na wymyślaniu ukrytych ścieżek, które z pewnością nie poprawią Twojego kodu. - -Jeśli zawsze i wszędzie będziesz trzymać się tej zasady, jesteś na drodze do kodu bez ukrytych zależności. Do kodu zrozumiałego nie tylko dla autora, ale i dla każdego, kto przeczyta go później. Gdzie wszystko wynika z sygnatur funkcji i klas i nie trzeba szukać ukrytych szczegółów w implementacji. - -Technika ta fachowo nazywa się **wstrzykiwaniem zależności** (Dependency Injection). A dane nazywane są **zależnościami**. To zwykłe przekazywanie parametrów, nic więcej. - -.[note] -Nie myl wstrzykiwania zależności, które jest wzorcem projektowym, z "kontenerem wstrzykiwania zależności", który jest narzędziem, czyli czymś zasadniczo innym. O kontenerach opowiemy później. - - -Od funkcji do klas ------------------- - -A jak ma się to do klas? Klasa jest bytem bardziej złożonym niż prosta funkcja, ale zasada nr 1 obowiązuje w pełni również tutaj. Jest tylko [więcej sposobów przekazywania argumentów |passing-dependencies]. Na przykład całkiem podobnie jak przy funkcji: - -```php -class Math -{ - public function sum(float $a, float $b): float - { - return $a + $b; - } -} - -$math = new Math; -echo $math->sum(23, 1); // 24 -``` - -Albo za pomocą innych metod, albo bezpośrednio konstruktora: - -```php -class Sum -{ - public function __construct( - private float $a, - private float $b, - ) { - } - - public function calculate(): float - { - return $this->a + $this->b; - } -} - -$sum = new Sum(23, 1); -echo $sum->calculate(); // 24 -``` - -Oba przykłady są w pełni zgodne ze wstrzykiwaniem zależności. - - -Przykłady z życia ------------------ - -W prawdziwym świecie nie będziesz pisać klas dodających liczby. Przejdźmy do praktycznych przykładów. - -Weźmy klasę `Article` reprezentującą artykuł na blogu: - -```php -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - // zapisujemy artykuł do bazy danych - } -} -``` - -a użycie będzie następujące: - -```php -$article = new Article; -$article->title = '10 rzeczy, które musisz wiedzieć o odchudzaniu'; -$article->content = 'Każdego roku miliony ludzi ...'; -$article->save(); -``` - -Metoda `save()` zapisze artykuł do tabeli bazy danych. Zaimplementowanie jej za pomocą [Nette Database |database:] byłoby proste, gdyby nie jeden szkopuł: skąd `Article` weźmie połączenie z bazą danych, czyli obiekt klasy `Nette\Database\Connection`? - -Wygląda na to, że mamy sporo możliwości. Mógłby wziąć je ze zmiennej statycznej. Albo przez dziedziczenie po klasie dostarczającej połączenie z bazą danych. Albo użyć [singletona |global-state#Singleton]. Albo tak zwanych fasad, jak w Laravelu: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - DB::insert( - 'INSERT INTO articles (title, content) VALUES (?, ?)', - [$this->title, $this->content], - ); - } -} -``` - -Świetnie, problem rozwiązany. - -Czy aby na pewno? - -Przypomnijmy sobie [zasadę nr 1: pozwól, aby Ci to przekazano |#Zasada nr 1: pozwól, aby Ci to przekazano]: wszystkie zależności, których klasa potrzebuje, muszą zostać jej przekazane. Bo jeśli złamiemy tę zasadę, wkroczyliśmy na drogę do bałaganiarskiego kodu pełnego ukrytych zależności i braku przejrzystości, a wynikiem będzie aplikacja, której utrzymanie i rozwijanie stanie się wyzwaniem. - -Użytkownik klasy `Article` nie ma pojęcia, gdzie metoda `save()` zapisuje artykuł. Do tabeli bazy danych? Której, produkcyjnej czy testowej? I jak to zmienić? - -Użytkownik musi zajrzeć do implementacji metody `save()` i znajduje w niej użycie metody `DB::insert()`. Musi więc szukać dalej, jak ta metoda pozyskuje połączenie z bazą danych. A ukryte zależności mogą tworzyć całkiem długi łańcuch. - -W czystym i dobrze zaprojektowanym kodzie nigdy nie ma ukrytych zależności, fasad Laravela ani zmiennych statycznych. W czystym i dobrze zaprojektowanym kodzie przekazuje się argumenty: - -```php -class Article -{ - public function save(Nette\Database\Connection $db): void - { - $db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -Jeszcze praktyczniejsze, jak zobaczymy później, jest użycie konstruktora: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function save(): void - { - $this->db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -.[note] -Jeśli jesteś doświadczonym programistą, możesz pomyśleć, że `Article` w ogóle nie powinien mieć metody `save()`; powinien reprezentować wyłącznie strukturę danych, a zapisem powinno zajmować się osobne repozytorium. To ma sens. Ale wyprowadziłoby nas to daleko poza zakres tematu, którym jest wstrzykiwanie zależności, i poza cel podawania prostych przykładów. - -Jeśli piszesz klasę, która do działania potrzebuje na przykład bazy danych, nie wymyślaj, skąd ją wziąć, tylko pozwól, aby Ci ją przekazano. Choćby jako parametr konstruktora albo innej metody. Przyznaj się do zależności. Przyznaj się do nich w API swojej klasy. Otrzymasz zrozumiały i przewidywalny kod. - -A co z tą klasą, która loguje komunikaty o błędach: - -```php -class Logger -{ - public function log(string $message): void - { - $file = LOG_DIR . '/log.txt'; - file_put_contents($file, $message . "\n", FILE_APPEND); - } -} -``` - -Jak myślisz, czy trzymaliśmy się [zasady nr 1: pozwól, aby Ci to przekazano |#Zasada nr 1: pozwól, aby Ci to przekazano]? - -Nie trzymaliśmy. - -Kluczową informację, czyli katalog zawierający plik logu, *klasa pozyskuje sama* ze stałej. - -Spójrz na przykład użycia: - -```php -$logger = new Logger; -$logger->log('Temperatura wynosi 23 °C'); -$logger->log('Temperatura wynosi 10 °C'); -``` - -Czy bez znajomości implementacji potrafiłbyś odpowiedzieć, gdzie zapisywane są komunikaty? Przyszłoby Ci do głowy, że do jej działania wymagane jest istnienie stałej `LOG_DIR`? I czy potrafiłbyś utworzyć drugą instancję, która zapisywałaby gdzie indziej? Z pewnością nie. - -Naprawmy tę klasę: - -```php -class Logger -{ - public function __construct( - private string $file, - ) { - } - - public function log(string $message): void - { - file_put_contents($this->file, $message . "\n", FILE_APPEND); - } -} -``` - -Klasa jest teraz o wiele bardziej zrozumiała, konfigurowalna, a przez to użyteczniejsza. - -```php -$logger = new Logger('/path/to/log.txt'); -$logger->log('Temperatura wynosi 15 °C'); -``` - - -Ale mnie to nie obchodzi! -------------------------- - -*"Gdy tworzę obiekt Article i wywołuję save(), nie chcę zajmować się bazą danych; chcę tylko, aby zapisał się w tej, którą mam skonfigurowaną."* - -*"Gdy używam Loggera, chcę tylko, aby komunikat został zapisany, i nie chcę zajmować się gdzie. Niech użyje ustawień globalnych."* - -To słuszne uwagi. - -Jako przykład pokażmy klasę, która rozsyła newslettery i loguje wynik: - -```php -class NewsletterDistributor -{ - public function distribute(): void - { - $logger = new Logger(/* ... */); - try { - $this->sendEmails(); - $logger->log('E-maile zostały wysłane'); - - } catch (Exception $e) { - $logger->log('Podczas wysyłania wystąpił błąd'); - throw $e; - } - } -} -``` - -Ulepszony `Logger`, który nie używa już stałej `LOG_DIR`, wymaga w konstruktorze ścieżki do pliku. Jak to rozwiązać? Klasa `NewsletterDistributor` nie interesuje się tym, gdzie zapisywane są komunikaty; chce je po prostu logować. - -Rozwiązaniem jest znów [zasada nr 1: pozwól, aby Ci to przekazano |#Zasada nr 1: pozwól, aby Ci to przekazano]: przekazujemy wszystkie dane, których klasa potrzebuje. - -Czy oznacza to więc, że przekażemy przez konstruktor ścieżkę do logu, której użyjemy potem przy tworzeniu obiektu `Logger`? - -```php -class NewsletterDistributor -{ - public function __construct( - private string $file, // ⛔ NIE TAK! - ) { - } - - public function distribute(): void - { - $logger = new Logger($this->file); -``` - -Nie tak! Bo ścieżka **nie** jest daną, której potrzebuje klasa `NewsletterDistributor`; potrzebuje jej `Logger`. Dostrzegasz różnicę? Klasa `NewsletterDistributor` potrzebuje samego loggera. Przekażemy więc sam logger: - -```php -class NewsletterDistributor -{ - public function __construct( - private Logger $logger, // ✅ - ) { - } - - public function distribute(): void - { - try { - $this->sendEmails(); - $this->logger->log('E-maile zostały wysłane'); - - } catch (Exception $e) { - $this->logger->log('Podczas wysyłania wystąpił błąd'); - throw $e; - } - } -} -``` - -Teraz z sygnatury klasy `NewsletterDistributor` jasno wynika, że logowanie jest częścią jej funkcji. A zadanie podmiany loggera na inny, choćby na potrzeby testów, jest całkowicie proste. Co więcej, gdyby konstruktor klasy `Logger` się zmienił, nie wpłynie to na naszą klasę. - - -Zasada nr 2: bierz to, co Twoje -------------------------------- - -Nie daj się zwieść i nie przyjmuj zależności swoich zależności. Przyjmuj wyłącznie własne zależności. - -Dzięki temu kod używający innych obiektów będzie całkowicie niezależny od zmian ich konstruktorów. Jego API będzie dokładniejsze. A co najważniejsze, podmiana tych zależności na inne będzie prosta. - - -Nowy członek rodziny --------------------- - -Zespół deweloperski zdecydował się stworzyć drugi logger, taki, który zapisuje do bazy danych. Tworzymy więc klasę `DatabaseLogger`. Mamy teraz dwie klasy, `Logger` i `DatabaseLogger`; jedna zapisuje do pliku, druga do bazy danych... czy nazewnictwo nie wydaje się nieco dziwne? Czy nie lepiej byłoby zmienić nazwę `Logger` na `FileLogger`? Z pewnością. - -Zróbmy to jednak sprytnie. Tworzymy interfejs pod pierwotną nazwą: - -```php -interface Logger -{ - function log(string $message): void; -} -``` - -… który zaimplementują oba loggery: - -```php -class FileLogger implements Logger -// ... - -class DatabaseLogger implements Logger -// ... -``` - -I dzięki temu w reszcie kodu, w której wykorzystywany jest logger, nie trzeba będzie niczego modyfikować. Na przykład konstruktor klasy `NewsletterDistributor` nadal będzie zadowolony, wymagając jako parametru `Logger`. A od nas zależy, którą instancję mu dostarczymy. - -**Dlatego nigdy nie dodajemy do nazw interfejsów przyrostka `Interface` ani przedrostka `I`.** W przeciwnym razie nie dałoby się tak elegancko rozszerzać kodu. - - -Houston, mamy problem ---------------------- - -Podczas gdy w całej aplikacji wystarczy nam jedna instancja loggera, plikowego albo bazodanowego, i wystarczy przekazać ją wszędzie tam, gdzie odbywa się logowanie, w przypadku klasy `Article` sytuacja jest zupełnie inna. Jej instancje tworzymy według potrzeb, nawet wielokrotnie. Jak poradzić sobie z zależnością od bazy danych w jej konstruktorze? - -Przykładem może być kontroler, który po wysłaniu formularza ma zapisać artykuł do bazy danych: - -```php -class EditController extends Controller -{ - public function formSubmitted($data) - { - $article = new Article(/* ... */); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Możliwe rozwiązanie wydaje się oczywiste: każmy przekazać obiekt bazy danych przez konstruktor do `EditController` i użyjmy `$article = new Article($this->db)`. - -Tak jak w poprzednim przypadku z `Logger` i ścieżką do pliku, nie jest to poprawne podejście. Baza danych nie jest zależnością `EditController`, lecz `Article`. Przekazanie bazy danych łamie więc [zasadę nr 2: bierz to, co Twoje |#Zasada nr 2: bierz to, co Twoje]. Jeśli konstruktor klasy `Article` się zmieni (dojdzie nowy parametr), będziesz musiał zmodyfikować kod we wszystkich miejscach, w których tworzone są instancje. Ojej. - -Houston, jaka jest Twoja propozycja? - - -Zasada nr 3: niech zajmie się tym fabryka ------------------------------------------ - -Eliminując ukryte zależności i przekazując wszystkie zależności jako argumenty, uzyskaliśmy bardziej konfigurowalne i elastyczne klasy. Potrzebujemy więc czegoś dodatkowego, co utworzy i skonfiguruje za nas te elastyczniejsze klasy. Nazwiemy to fabrykami. - -Zasada brzmi: jeśli klasa ma zależności, deleguj tworzenie jej instancji do fabryki. - -Fabryki są sprytniejszą alternatywą dla operatora `new` w świecie wstrzykiwania zależności. - -.[note] -Nie myl tego ze wzorcem projektowym *factory method*, który opisuje konkretny sposób wykorzystania fabryk i nie ma z tym tematem związku. - - -Fabryka -------- - -Fabryka to metoda albo klasa tworząca i konfigurująca obiekty. Klasę produkującą `Article` nazwiemy `ArticleFactory`, a może wyglądać tak: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Jej użycie w kontrolerze będzie następujące: - -```php -class EditController extends Controller -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function formSubmitted($data) - { - // pozwalamy fabryce utworzyć obiekt - $article = $this->articleFactory->create(); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -W tym momencie, jeśli sygnatura konstruktora klasy `Article` się zmieni, jedyną częścią kodu, która musi zareagować, jest sama `ArticleFactory`. Cały pozostały kod pracujący z obiektami `Article`, na przykład `EditController`, pozostanie nietknięty. - -Możesz teraz drapać się po głowie, zastanawiając się, czy rzeczywiście poprawiliśmy sytuację. Ilość kodu wzrosła, a całość zaczyna wyglądać podejrzanie skomplikowanie. - -Bez obaw, wkrótce dojdziemy do kontenera Nette DI. A ma on w rękawie kilka sztuczek, które ogromnie uproszczą budowanie aplikacji ze wstrzykiwaniem zależności. Na przykład zamiast klasy `ArticleFactory` wystarczy [napisać sam interfejs |factory]: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Ale wyprzedzamy fakty, bądź czujny :-) - - -Podsumowanie ------------- - -Na początku tego rozdziału obiecaliśmy pokazać sposób projektowania czystego kodu. Wystarczy zadbać o to, aby klasom: - -1) [były przekazywane zależności, których potrzebują |#Zasada nr 1: pozwól, aby Ci to przekazano] -2) [i odwrotnie, aby nie przekazywano im tego, czego bezpośrednio nie potrzebują |#Zasada nr 2: bierz to, co Twoje] -3) [a obiekty z zależnościami najlepiej było tworzyć w fabrykach |#Zasada nr 3: niech zajmie się tym fabryka] - -Na pierwszy rzut oka może to nie być widoczne, ale te trzy zasady mają daleko idące konsekwencje. Prowadzą do radykalnie innego spojrzenia na projektowanie kodu. Czy warto? Programiści, którzy porzucili stare nawyki i zaczęli konsekwentnie używać wstrzykiwania zależności, uznają ten krok za przełomowy moment w swojej karierze zawodowej. Otworzył im świat przejrzystych i utrzymywalnych aplikacji. - -A co, jeśli kod nie używa wstrzykiwania zależności konsekwentnie? Co, jeśli zbudowany jest na metodach statycznych albo singletonach? Czy prowadzi to do problemów? [Tak, i to bardzo poważnych |global-state]. diff --git a/dependency-injection/pl/nette-container.texy b/dependency-injection/pl/nette-container.texy deleted file mode 100644 index b906f70810..0000000000 --- a/dependency-injection/pl/nette-container.texy +++ /dev/null @@ -1,117 +0,0 @@ -Nette DI Container -****************** - -.[perex] -Nette DI to jedna z najciekawszych bibliotek Nette. Potrafi generować i automatycznie aktualizować kompilowane kontenery DI, które są wyjątkowo szybkie i zaskakująco łatwe do skonfigurowania. - -Postać usług, które kontener DI ma tworzyć, definiuje się zwykle w plikach konfiguracyjnych w [formacie NEON|neon:format]. Kontener, który ręcznie utworzyliśmy w [poprzednim rozdziale|container], zapisalibyśmy tak: - -```neon -parameters: - db: - dsn: 'mysql:' - user: root - password: '***' - -services: - - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - - ArticleFactory - - EditController -``` - -Składnia jest bardzo zwięzła. - -Wszystkie zależności zadeklarowane w konstruktorach klas `ArticleFactory` i `EditController` Nette DI odnajduje i przekazuje automatycznie dzięki tak zwanemu [autowiringowi|autowiring], nie trzeba więc niczego podawać w pliku konfiguracyjnym. Nawet jeśli parametry się zmienią, nie musisz niczego zmieniać w konfiguracji. Podczas tworzenia aplikacji Nette automatycznie regeneruje kontener. Możesz skupić się wyłącznie na rozwijaniu aplikacji. - -Jeśli chcemy przekazywać zależności przez settery, użyjemy do tego sekcji [setup |services#Setup]. - -Nette DI generuje kod PHP kontenera bezpośrednio. Wynikiem jest więc plik `.php`, który możesz otworzyć i zbadać. Pozwala to zobaczyć dokładnie, jak kontener działa. Możesz też debugować go w swoim IDE i krokować po jego wykonaniu. A co najważniejsze: wygenerowany kod PHP jest wyjątkowo szybki. - -Nette DI potrafi też wygenerować kod [fabryki|factory] na podstawie podanego interfejsu. Zamiast klasy `ArticleFactory` wystarczy więc utworzyć w aplikacji interfejs: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Pełny przykład znajdziesz [na GitHubie|https://github.com/nette-examples/di-example-doc]. - - -Użycie samodzielne ------------------- - -Integracja biblioteki Nette DI z aplikacją jest bardzo łatwa. Najpierw instalujemy ją przez Composera (bo pobieranie plików zip jest już takie przestarzałe): - -```shell -composer require nette/di -``` - -Poniższy kod używa [Compilera |api:Nette\DI\Compiler] do utworzenia instancji kontenera DI zgodnie z konfiguracją zapisaną w pliku `config.neon`: - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp'); -$class = $loader->load(function ($compiler) { - $compiler->loadConfig(__DIR__ . '/config.neon'); -}); -$container = new $class; -``` - -Kontener generowany jest tylko raz, jego kod zapisywany jest do cache (katalog `__DIR__ . '/temp'`), a przy kolejnych żądaniach jest już tylko stamtąd wczytywany. - -Sam `Compiler` włącza w konfiguracji tylko sekcje `services` i `parameters`. Aby użyć innych, takich jak `search`, `decorator`, `di` czy `inject`, zarejestruj najpierw ich rozszerzenia. A aby umożliwić rejestrowanie rozszerzeń z sekcji `extensions` konfiguracji, dodaj `ExtensionsExtension`: - -```php -$compiler->addExtension('search', new Nette\DI\Extensions\SearchExtension($tempDir)); -$compiler->addExtension('extensions', new Nette\DI\Extensions\ExtensionsExtension); -``` - -[Configurator |application:bootstrapping] używany w pełnych aplikacjach Nette rejestruje je wszystkie automatycznie. - -Jeśli trzymasz kilka różnych kontenerów w tym samym katalogu cache, odróżnij je kluczem przekazanym jako drugi argument `load()`; staje się on częścią nazwy wygenerowanej klasy: - -```php -$class = $loader->load( - fn($compiler) => $compiler->loadConfig(__DIR__ . '/config.neon'), - 'my-key', -); -``` - -Do tworzenia i pobierania usług służą metody `getService()` albo `getByType()`. Tak utworzymy obiekt `EditController`: - -```php -$controller = $container->getByType(EditController::class); -$controller->someMethod(); -``` - -Podczas tworzenia aplikacji przydaje się włączenie trybu automatycznego odświeżania, w którym kontener regeneruje się automatycznie, gdy zmieni się jakakolwiek klasa albo plik konfiguracyjny. Wystarczy podać `true` jako drugi argument konstruktora [ContainerLoader |api:Nette\DI\ContainerLoader]. - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp', true); -``` - - -Praca z kontenerem ------------------- - -Poza `getService()` i `getByType()` obiekt kontenera oferuje kilka innych przydatnych metod: - -- `getByType(string $type, bool $throw = true): ?object` zwraca usługę podanego typu. Jeśli jako drugi argument podasz `false`, przy braku takiej usługi zwróci `null` zamiast zgłaszać wyjątek. -- `hasService(string $name): bool` i `isCreated(string $name): bool` mówią, czy usługa jest zdefiniowana i czy jej instancja została już utworzona. -- `getParameters(): array` zwraca wszystkie parametry kontenera, `getParameter($key)` zwraca pojedynczy. -- `createInstance(string $class, array $args = []): object` tworzy nową instancję podanej klasy i przekazuje zależności jej konstruktora przez autowiring. -- `callMethod(callable $function, array $args = []): mixed` wywołuje podany callable i przekazuje jego argumenty przez autowiring. -- `callInjects(object $service): void` wywołuje na podanym obiekcie wszystkie metody `inject*()` i przekazuje im zależności. - -Konstruktor kontenera przyjmuje też tablicę parametrów, które uzupełniają te zdefiniowane w konfiguracji: - -```php -$container = new $class(['host' => 'localhost']); -``` - - -Użycie z Nette Framework ------------------------- - -Jak pokazaliśmy, użycie Nette DI nie ogranicza się do aplikacji zbudowanych na Nette Framework; możesz zintegrować je gdziekolwiek zaledwie trzema wierszami kodu. Jeśli jednak tworzysz aplikacje z użyciem Nette Framework, konfiguracją i utworzeniem kontenera zajmuje się [Bootstrap |application:bootstrapping#Konfiguracja kontenera DI]. diff --git a/dependency-injection/pl/passing-dependencies.texy b/dependency-injection/pl/passing-dependencies.texy deleted file mode 100644 index 6f906dee13..0000000000 --- a/dependency-injection/pl/passing-dependencies.texy +++ /dev/null @@ -1,215 +0,0 @@ -Przekazywanie zależności -************************ - -<div class=perex> - -Argumenty, czyli w terminologii DI "zależności", można przekazywać klasom na następujące główne sposoby: - -* przez konstruktor -* przez metodę (tak zwany setter injection) -* przez właściwość -* za pomocą metody `inject*()` albo atrybutu `#[Inject]` - -</div> - -Pokażmy każdy wariant na konkretnych przykładach. - - -Przekazywanie przez konstruktor -=============================== - -Zależności podawane są jako argumenty konstruktora w chwili tworzenia instancji obiektu: - -```php -class MyClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -$obj = new MyClass($cache); -``` - -To podejście nadaje się do zależności obowiązkowych, których klasa bezwzględnie potrzebuje do działania, bo bez nich nie da się utworzyć instancji. - -Od PHP 8.0 możemy użyć krótszego zapisu ([promocja właściwości w konstruktorze |https://blog.nette.org/pl/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), funkcjonalnie równoważnego: - -```php -// PHP 8.0 -class MyClass -{ - public function __construct( - private Cache $cache, - ) { - } -} -``` - -Od PHP 8.1 właściwość można oznaczyć flagą `readonly`, która deklaruje, że wartość właściwości nie zmieni się po inicjalizacji: - -```php -// PHP 8.1 -class MyClass -{ - public function __construct( - private readonly Cache $cache, - ) { - } -} -``` - -Kontener DI przekazuje zależności do konstruktora automatycznie za pomocą [autowiringu |autowiring]. Argumenty, których nie da się w ten sposób podać (np. stringi, liczby, wartości logiczne), [podaje się w konfiguracji |services#Argumenty]. - - -Piekło konstruktorów --------------------- - -Termin *piekło konstruktorów* opisuje sytuację, w której klasa potomna dziedziczy po klasie nadrzędnej, której konstruktor wymaga zależności, a klasa potomna również wymaga zależności. Musi wtedy przyjąć i przekazać dalej również zależności rodzica: - -```php -abstract class BaseClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass extends BaseClass -{ - private Database $db; - - // ⛔ PIEKŁO KONSTRUKTORÓW - public function __construct(Cache $cache, Database $db) - { - parent::__construct($cache); - $this->db = $db; - } -} -``` - -Problem pojawia się, gdy chcemy zmienić konstruktor klasy `BaseClass`, na przykład gdy dochodzi nowa zależność. Wtedy trzeba zmodyfikować również wszystkie konstruktory klas potomnych. Co zamienia taką modyfikację w piekło. - -Jak temu zapobiec? Rozwiązaniem jest **preferowanie [kompozycji nad dziedziczeniem |faq#Dlaczego kompozycja jest preferowana nad dziedziczeniem?]**. - -Projektujemy więc kod inaczej. Unikniemy [abstrakcyjnych |nette:introduction-to-object-oriented-programming#Klasy abstrakcyjne] klas `Base*`. Zamiast tego, aby `MyClass` uzyskiwała pewną funkcjonalność przez dziedziczenie po `BaseClass`, otrzyma tę funkcjonalność przekazaną jako zależność: - -```php -final class SomeFunctionality -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass -{ - private SomeFunctionality $sf; - private Database $db; - - public function __construct(SomeFunctionality $sf, Database $db) // ✅ - { - $this->sf = $sf; - $this->db = $db; - } -} -``` - - -Przekazywanie przez setter -========================== - -Zależności podawane są przez wywołanie metody, która zapisuje je do prywatnej właściwości. Powszechną konwencją nazewniczą tych metod jest wzorzec `set*()`, stąd nazywane są setterami, ale oczywiście mogą nazywać się inaczej. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - $this->cache = $cache; - } -} - -$obj = new MyClass; -$obj->setCache($cache); -``` - -To podejście nadaje się do zależności opcjonalnych, które nie są niezbędne do działania klasy, bo nie ma gwarancji, że obiekt rzeczywiście otrzyma zależność (czyli że wywołujący wywoła metodę). - -Jednocześnie metoda ta pozwala wywoływać setter wielokrotnie, aby zmienić zależność. Jeśli jest to niepożądane, dodaj w metodzie sprawdzenie albo, od PHP 8.1, oznacz właściwość `$cache` flagą `readonly`. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - if (isset($this->cache)) { - throw new RuntimeException('The dependency has already been set'); - } - $this->cache = $cache; - } -} -``` - -Wywołanie settera definiuje się w konfiguracji kontenera DI pod [kluczem setup |services#Setup]. Również tutaj używane jest automatyczne podawanie zależności przez autowiring: - -```neon -services: - - create: MyClass - setup: - - setCache -``` - - -Przekazywanie przez właściwość -============================== - -Zależności podawane są przez bezpośredni zapis do właściwości składowej: - -```php -class MyClass -{ - public Cache $cache; -} - -$obj = new MyClass; -$obj->cache = $cache; -``` - -Metoda ta uznawana jest za nieodpowiednią, bo właściwość składowa musi być zadeklarowana jako `public`. W konsekwencji tracimy kontrolę nad tym, czy przekazana zależność rzeczywiście jest wymaganego typu (dotyczyło to zwłaszcza czasów przed deklaracjami typów właściwości w PHP 7.4) i tracimy możliwość zareagowania własną logiką na nowo przypisaną zależność, na przykład aby zapobiec późniejszej modyfikacji. Jednocześnie właściwość staje się częścią publicznego API klasy, co może nie być zamierzone. - -Przypisanie do właściwości definiuje się w konfiguracji kontenera DI w [sekcji setup |services#Setup]: - -```neon -services: - - create: MyClass - setup: - - $cache = @\Cache -``` - - -Inject -====== - -Podczas gdy poprzednie trzy podejścia obowiązują ogólnie we wszystkich językach obiektowych, wstrzykiwanie przez metody `inject*()` albo atrybut `#[Inject]` używane jest typowo z presenterami Nette, gdzie jest domyślnie włączone; dowolna inna usługa może włączyć je przez [`inject: true` |services#Tryb inject]. Omówione są w [osobnym rozdziale |best-practices:inject-method-attribute]. - - -Który sposób wybrać? -==================== - -- Konstruktor nadaje się do zależności obowiązkowych, których klasa bezwzględnie potrzebuje do działania. -- Setter przeciwnie, nadaje się do zależności opcjonalnych albo takich, które trzeba będzie później zmienić. -- Właściwości publiczne generalnie nie są zalecane. diff --git a/dependency-injection/pl/services.texy b/dependency-injection/pl/services.texy deleted file mode 100644 index 660ce57a23..0000000000 --- a/dependency-injection/pl/services.texy +++ /dev/null @@ -1,480 +0,0 @@ -Definiowanie usług -****************** - -.[perex] -W konfiguracji instruujemy kontener DI, jak ma tworzyć poszczególne usługi i jak łączyć je z ich zależnościami. Nette oferuje na to bardzo przejrzysty i elegancki sposób. - -Sekcja `services` w pliku konfiguracyjnym NEON to miejsce, w którym definiujemy własne usługi i ich konfigurację. Spójrzmy na prosty przykład definiujący usługę o nazwie `database`, reprezentującą instancję klasy `PDO`: - -```neon -services: - database: PDO('sqlite::memory:') -``` - -Powyższa konfiguracja daje w [kontenerze DI|container] następującą metodę fabryczną: - -```php -public function createServiceDatabase(): PDO -{ - return new PDO('sqlite::memory:'); -} -``` - -Nazwy usług pozwalają odwoływać się do nich w innych częściach pliku konfiguracyjnego, w formacie `@nazwaUsługi`. Jeśli nie ma potrzeby nadawania usłudze nazwy, możemy po prostu użyć myślnika (`-`): - -```neon -services: - - PDO('sqlite::memory:') -``` - -Aby pobrać usługę z kontenera DI, możemy użyć metody `getService()` z nazwą usługi jako parametrem albo metody `getByType()` z typem usługi: - -```php -$database = $container->getService('database'); -$database = $container->getByType(PDO::class); -``` - - -Tworzenie usługi -================ - -Zwykle tworzymy usługę po prostu przez utworzenie instancji konkretnej klasy. Na przykład: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Jeśli potrzebujemy rozbudować konfigurację o kolejne klucze, definicję można rozbić na wiele wierszy: - -```neon -services: - database: - create: PDO('sqlite::memory:') - setup: ... -``` - -Klucz `create` ma alias `factory`; oba warianty są powszechnie używane. Zalecamy jednak `create`. - -Argumenty konstruktora albo metody fabrycznej można alternatywnie podać kluczem `arguments`: - -```neon -services: - database: - create: PDO - arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] -``` - -Usługi nie muszą powstawać przez zwykłe utworzenie instancji klasy; mogą też być wynikiem wywołania metod statycznych albo metod innych usług: - -```neon -services: - database: DatabaseFactory::create() - router: @routerFactory::create() -``` - -Zwróć uwagę, że dla uproszczenia zamiast `->` używa się `::`, zobacz [#Język wyrażeń]. Wygenerowane zostaną takie metody fabryczne: - -```php -public function createServiceDatabase(): PDO -{ - return DatabaseFactory::create(); -} - -public function createServiceRouter(): RouteList -{ - return $this->getService('routerFactory')->create(); -} -``` - -Kontener DI musi znać typ tworzonej usługi. Jeśli tworzymy usługę metodą, która nie ma podanego typu zwracanego, musimy podać ten typ jawnie w konfiguracji: - -```neon -services: - database: - create: DatabaseFactory::create() - type: PDO -``` - - -Argumenty -========= - -Argumenty do konstruktorów i metod przekazujemy bardzo podobnie jak w samym PHP: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Dla lepszej czytelności możemy wypisać argumenty w osobnych wierszach. W takim przypadku używanie przecinków staje się opcjonalne: - -```neon -services: - database: PDO( - 'mysql:host=127.0.0.1;dbname=test' - root - secret - ) -``` - -Argumenty możesz też nazwać, dzięki czemu nie musisz przejmować się ich kolejnością: - -```neon -services: - database: PDO( - username: root - password: secret - dsn: 'mysql:host=127.0.0.1;dbname=test' - ) -``` - -Jeśli chcesz pominąć niektóre argumenty i użyć ich wartości domyślnych albo pozwolić, aby usługa została wstrzyknięta przez [autowiring|autowiring], użyj podkreślenia (`_`): - -```neon -services: - foo: Foo(_, %appDir%) -``` - -Argumenty mogą zawierać usługi, parametry i wiele więcej, zobacz [#Język wyrażeń]. - - -Setup -===== - -W sekcji `setup` definiujemy metody, które mają zostać wywołane przy tworzeniu usługi. - -```neon -services: - database: - create: PDO(%dsn%, %user%, %password%) - setup: - - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) -``` - -W PHP wyglądałoby to tak: - -```php -public function createServiceDatabase(): PDO -{ - $service = new PDO('...', '...', '...'); - $service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - return $service; -} -``` - -Poza wywołaniami metod można też przypisywać wartości do właściwości. Obsługiwane jest również dodawanie elementów do tablic, co wymaga ujęcia dostępu do tablicy w cudzysłowy, aby uniknąć konfliktu ze składnią NEON: - -```neon -services: - foo: - create: Foo - setup: - - $value = 123 - - '$onClick[]' = [@bar, clickHandler] -``` - -W kodzie PHP wyglądałoby to następująco: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - $service->value = 123; - $service->onClick[] = [$this->getService('bar'), 'clickHandler']; - return $service; -} -``` - -W setupie możesz jednak wywoływać również metody statyczne albo metody innych usług. Jeśli potrzebujesz przekazać jako argument samą bieżącą usługę, odwołaj się do niej przez `@self`: - -```neon -services: - foo: - create: Foo - setup: - - My\Helpers::initializeFoo(@self) - - @anotherService::setFoo(@self) -``` - -Zwróć uwagę, że dla uproszczenia zamiast `->` używa się `::`, zobacz [#Język wyrażeń]. Wygenerowana zostanie taka metoda fabryczna: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - My\Helpers::initializeFoo($service); - $this->getService('anotherService')->setFoo($service); - return $service; -} -``` - - -Język wyrażeń -============= - -Nette DI udostępnia wyjątkowo bogaty język wyrażeń, którym możemy zdefiniować niemal wszystko. W plikach konfiguracyjnych możemy więc używać [parametrów |configuration#Parametry]: - -```neon -# parametr -%wwwDir% - -# wartość parametru pod kluczem -%mailer.user% - -# parametr wewnątrz stringa -'%wwwDir%/images' -``` - -Ponadto tworzyć obiekty, wywoływać metody i funkcje: - -```neon -# utworzenie obiektu -DateTime() - -# wywołanie metody statycznej -Collator::create(%locale%) - -# wywołanie funkcji PHP -::getenv(DB_USER) -``` - -Odwoływać się do usług po nazwie albo po typie: - -```neon -# usługa po nazwie -@database - -# usługa po typie -@Nette\Database\Connection -``` - -Używać składni first-class callable: .{data-version:3.2.0} - -```neon -# utworzenie callbacku, odpowiednik [@user, logout] -@user::logout(...) -``` - -Używać stałych: - -```neon -# stała klasowa -FilesystemIterator::SKIP_DOTS - -# pobranie stałej globalnej funkcją PHP constant() -::constant(\PHP_VERSION) -``` - -Sięgać po publiczne właściwości i stałe usługi przez `@service::member`. O tym, czy nazwa oznacza właściwość, czy stałą, decyduje jej pierwsza litera - mała oznacza właściwość publiczną, wielka oznacza stałą: - -```neon -# właściwość publiczna usługi (zaczyna się małą literą) -@settings::apiUrl - -# stała klasowa usługi (zaczyna się wielką literą) -@settings::Version -``` - -Wywołania metod można łączyć w łańcuch, tak jak w PHP. Dla uproszczenia zamiast `->` używa się `::`: - -```neon -DateTime()::format('Y-m-d') -# PHP: (new DateTime())->format('Y-m-d') - -@http.request::getUrl()::getHost() -# PHP: $this->getService('http.request')->getUrl()->getHost() -``` - -Wyrażeń tych możesz używać wszędzie: przy [tworzeniu usług |#Tworzenie usługi], w [argumentach |#Argumenty], w sekcji [setup |#Setup] albo w [parametrach |configuration#Parametry]: - -```neon -parameters: - ipAddress: @http.request::getRemoteAddress() - -services: - database: - create: DatabaseFactory::create( @anotherService::getDsn() ) - setup: - - initialize( ::getenv('DB_USER') ) -``` - - -Funkcje specjalne ------------------ - -W plikach konfiguracyjnych możesz używać następujących funkcji specjalnych: - -- `not()` neguje wartość -- `bool()`, `int()`, `float()`, `string()` bezstratne rzutowanie na podany typ .{data-version:3.0.5} -- `typed()` tworzy tablicę wszystkich usług podanego typu -- `tagged()` tworzy tablicę wszystkich usług z danym tagiem - -```neon -services: - - Foo( - id: int(::getenv('ProjectId')) - productionMode: not(%debugMode%) - ) -``` - -W odróżnieniu od standardowego rzutowania PHP, jak `(int)`, rzutowanie bezstratne zgłasza wyjątek dla wartości nieliczbowych. - -Funkcja `typed()` tworzy tablicę wszystkich usług podanego typu (klasy albo interfejsu). Pomija usługi z wyłączonym autowiringiem. Można podać również kilka typów, oddzielonych przecinkami. - -```neon -services: - - BarsDependent( typed(Bar) ) -``` - -Tablicę usług określonego typu można też przekazać jako argument automatycznie, za pomocą [autowiringu |autowiring#Kolekcja usług]. - -Funkcja `tagged()` tworzy z kolei tablicę wszystkich usług z określonym tagiem. Również tutaj możesz podać kilka tagów oddzielonych przecinkami. - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - - -Autowiring -========== - -Klucz `autowired` pozwala wpłynąć na zachowanie autowiringu dla konkretnej usługi. Szczegóły znajdziesz w [rozdziale o autowiringu|autowiring]. - -```neon -services: - foo: - create: Foo - autowired: false # usługa foo jest wyłączona z autowiringu -``` - - -Usługi leniwe .{data-version:3.2.4} -=================================== - -Leniwe ładowanie to technika odraczająca utworzenie usługi do chwili, gdy jest ona faktycznie potrzebna. W konfiguracji globalnej możesz [włączyć leniwe tworzenie |configuration#Usługi leniwe] dla wszystkich usług naraz. Dla poszczególnych usług możesz potem to zachowanie nadpisać: - -```neon -services: - foo: - create: Foo - lazy: false -``` - -Gdy usługa jest zdefiniowana jako leniwa, przy prośbie o nią do kontenera DI otrzymujemy specjalny obiekt proxy. Proxy to wygląda i zachowuje się identycznie jak prawdziwa usługa, ale faktyczna inicjalizacja (wywołanie konstruktora i wywołania setupu) następuje dopiero przy pierwszym dostępie do którejkolwiek z jej metod albo właściwości. - -Pamiętaj, że skoro usługa tworzona jest później, również błędy w jej konfiguracji ujawniają się później. Na przykład nieprawidłowe poświadczenia bazy danych nie ujawnią się przy starcie aplikacji, lecz dopiero przy pierwszym zapytaniu. - -Leniwe tworzenie łagodzi też zależności cykliczne, czyli sytuację, w której usługa A wymaga usługi B, a B jednocześnie wymaga A. Bez niego kontener zgłasza błąd `Circular reference detected`. Z leniwym proxy usługa A otrzymuje tylko proxy usługi B, które inicjalizuje się przy faktycznym użyciu, w momencie, gdy A już istnieje. Mimo to zależność cykliczna sygnalizuje wadliwy projekt i lepiej się jej pozbyć. - -.[note] -Leniwe ładowanie wymaga PHP 8.4 lub nowszego i działa tylko dla usług tworzonych przez bezpośrednie utworzenie instancji klasy (np. `create: Foo`), a nie dla tych tworzonych metodą fabryczną. Nie da się go też użyć dla klas, które ostatecznie rozszerzają wewnętrzną klasę PHP. Gdy leniwego ładowania nie da się zastosować, flaga `lazy: true` jest po cichu ignorowana. - - -Tagi -==== - -Tagi służą do dodawania usługom dodatkowych informacji. Usłudze możesz przypisać jeden albo więcej tagów: - -```neon -services: - foo: - create: Foo - tags: - - cached -``` - -Tagi mogą też przechowywać wartości: - -```neon -services: - foo: - create: Foo - tags: - logger: monolog.logger.event -``` - -Aby pobrać wszystkie usługi powiązane z określonymi tagami, możesz użyć funkcji `tagged()`: - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - -W kontenerze DI nazwy wszystkich usług z określonym tagiem pobierzesz metodą `findByTag()`: - -```php -$names = $container->findByTag('logger'); -// $names to tablica z nazwami usług jako kluczami i wartościami tagów jako wartościami -// np. ['foo' => 'monolog.logger.event', ...] -``` - - -Tryb inject -=========== - -Flaga `inject: true` włącza wstrzykiwanie zależności przez właściwości publiczne z atrybutem [Inject |best-practices:inject-method-attribute#Atrybuty Inject] i metody [inject*() |best-practices:inject-method-attribute#Metody inject*()]. - -```neon -services: - articles: - create: App\Model\Articles - inject: true -``` - -Domyślnie tryb `inject` włączony jest tylko dla presenterów. - - -Modyfikowanie usług -=================== - -Kontener DI zawiera liczne usługi dodane przez rozszerzenia wbudowane albo [użytkownika|extensions]. Definicje tych istniejących usług możesz modyfikować bezpośrednio w konfiguracji. Możesz na przykład zmienić klasę usługi `application.application`, którą domyślnie jest `Nette\Application\Application`, na inną: - -```neon -services: - application.application: - create: MyApplication - alteration: true -``` - -Flaga `alteration` wskazuje, że jedynie modyfikujemy istniejącą usługę. Działa też jako zabezpieczenie: jeśli modyfikowana usługa nie istnieje, kompilacja kończy się wyjątkiem. - -Możemy też uzupełnić setup: - -```neon -services: - application.application: - create: MyApplication - alteration: true - setup: - - '$onStartup[]' = [@resource, init] -``` - -Usługi nie musisz identyfikować jej wewnętrzną nazwą - możesz odwołać się do niej przez typ. Poprzedni przykład można zapisać także tak: - -```neon -services: - @Nette\Application\Application: - create: MyApplication -``` - -Modyfikując usługę, możemy chcieć usunąć pierwotne argumenty, elementy setupu albo tagi, używając klucza `reset`: - -```neon -services: - application.application: - create: MyApplication - alteration: true - reset: - arguments: true - setup: true - tags: true -``` - -Jeśli chcesz usunąć usługę dodaną przez rozszerzenie, możesz zrobić to tak: - -```neon -services: - cache.journal: false -``` diff --git a/dependency-injection/pl/upgrading.texy b/dependency-injection/pl/upgrading.texy deleted file mode 100644 index 9ae2f60298..0000000000 --- a/dependency-injection/pl/upgrading.texy +++ /dev/null @@ -1,49 +0,0 @@ -Aktualizacja -************ - - -Aktualizacja do wersji 3.1 -========================== - -- autowiring nie przekazuje już `null` do parametru nullable bez wartości domyślnej; przekaż argument jawnie albo nadaj parametrowi wartość domyślną -- wsparcie dla adnotacji `@return` zostało usunięte; użyj typu zwracanego albo podaj typ w definicji usługi przez `type:` -- klucz `dynamic` zmienił nazwę na `imported`, a `class` na `type` -- symbol pominiętego argumentu zmienił się z `...` na `_`, np. `MyService(_, 123)` -- w plikach NEON znaku `@` na początku stringa nie trzeba już escapować -- klucz `parameters` wewnątrz definicji generowanych fabryk jest przestarzały -- metoda `Nette\DI\Config\Loader::save()` jest przestarzała; eksportuj konfigurację przez `Nette\DI\Config\Adapters\NeonAdapter::dump()` - -Wersja 3.1 jest wydaniem przejściowym: nie przynosi nowych funkcji, ale ostrzega notice'ami przed wszystkim, co później będzie działać inaczej. Zobacz artykuł [Nette DI 3.1: wydanie przejściowe |https://blog.nette.org/en/nette-di-3-1-transition-release]. - - -Aktualizacja do wersji 3.0 -========================== - -- wsparcie dla plików INI zostało usunięte -- usunięto bezpośrednie zapisywanie kodu PHP w konfiguracji za pomocą znaków zapytania (np. `"$service->onError[] = ?"(...)`); użyj zamiast tego składni tablicowej `'$onError[]' = [...]` -- w plikach konfiguracyjnych używaj `factory: PDO(...)` zamiast `class: PDO(...)` -- tag `nette.presenter` nie jest już używany dla presenterów - - -Dla autorów rozszerzeń kompilatora ----------------------------------- - -Podczas gdy Nette 2.4 wewnętrznie opisywało każdą usługę jako `Nette\DI\ServiceDefinition`, teraz istnieje kilka typów definicji: `Nette\DI\Definitions\ImportedDefinition` dla usług importowanych (dynamicznych), `Nette\DI\Definitions\FactoryDefinition` dla generowanych fabryk opartych na interfejsach, `Nette\DI\Definitions\AccessorDefinition` dla generowanych akcesorów i `Nette\DI\Definitions\ServiceDefinition` dla zwykłych usług. - -Dlatego poza `ContainerBuilder::addDefinition()` istnieje kilka innych metod tworzenia nowej definicji: `addFactoryDefinition()`, `addAccessorDefinition()` i `addImportedDefinition()`. - - -Aktualizacja do wersji 2.4 -========================== - -- sekcje konfiguracyjne (np. production, development) w jednym pliku konfiguracyjnym są przestarzałe; użyj pary plików `config.neon` i `config.local.neon` -- dziedziczenie definicji usług jest przestarzałe -- `Statement::setEntity()` jest przestarzałe - - -Aktualizacja do wersji 2.3 -========================== - -- usunięto wsparcie dla umieszczania usług wewnątrz sekcji rozszerzenia w pliku konfiguracyjnym -- usunięto wsparcie dla rozszerzeń dodawanych dynamicznie -- przy dynamicznym zastępowaniu usługi (przez `removeService()`, `addService()`) nowa usługa musi być instancją tego samego interfejsu lub klasy co pierwotna diff --git a/dependency-injection/ru/@home.texy b/dependency-injection/ru/@home.texy deleted file mode 100644 index c2e6f046ae..0000000000 --- a/dependency-injection/ru/@home.texy +++ /dev/null @@ -1,22 +0,0 @@ -Nette DI -******** - -.[perex] -Dependency Injection - шаблон проектирования, который в корне изменит ваш взгляд на код и разработку. Он открывает дорогу в мир чисто спроектированных и жизнеспособных приложений. - -- [Что такое Dependency Injection? |introduction] -- [Глобальное состояние и синглтоны |global-state] -- [Передача зависимостей |passing-dependencies] -- [Что такое DI-контейнер? |container] -- [Часто задаваемые вопросы |faq] - - -Пакет `nette/di` предоставляет чрезвычайно продвинутый компилируемый DI-контейнер для PHP. - -- [Nette DI Container |nette-container] -- [Конфигурация |configuration] -- [Определение сервисов |services] -- [Autowiring |autowiring] -- [Генерируемые фабрики |factory] -- [Создание расширений для Nette DI |extensions] -- [Компиляция в подробностях |compilation-internals] diff --git a/dependency-injection/ru/@left-menu.texy b/dependency-injection/ru/@left-menu.texy deleted file mode 100644 index 31eccac945..0000000000 --- a/dependency-injection/ru/@left-menu.texy +++ /dev/null @@ -1,28 +0,0 @@ -Dependency Injection -******************** -- [Что такое DI? |introduction] -- [Глобальное состояние и синглтоны |global-state] -- [Передача зависимостей |passing-dependencies] -- [Что такое DI-контейнер? |container] -- [Часто задаваемые вопросы |faq] - - -Nette DI --------- -- [Nette DI Container |nette-container] -- [Конфигурация |configuration] -- [Определение сервисов |services] -- [Autowiring |autowiring] -- [Генерируемые фабрики |factory] -- [Создание расширений для Nette DI |extensions] -- [Компиляция в подробностях |compilation-internals] -- [Обновление|upgrading] - - -Дополнительные материалы -************************ -- [Документация Nette |nette:] -- [Nette Application |application:how-it-works] -- [Utilities |utils:] -- [Лучшие практики |best-practices:] -- [Устранение неполадок |nette:troubleshooting] diff --git a/dependency-injection/ru/@meta.texy b/dependency-injection/ru/@meta.texy deleted file mode 100644 index 7f329adfce..0000000000 --- a/dependency-injection/ru/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документация Nette}} diff --git a/dependency-injection/ru/autowiring.texy b/dependency-injection/ru/autowiring.texy deleted file mode 100644 index 23ebf399c8..0000000000 --- a/dependency-injection/ru/autowiring.texy +++ /dev/null @@ -1,279 +0,0 @@ -Autowiring -********** - -.[perex] -Autowiring - прекрасная возможность, которая автоматически передаёт нужные сервисы в конструктор и другие методы, так что нам не приходится указывать их явно. Это экономит вам массу времени. - -Благодаря ему мы можем опускать подавляющее большинство аргументов при написании определений сервисов. Вместо: - -```neon -services: - articles: Model\ArticleRepository(@database, @cache.storage) -``` - -Достаточно написать: - -```neon -services: - articles: Model\ArticleRepository -``` - -Autowiring руководствуется типами, поэтому, чтобы он работал, класс `ArticleRepository` должен быть определён примерно так: - -```php -namespace Model; - -class ArticleRepository -{ - public function __construct(\PDO $db, \Nette\Caching\Storage $storage) - {} -} -``` - -Autowiring никогда не использует имена сервисов. Он руководствуется исключительно системой типов PHP, поэтому знает и то, что класс удовлетворяет интерфейсам, которые он реализует, и классам, от которых он наследуется. Благодаря этому имя сервиса - лишь вспомогательный идентификатор, и его переименование ничего в приложении не сломает. - -Чтобы autowiring можно было использовать, в контейнере должен быть **ровно один сервис** каждого типа. Если бы их было больше, autowiring не знал бы, какой передать, и выбросил бы исключение: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - tempDb: PDO('sqlite::memory:') - articles: Model\ArticleRepository # ВЫБРОСИТ ИСКЛЮЧЕНИЕ, подходят и mainDb, и tempDb -``` - -Одно из решений - обойти autowiring и явно указать имя сервиса (например, `articles: Model\ArticleRepository(@mainDb)`). Однако удобнее либо [отключить |#Отключение autowiring] autowiring для одного из сервисов, либо [сделать |#Предпочтительный сервис для autowiring] один сервис предпочтительным перед остальными. - - -Отключение autowiring ---------------------- - -Мы можем отключить autowiring для сервиса параметром `autowired: false`: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - - tempDb: - create: PDO('sqlite::memory:') - autowired: false # сервис tempDb исключён из autowiring - - articles: Model\ArticleRepository # поэтому в конструктор передаётся mainDb -``` - -Сервис `articles` не выбросит исключение о том, что для конструктора доступны два подходящих сервиса `PDO` (`mainDb` и `tempDb`), потому что он видит только сервис `mainDb`. - -Autowiring можно отключить и глобально для целых типов параметром конфигурации [`di › excluded` |configuration#DI], где перечисляются типы (и их потомки), которые никогда не должны попадать в autowiring. - -.[note] -Настройка autowiring в Nette отличается от Symfony. В Symfony `autowire: false` означает, что autowiring не нужно использовать для аргументов конструктора сервиса. В Nette autowiring относится к аргументам конструктора и любых других методов, вызываемых через контейнер (например, при внедрении через сеттер). Параметр `autowired: false` не даёт контейнеру автоматически передавать экземпляр этого сервиса как зависимость другим сервисам. - - -Предпочтительный сервис для autowiring --------------------------------------- - -Если у нас несколько сервисов одного типа и для одного из них мы задаём параметр `autowired`, этот сервис становится предпочтительным: - -```neon -services: - mainDb: - create: PDO(%dsn%, %user%, %password%) - autowired: PDO # становится предпочтительным - - tempDb: - create: PDO('sqlite::memory:') - - articles: Model\ArticleRepository -``` - -Сервис `articles` не выбросит исключение о нескольких подходящих сервисах `PDO` (`mainDb` и `tempDb`), а использует предпочтительный, то есть `mainDb`. - - -Набор сервисов --------------- - -Autowiring умеет передавать и массивы сервисов определённого типа. Поскольку PHP не поддерживает указание типа элементов массива в объявлениях типов, вам нужно дополнить объявление типа `array` комментарием phpDoc с типом элементов, например `ClassName[]`: - -```php -namespace Model; - -class ShipManager -{ - /** - * @param Shipper[] $shippers - */ - public function __construct(array $shippers) - {} -} -``` - -DI-контейнер тогда автоматически передаст массив сервисов соответствующего типа. Он пропускает сервисы, у которых [autowiring отключён |#Отключение autowiring], и никогда не включает создаваемый в данный момент сервис в его собственный набор. В отличие от передачи отдельного сервиса, [сужение |#Сужение autowiring] autowiring до определённого типа или пометка сервиса как [предпочтительного |#Предпочтительный сервис для autowiring] здесь не действуют: в массиве всегда оказываются все сервисы заданного типа. - -Тип в комментарии может иметь и вид `array<int, Class>` или `list<Class>`. Если вы не можете управлять видом комментария phpDoc, вы можете передать массив сервисов прямо в конфигурации через [`typed()` |services#Специальные функции]. - - -Скалярные аргументы -------------------- - -Autowiring работает только для объектов и массивов объектов. Скалярные аргументы (например, строки, числа, логические значения) нужно [указывать в конфигурации |services#Аргументы]. Альтернатива - создать [объект настроек|best-practices:passing-settings-to-presenters], упаковывающий скалярное значение (или несколько значений). Такой объект затем можно передавать через autowiring. - -```php -class MySettings -{ - public function __construct( - // readonly можно использовать начиная с PHP 8.1 - public readonly bool $value, - ) - {} -} -``` - -Вы регистрируете его как сервис, добавив в конфигурацию: - -```neon -services: - - MySettings('any value') -``` - -Остальные классы затем могут запросить его через autowiring. - - -Необязательные зависимости --------------------------- - -Если у параметра конструктора или метода есть значение по умолчанию, а сервиса нужного типа в контейнере нет, autowiring не выбрасывает исключение: он просто пропускает аргумент, и используется значение по умолчанию. Так объявляются необязательные зависимости: - -```php -class Foo -{ - public function __construct( - private ?Logger $logger = null, - ) {} -} -``` - -Напротив, для параметра без значения по умолчанию отсутствие сервиса всегда приводит к исключению. - - -Сужение autowiring ------------------- - -Для отдельных сервисов autowiring можно сузить до определённых классов или интерфейсов. - -Обычно autowiring передаёт сервис в каждый параметр метода, типу которого сервис соответствует. Сужение означает, что мы устанавливаем условия, которым должны отвечать типы, указанные у параметров метода, чтобы сервис туда передавался. - -Возьмём пример: - -```php -class ParentClass -{} - -class ChildClass extends ParentClass -{} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Если бы мы зарегистрировали их все как сервисы, autowiring не справился бы: - -```neon -services: - parent: ParentClass - child: ChildClass - parentDep: ParentDependent # ВЫБРОСИТ ИСКЛЮЧЕНИЕ, подходят и parent, и child - childDep: ChildDependent # autowiring передаёт в конструктор сервис child -``` - -Сервис `parentDep` выбрасывает исключение `Multiple services of type ParentClass found: child, parent`, потому что в его конструктор подходят и сервис `parent`, и сервис `child`, а autowiring не может решить, какой выбрать. - -Поэтому для сервиса `child` мы можем сузить autowiring до типа `ChildClass`: - -```neon -services: - parent: ParentClass - child: - create: ChildClass - autowired: ChildClass # можно записать и как 'autowired: self' - - parentDep: ParentDependent # autowiring передаёт в конструктор сервис parent - childDep: ChildDependent # autowiring передаёт в конструктор сервис child -``` - -Теперь в конструктор сервиса `parentDep` передаётся сервис `parent`, потому что он стал единственным подходящим объектом. Сервис `child` autowiring туда больше не передаёт. Да, сервис `child` по-прежнему имеет тип `ParentClass`, но условие сужения `autowired: ChildClass` означает, что он будет передаваться только в параметры, явно объявленные как `ChildClass` (или его подтипы). Поскольку `ParentDependent` требует `ParentClass`, сервис `child` больше не считается там кандидатом на autowiring. - -Для сервиса `child` запись `autowired: ChildClass` можно было бы заменить на `autowired: self`, потому что `self` - это подстановка для класса текущего сервиса. - -В ключе `autowired` можно указать и несколько классов или интерфейсов массивом: - -```neon -autowired: [ParentClass, FooInterface] -``` - -Попробуем добавить в пример интерфейсы: - -```php -interface FooInterface -{} - -interface BarInterface -{} - -class ParentClass implements FooInterface -{} - -class ChildClass extends ParentClass implements BarInterface -{} - -class FooDependent -{ - function __construct(FooInterface $obj) - {} -} - -class BarDependent -{ - function __construct(BarInterface $obj) - {} -} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Если мы никак не ограничим сервис `child`, он подойдёт в конструкторы всех классов `FooDependent`, `BarDependent`, `ParentDependent` и `ChildDependent`, и autowiring передаст его туда. - -Однако если мы сузим его autowiring до `ChildClass` через `autowired: ChildClass` (или `self`), autowiring передаст его только в конструктор `ChildDependent`, потому что тот требует аргумент типа `ChildClass`, а `ChildClass` *имеет тип* `ChildClass`. Ни у одного другого параметра требуемый тип не является `ChildClass` или его подтипом, поэтому в них сервис не передаётся. - -Если мы ограничим его до `ParentClass` через `autowired: ParentClass`, autowiring снова передаст его в конструктор `ChildDependent` (потому что требуемый `ChildClass` - подтип `ParentClass`), а теперь ещё и в конструктор `ParentDependent`, потому что требуемый тип `ParentClass` тоже подходит. - -Если мы ограничим его до `FooInterface`, он по-прежнему будет передаваться в `ParentDependent` (требуемый `ParentClass` - подтип `FooInterface`) и `ChildDependent`, а дополнительно и в конструктор `FooDependent`, но не в `BarDependent`, потому что `BarInterface` не является подтипом `FooInterface`. - -```neon -services: - child: - create: ChildClass - autowired: FooInterface - - fooDep: FooDependent # autowiring передаёт в конструктор сервис child - barDep: BarDependent # ВЫБРОСИТ ИСКЛЮЧЕНИЕ, ни один сервис не подходит - parentDep: ParentDependent # autowiring передаёт в конструктор сервис child - childDep: ChildDependent # autowiring передаёт в конструктор сервис child -``` diff --git a/dependency-injection/ru/compilation-internals.texy b/dependency-injection/ru/compilation-internals.texy deleted file mode 100644 index 6a7c921834..0000000000 --- a/dependency-injection/ru/compilation-internals.texy +++ /dev/null @@ -1,222 +0,0 @@ -Компиляция в подробностях -************************* - -.[perex] -Эта страница раскрывает компиляцию контейнера: через какие фазы она проходит, когда разворачиваются параметры конфигурации, когда строки `@service` превращаются в настоящие ссылки и - вопрос, который авторы расширений задают чаще всего - в какой фазе можно спокойно искать сервисы по типу. Это углублённое дополнение к главе [Создание расширений |extensions]. - -Чтобы написать обычное приложение или даже обычное расширение, ничего из этого знать не нужно. Но как только ваше расширение начинает исследовать или перекраивать граф сервисов, момент становится решающим: один и тот же вызов `getByType()` в одной фазе даёт надёжный ответ, а в другой - обманчивый. Эта страница объясняет почему, чтобы вы всегда знали, где место вашему коду. - - -Два мира: компиляция и время выполнения -======================================= - -Самое важное, что нужно понять: контейнер Nette **не собирается при каждом запросе**. Он однажды собирается в оптимизированный PHP-класс, этот класс сохраняется на диск, и каждый последующий запрос лишь подключает готовый файл через `include`. Вся описанная ниже механика - расширения, резолверы, генератор кода - работает **только во время (пере)компиляции**. - -Это делит мир на два представления, которые никогда не сосуществуют: - -| | во время компиляции | во время выполнения -|---|---|--- -| Что существует | **определения** (рецепты) в `ContainerBuilder` | **экземпляры** сервисов в `Container` -| Ключевые классы | `Compiler`, `ContainerBuilder`, `Resolver`, `PhpGenerator` | `Container` (родитель порождённого класса) -| `%param%`, `@service` | текстовые пометки, ещё подлежащие переводу | уже переведены и вписаны в код - -Порождённый класс расширяет `Nette\DI\Container` и содержит по методу `createServiceXxx()` для каждого сервиса. Его параметры и метаданные autowiring вычислены заранее, поэтому во время выполнения разрешать уже нечего - остаётся только создавать сервисы по требованию. - -.[note] -В режиме разработки контейнер пересобирается автоматически при изменении конфигурационного файла или класса расширения; и то, и другое отслеживается как зависимость. В продакшене он компилируется однажды и больше не проверяется, откуда и берётся скорость. - - -Фазы вкратце -============ - -Компиляцией дирижирует `Compiler::compile()`, и сводится она к трём шагам: - -```php -public function compile(): string -{ - $this->processExtensions(); // ФАЗА A: схемы + loadConfiguration() - $this->processBeforeCompile(); // ФАЗА B: resolve + beforeCompile() + complete - return $this->generateCode(); // ФАЗА C: генерация кода + afterCompile() -} -``` - -Вся мысленная модель умещается в одну идею: **каждая фаза знает больше предыдущей.** - -- **Фаза A** наполняет граф определениями. Типы сервисов **ещё не известны надёжно**, потому что тип может происходить из возвращаемого значения фабрики, в которое никто ещё не заглядывал. -- **Фаза B** сначала разрешает все типы (`resolve`), затем позволяет расширениям перекроить граф (`beforeCompile`), а в конце заполняет аргументы через [autowiring |autowiring] (`complete`). -- **Фаза C** превращает готовый граф в PHP и позволяет расширениям поправить порождённый код. - -Именно это нарастающее знание объясняет, почему одна и та же операция в одной фазе безопасна, а в другой ненадёжна. Остальная часть страницы проходит по фазам с этой мыслью в голове. - - -Фаза A: регистрация определений -=============================== - -В этой фазе Nette вызывает у каждого расширения три метода - `getConfigSchema()`, затем `setConfig()`, затем `loadConfiguration()`, - но в **тщательно выверенном порядке**, потому что здесь порядок действительно важен. - - -Почему порядок важен --------------------- - -- **`ParametersExtension` и `ExtensionsExtension` идут первыми.** Первое должно отработать раньше всех, чтобы развернуть `%param%` по всей конфигурации: каждое следующее расширение получает свою секцию уже с подставленными значениями. Второе регистрирует дальнейшие расширения, перечисленные в секции `extensions:`, поэтому оно тоже должно существовать до обработки остальных. -- **`ServicesExtension` идёт последним.** Поэтому пользовательская секция `services:` всегда имеет последнее слово и может переопределить всё, что настроили расширения. -- **`InjectExtension` перенесено в самый конец**, чтобы его работа видела настройки, добавленные всеми остальными расширениями. - -Вывод для вас: к моменту, когда выполняется `loadConfiguration()` вашего расширения, параметры уже развёрнуты, но пользовательских сервисов ещё нет. Этот единственный факт определяет большинство правил о моментах ниже. - - -Превращение services: в определения ------------------------------------ - -Пользовательская секция `services:` превращается в [объекты определений |extensions#Типы определений] здесь, на последнем шаге фазы A. Каждая запись NEON нормализуется (краткие записи приводятся к единому виду), определяется её вид (обычный сервис, фабрика, аксессор, ...), и в билдере создаётся соответствующее определение. Это же первый момент, когда простые аргументы `@name` / `@Type` становятся ссылками, см. [ниже |#Ссылки: когда @service становится ссылкой]. - -К концу фазы A все определения на месте - каждое расширение и пользователь зарегистрировали то, что хотели, - но картина ещё не резкая: - -- **типы не разрешены** у определений, тип которых происходит из возвращаемого значения фабрики, -- **аргументы не заполнены autowiring**, -- часть ссылок `@service` всё ещё остаётся обычными строками. - -Именно поэтому поиск по типу здесь ненадёжен, подробнее [ниже |#Исследование ContainerBuilder: когда это безопасно]. - - -Параметры: когда разворачивается %param% -======================================== - -Один из двух главных вопросов. Ответ короткий: **однажды, в самом начале фазы A, по всему дереву конфигурации.** - -`ParametersExtension` выполняется первым, и одно из первых его дел - развернуть подстановки `%param%`: сначала внутри самих параметров (параметр может ссылаться на другой), затем по всей остальной конфигурации. Так что к моменту, когда любое другое расширение, включая `ServicesExtension`, получает свою секцию, подстановок уже нет. Расширения работают с конкретными значениями, а не с `%...%`. - -Когда подстановка занимает всю строку, её значение возвращается *как есть*, включая массивы и объекты, поэтому `%mailer%` может развернуться в целый массив. В любом другом месте оно склеивается в строку, а запись через точку `%foo.bar%` добирается до вложенных массивов. - - -Статические и динамические параметры ------------------------------------- - -Не всякое значение можно впечь в код. Параметр, значение которого различается в разных средах, - переменная окружения, `baseUrl`, выведенный из запроса, - должен остаться **динамическим**. Такие параметры вы объявляете через `setDynamicParameterNames()` или `Expect::...->dynamic()` в схеме; подробнее в разделе [Динамические параметры |application:bootstrapping#Динамические параметры]. - -Динамический параметр заменяется не значением, а выражением, которое читает его *во время выполнения*. Поэтому `%env.DB_HOST%` не застывает в строку, а становится обращением во время выполнения в порождённом контейнере. Всё остальное статично и застывает во время компиляции, откуда и берётся обычное удивление "моё значение `getenv()` одинаково во всех средах": параметр просто был статическим. - -Обратная операция - **экранирование**: чтобы буквальные `%` или `@` не были истолкованы, они удваиваются (`%%`, `@@`). Nette делает это автоматически для параметров, которые подставляет за вас, поэтому их значения никогда не примут за подстановки или ссылки. - - -Ссылки: когда @service становится ссылкой -========================================= - -Второй главный вопрос. Перевод `@service` происходит **в несколько шагов в разных фазах**, в зависимости от того, насколько сложна строка. Отслеживать это вручную приходится редко, но знание шагов объясняет, почему одни ссылки разрешаются раньше других. - -- **Разбор (загрузка конфигурации).** `@service`, использованный *как сущность* - то, что создаёт сервис, как в `Foo(@bar)`, - становится ссылкой сразу. `@service`, использованный *как аргумент*, пока остаётся обычной строкой. `@` в кавычках экранируется в `@@`, поэтому считается буквальным текстом, а не ссылкой. -- **Фаза A (`loadConfiguration`).** При обработке определений чистый аргумент `@name` или `@Type` превращается в объект `Reference`. Это ловит только простые формы; `@service::CONST` или `@` внутри более крупного выражения остаются на потом. -- **Фаза B (`complete`).** Здесь происходит настоящий "умный" перевод: `@service` → ссылка, `@service::CONSTANT` → буквальная константа класса, `@service::property` → чтение этого свойства, `@@x` → буквальный текст `@x`. - -В самом слове *ссылка* скрыт ещё один перевод. `Reference` может указывать либо по **имени**, либо по **типу** (`@Namespace\Type`). Ссылка по типу **ещё не является именем сервиса** - в конкретное имя её разрешает autowiring, а это происходит только на шаге **complete**, когда построен индекс autowiring. Это мостик к следующему разделу: обращения к autowiring намеренно откладываются до готовности индекса. - -| Форма | Становится ссылкой или выражением в | Разрешается в конкретный сервис в -|---|---|--- -| сущность (`@foo` как фабрика) | разборе | complete -| аргумент `@foo`, `@Type` | фазе A | complete -| `@foo::CONST`, `@foo::prop` | фазе B | complete -| ссылка по типу `@Type` | фазе A/B | complete (autowiring) - - -Исследование ContainerBuilder: когда это безопасно -================================================== - -Теперь вопрос, который авторы расширений задают чаще всего: **в каком методе можно искать сервисы по типу?** Ответ вытекает из одного простого правила о том, как билдер следит за собственным состоянием. - -Поиск **по типу** (`getByType()`, `getDefinitionByType()`, `findByType()`) требует, чтобы граф сервисов был *разрешён*: все типы известны, индекс autowiring построен. Поэтому, когда вы вызываете один из этих методов, а граф изменился с последнего разрешения, билдер **тут же разрешает весь известный граф**. Во время самого разрешения любой поиск по типу запрещён и выбрасывает `NotAllowedDuringResolvingException`. - -У поиска **по тегу** (`findByTag()`) такого требования нет: теги не зависят от типов, поэтому он работает в **любой фазе**. - -По фазам: - -- **`loadConfiguration()` (фаза A) - поиск по типу ненадёжен.** Граф неполон: расширения, которые выполняются позже, ещё не зарегистрировали свои сервисы, а главное - нет пользовательской секции `services:` (она идёт последней). Вызов `getByType()` сработает, он вызовет преждевременное разрешение частичного графа, но ответ придёт из неполной картины, а само преждевременное разрешение потратит силы впустую. Правило: **в `loadConfiguration()` только регистрируйте определения, не ищите по типу.** `findByTag()` здесь допустим. -- **`beforeCompile()` (фаза B) - правильное место для исследования.** К этому моменту существуют **все** определения (включая пользовательские), **типы разрешены** и **индекс autowiring построен**, поэтому `getByType()`, `findByType()` и `findByTag()` возвращают **надёжные** ответы. Аргументы ещё *не* заполнены autowiring: это следующий шаг (`complete`), после всех вызовов `beforeCompile()`. Когда вы меняете здесь определение, следующий `getByType()` незаметно перерешает граф, так что вы можете свободно чередовать правки и запросы. -- **`afterCompile()` (фаза C) - только код.** Он работает над порождённым классом, а не над билдером. Граф уже готов; здесь вы формируете итоговый PHP. - -| Я хочу... | Фаза -|---|--- -| зарегистрировать сервис | `loadConfiguration()` -| искать по **тегу** и менять определения | `loadConfiguration()` или `beforeCompile()` -| искать по **типу** (`getByType`/`findByType`) | **`beforeCompile()`** -| зависеть от того, какие сервисы autowiring выбрал для аргументов | не во время компиляции - смотрите это во время выполнения -| поправить порождённый код | `afterCompile()` -| выполнить код после старта контейнера | [код инициализации |extensions#Код инициализации] - - -Внутри фазы B: resolve и complete -================================= - -Фаза B состоит из двух проходов, между которыми зажаты вызовы `beforeCompile()`: - -```php -$this->builder->resolve(); // типы разрешены, индекс autowiring построен -foreach ($this->extensions as $extension) { - $extension->beforeCompile(); -} -$this->builder->complete(); // ТОЛЬКО ТЕПЕРЬ аргументы заполняются autowiring -``` - -**`resolve()`** определяет тип каждого сервиса - берёт его из объявленного `type` либо выводит из фабрики: тип возвращаемого значения фабричного метода, класс, который она создаёт, или сервис, на который указывает ссылка, - и затем строит индекс autowiring, сопоставляющий каждому типу (классу вместе с его родителями и интерфейсами) имя сервиса. Сервис, помеченный `autowired: false`, в индекс не попадает; `autowired: [A, B]` сужает набор типов, под которыми он виден. Принципиально важно, что resolve улаживает *типы*, а не *аргументы*: заполнению аргументов через autowiring нужен готовый индекс, который появляется только после этого прохода. - -**`complete()`** - место, где на самом деле происходит заполнение аргументов через autowiring. Для каждого определения он подставляет недостающие аргументы конструктора и настроек, разыскивая их типы в уже готовом индексе. Именно поэтому ссылки по типу оставались неразрешёнными во время resolve: этот поиск - дело complete, когда есть надёжный индекс, в котором можно искать. - - -Фаза C: генерация кода -====================== - -`generateCode()` передаёт готовый граф в `PhpGenerator`, который порождает класс, расширяющий `Container`, с методом `createServiceXxx()` на каждый сервис, а также заранее вычисленные метаданные `aliases`, `tags` и `wiring`. Каждый `Statement` становится текстом PHP (`new Foo(...)`, вызовы методов, обращения к свойствам), а каждая `Reference` - вызовом `$this->getService(...)`. - -Затем расширения получают заключительный проход `afterCompile()` над порождённым классом: именно здесь, например, выводятся геттеры статических и динамических параметров, - а вместе с ним возможность добавить [код инициализации |extensions#Код инициализации], выполняемый при каждом запросе. - - -Вся картина одним рисунком -========================== - -``` -КОМПИЛЯЦИЯ (однажды, в кеш) -│ -├─ загрузка конфигурации NEON -> Statement/массив; слияние файлов -│ @ в кавычках -> @@ ; сущности -> Statement -│ -▼ Compiler::compile() -│ -├─ ФАЗА A processExtensions() -│ ├─ ParametersExtension (ПЕРВОЕ) ── %param% РАЗВЁРНУТ по всей конфигурации -│ │ динамические -> выражение времени выполнения -│ ├─ ExtensionsExtension (ПЕРВОЕ) ── регистрирует дальнейшие расширения -│ ├─ ...остальные расширения... ── loadConfiguration(): только регистрация определений -│ └─ ServicesExtension (ПОСЛЕДНЕЕ) ── services: -> объекты Definition -│ @name/@Type -> Reference -│ [граф полон по количеству; ТИПЫ и АРГУМЕНТЫ ещё нет; поиск по типу ненадёжен] -│ -├─ ФАЗА B processBeforeCompile() -│ ├─ builder.resolve() ── разрешить все типы; построить индекс autowiring -│ │ [типы готовы; индекс готов] -│ ├─ beforeCompile() расширения ── здесь getByType/findByType/findByTag БЕЗОПАСНЫ -│ │ (аргументы ещё не заполнены autowiring) -│ └─ builder.complete() ── заполнить АРГУМЕНТЫ; завершить перевод ссылок -│ ссылки по типу -> имена сервисов -│ -└─ ФАЗА C generateCode() - ├─ PhpGenerator.generate() ── Statement -> PHP; методы createServiceXxx() - ├─ afterCompile() расширения ── поправить код; вывести геттеры параметров - └─ toString() ── итоговый PHP-код -> кеш - -──────────────────────────────────────────────────────────── - -ВРЕМЯ ВЫПОЛНЕНИЯ (каждый запрос) -│ -├─ new Container($dynamicParams) -├─ initialize() ── загрузочный код расширений (сессия, заголовки, валидация) -└─ getService()/getByType() ── ленивые экземпляры из заранее вычисленных метаданных -``` - - -Распространённые заблуждения -============================ - -- "В `loadConfiguration()` я поищу сервисы по типу." Нет: граф неполон (пользовательская секция `services:` выполняется после вас), и `getByType()` вызывает преждевременное разрешение частичного графа. Перенесите это в `beforeCompile()`. `findByTag()` здесь допустим. -- "Значение из `getenv()` в параметре будет разным в каждой среде." Только если параметр динамический. Иначе оно впекается во время компиляции и остаётся везде одинаковым. -- "Ссылка `@Type` - это уже имя сервиса." Нет: это ссылка по типу, которую autowiring разрешает в конкретное имя только на шаге complete. -- "Моё расширение читает вспомогательный файл, но изменения не проявляются." Зарегистрируйте его через `$builder->addDependency($file)`, иначе кеш о нём не знает и пересобираться не будет. -- "Во время `resolve()` я могу вызвать `getByType()`." Нет: он выбросит `NotAllowedDuringResolvingException`. Поиску по типу место в `beforeCompile()` или позже, но никогда посреди разрешения. diff --git a/dependency-injection/ru/configuration.texy b/dependency-injection/ru/configuration.texy deleted file mode 100644 index e8671a28b2..0000000000 --- a/dependency-injection/ru/configuration.texy +++ /dev/null @@ -1,335 +0,0 @@ -Конфигурация DI-контейнера -************************** - -.[perex] -Обзор параметров конфигурации DI-контейнера Nette. - - -Конфигурационный файл -===================== - -DI-контейнером Nette легко управлять с помощью конфигурационных файлов. Обычно они пишутся в [формате NEON|neon:format]. Мы рекомендуем использовать [редакторы с поддержкой |tools:ide] этого формата. - -<pre> -"decorator .[prism-token prism-atrule]":[#Decorator]: "Decorator .[prism-token prism-comment]"<br> -"di .[prism-token prism-atrule]":[#DI]: "DI-контейнер .[prism-token prism-comment]"<br> -"extensions .[prism-token prism-atrule]":[#Extensions]: "Установка дополнительных DI-расширений .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#Подключение файлов]: "Подключение файлов .[prism-token prism-comment]"<br> -"parameters .[prism-token prism-atrule]":[#Параметры]: "Параметры .[prism-token prism-comment]"<br> -"search .[prism-token prism-atrule]":[#Search]: "Автоматическая регистрация сервисов .[prism-token prism-comment]"<br> -"services .[prism-token prism-atrule]":[services]: "Сервисы .[prism-token prism-comment]" -</pre> - -.[note] -Чтобы записать строку, содержащую символ `%`, его нужно экранировать удвоением до `%%`. - - -Параметры -========= - -В конфигурации можно определить параметры, которые затем используются в определениях сервисов. Это позволяет сделать конфигурацию нагляднее или собрать в одном месте значения, которые могут меняться. - -```neon -parameters: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: secret -``` - -На параметр `dsn` мы ссылаемся где угодно в конфигурации записью `%dsn%`. Параметры можно использовать и внутри строк вроде `'%wwwDir%/images'`. - -Параметрами могут быть не только строки или числа, они могут содержать и массивы: - -```neon -parameters: - mailer: - host: smtp.example.com - secure: ssl - user: franta@gmail.com - languages: [cs, en, de] -``` - -На конкретный ключ мы ссылаемся как `%mailer.user%`. - -Если вашему коду (например, классу) нужно значение параметра, передайте его в класс. Например, в конструкторе. Никакого глобального объекта конфигурации, у которого классы могли бы запросить значения параметров, нет. Это было бы нарушением принципа внедрения зависимостей. - - -Сервисы -======= - -См. [отдельную главу|services]. - - -Decorator -========= - -Как изменить сразу несколько сервисов определённого типа? Например, как вызвать определённый метод у всех презентеров, наследующих от конкретного базового класса? Для этого и служит decorator. - -```neon -decorator: - # для всех сервисов, которые являются экземплярами этого класса или интерфейса - App\Presentation\BasePresenter: - setup: - - setProjectId(10) # вызвать этот метод - - $absoluteUrls = true # и задать переменную -``` - -Decorator можно использовать и для установки [тегов |services#Теги] или включения [режима inject |services#Режим inject]. - -```neon -decorator: - InjectableInterface: - tags: [mytag: 1] - inject: true -``` - - -DI -=== - -Технические настройки DI-контейнера. - -```neon -di: - # показывать DIC в панели Tracy? - debugger: ... # (bool) по умолчанию определяется автоматически (включено при наличии Tracy) - - # типы параметров, которые никогда не проходят autowiring - excluded: ... # (string[]) - - # включить ленивое создание сервисов? - lazy: ... # (bool) по умолчанию false - - # класс, от которого наследуется DI-контейнер - parentClass: ... # (string) по умолчанию Nette\DI\Container -``` - - -Ленивые сервисы .{data-version:3.2.4} -------------------------------------- - -Установка `lazy: true` включает ленивое (отложенное) создание сервисов. Это значит, что сервисы на самом деле создаются не в момент запроса из DI-контейнера, а только при первом использовании. Это может ускорить старт приложения и снизить расход памяти, потому что создаются только те сервисы, которые действительно нужны для данного запроса. - -Для конкретного сервиса ленивое создание можно [настроить отдельно |services#Ленивые сервисы]. - -.[note] -Ленивые объекты можно использовать только для пользовательских классов, но не для внутренних классов PHP. Требуется PHP 8.4 или новее. - - -Экспорт метаданных ------------------- - -Класс DI-контейнера содержит также много метаданных. Вы можете уменьшить его размер, сократив экспорт метаданных. - -```neon -di: - export: - # экспортировать параметры? - parameters: false # (bool) по умолчанию true - - # экспортировать теги и какие? - tags: # (string[]|bool) по умолчанию все - - event.subscriber - - # экспортировать данные для autowiring и какие? - types: # (string[]|bool) по умолчанию все - - Nette\Database\Connection - - Symfony\Component\Console\Application -``` - -Если вы не используете `$container->getParameters()`, вы можете отключить экспорт параметров. Кроме того, вы можете экспортировать только те теги, которые действительно используете для получения сервисов через `$container->findByTag(...)`. Если вы этот метод вообще не вызываете, экспорт тегов можно полностью отключить значением `false`. - -Метаданные для [autowiring|autowiring] можно заметно сократить, перечислив только те классы, которые вы действительно запрашиваете через `$container->getByType()`. Снова: если вы этот метод не вызываете (или вызываете только в файле [bootstrap|application:bootstrapping], например чтобы получить `Nette\Application\Application`), экспорт типов можно полностью отключить значением `false`. - - -Extensions -========== - -Регистрация дополнительных DI-расширений. Вот так вы добавите, например, DI-расширение `Dibi\Bridges\Nette\DibiExtension3` под именем `dibi`: - -```neon -extensions: - dibi: Dibi\Bridges\Nette\DibiExtension3 -``` - -Затем вы настраиваете его в секции `dibi`: - -```neon -dibi: - host: localhost -``` - -Как расширение можно добавить и класс с параметрами: - -```neon -extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, [%appDir%], %tempDir%/cache) -``` - - -Подключение файлов -================== - -Дополнительные конфигурационные файлы можно подключить в секции `includes`: - -```neon -includes: - - parameters.php - - services.neon - - presenters.neon -``` - -Имя `parameters.php` - не опечатка: конфигурацию можно записать и в PHP-файле, который возвращает её массивом: - -```php -<?php -return [ - 'database' => [ - 'main' => [ - 'dsn' => 'sqlite::memory:', - ], - ], -]; -``` - -Если в нескольких конфигурационных файлах встречаются элементы с одинаковыми ключами, они будут перезаписаны или, в случае массивов, [объединены |#Слияние]. Файл, подключённый позже, имеет более высокий приоритет, чем предыдущий. Файл, в котором указана секция `includes`, имеет более высокий приоритет, чем подключённые в нём файлы. - - -Search -====== - -Автоматическая регистрация сервисов в DI-контейнере значительно упрощает разработку. Nette автоматически добавляет в контейнер презентеры, но вы легко можете добавить и любые другие классы. - -Достаточно указать, в каких каталогах (и подкаталогах) искать классы: - -```neon -search: - - in: %appDir%/Forms - - in: %appDir%/Model -``` - -Если вам нужно всего одно правило поиска, список можно опустить и записать его ключи прямо под `search`: - -```neon -search: - in: %appDir% -``` - -Однако обычно мы не хотим добавлять совершенно все классы и интерфейсы, поэтому их можно отфильтровать: - -```neon -search: - - in: %appDir%/Forms - - # фильтрация по имени файла (string|string[]) - files: - - *Factory.php - - # фильтрация по имени класса (string|string[]) - classes: - - *Factory -``` - -Либо мы можем выбрать классы, которые наследуют или реализуют хотя бы один из перечисленных: - - -```neon -search: - - in: %appDir% - extends: - - App\*Form - implements: - - App\*FormInterface -``` - -Можно задать и правила исключения по маскам имён классов или по предкам. Если класс подходит под правило исключения, он не будет добавлен в DI-контейнер: - -```neon -search: - - in: %appDir% - exclude: - files: ... - classes: ... - extends: ... - implements: ... -``` - -Всем автоматически зарегистрированным сервисам можно назначить теги: - -```neon -search: - - in: %appDir% - tags: ... -``` - -Помимо классов поиск регистрирует и интерфейсы с единственным методом `create()` или `get()` - как [генерируемые фабрики или аксессоры |factory]. Классы, для которых сервис того же типа уже зарегистрирован в контейнере, пропускаются, поэтому дубликаты не возникают. - - -Слияние -======= - -Если в нескольких конфигурационных файлах встречаются элементы с одинаковыми ключами, они будут перезаписаны или, в случае массивов, объединены. Подключённый позже файл имеет более высокий приоритет, чем предыдущий. - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>результат</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> - <td> -```neon -items: - - 1 - - 2 - - 3 -``` - </td> -</tr> -</table> - -Для массивов слияние можно предотвратить, добавив после имени ключа восклицательный знак: - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>результат</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items!: - - 3 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> -</tr> -</table> - -{{maintitle: Конфигурация Dependency Injection}} diff --git a/dependency-injection/ru/container.texy b/dependency-injection/ru/container.texy deleted file mode 100644 index 5e973b08e8..0000000000 --- a/dependency-injection/ru/container.texy +++ /dev/null @@ -1,142 +0,0 @@ -Что такое DI-контейнер? -*********************** - -.[perex] -Контейнер внедрения зависимостей (DIC или DI-контейнер) - объект, отвечающий за создание и настройку других объектов (называемых сервисами). - -Возможно, вас это удивит, но во многих случаях, чтобы пользоваться преимуществами внедрения зависимостей (сокращённо DI), контейнер не нужен. Ведь даже во [вводной главе|introduction] мы показали конкретные примеры DI, и никакой контейнер там не понадобился. - -Однако когда приходится управлять большим количеством объектов со сложными зависимостями, DI-контейнер становится очень полезен. Обычно так и бывает у веб-приложений, построенных на фреймворке. - -В предыдущей главе мы познакомились с классами `Article` и `EditController`. У обоих есть зависимости, а именно база данных и фабрика `ArticleFactory`. Для этих классов мы сейчас и создадим контейнер. Разумеется, создавать контейнер для такого простого примера - перебор. Но мы создадим его, чтобы показать, как он выглядит и работает. - -Вот простой жёстко прописанный контейнер для приведённого примера: - -```php -class Container -{ - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection('mysql:', 'root', '***'); - } - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->createDatabase()); - } - - public function createEditController(): EditController - { - return new EditController($this->createArticleFactory()); - } -} -``` - -Использование выглядело бы так: - -```php -$container = new Container; -$controller = $container->createEditController(); -``` - -Мы просто запрашиваем объект у контейнера, и нам не нужно знать, как его создать и какие у него зависимости; всем этим занимается контейнер. Зависимости контейнер внедряет автоматически. В этом его сила. - -Пока в контейнере все сведения прописаны жёстко. Поэтому сделаем следующий шаг и добавим параметры, чтобы контейнер стал по-настоящему полезным: - -```php -class Container -{ - public function __construct( - private array $parameters, - ) { - } - - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection( - $this->parameters['db.dsn'], - $this->parameters['db.user'], - $this->parameters['db.password'], - ); - } - - // ... -} - -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); -``` - -Внимательные читатели могли заметить проблему. Каждый раз, когда мы получаем объект `EditController`, создаются и новые экземпляры `ArticleFactory` и соединения с базой данных. Этого мы точно не хотим. - -Поэтому мы добавим метод `getService()`, который будет всегда возвращать одни и те же экземпляры: - -```php -class Container -{ - private array $services = []; - - public function __construct( - private array $parameters, - ) { - } - - public function getService(string $name): object - { - if (!isset($this->services[$name])) { - // getService('Database') вызовет createDatabase() - $method = 'create' . $name; - $this->services[$name] = $this->$method(); - } - return $this->services[$name]; - } - - // ... -} -``` - -При первом вызове, например `$container->getService('Database')`, он вызывает `createDatabase()`, чтобы создать объект базы данных, сохраняет его в массив `$services` и возвращает. При последующих вызовах он сразу возвращает уже сохранённый экземпляр. - -Изменим и остальную часть контейнера, чтобы она использовала `getService()`: - -```php -class Container -{ - // ... - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->getService('Database')); - } - - public function createEditController(): EditController - { - return new EditController($this->getService('ArticleFactory')); - } -} -``` - -Кстати, термином "сервис" называют любой объект, управляемый контейнером. Отсюда и имя метода `getService()`. - -Готово. У нас полностью работающий DI-контейнер! И мы можем им пользоваться: - -```php -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); - -$controller = $container->getService('EditController'); -$database = $container->getService('Database'); -``` - -Как видите, написать DIC несложно. Стоит отметить, что сами объекты не знают о том, что их создаёт контейнер. Благодаря этому таким способом можно создавать любой объект PHP, не меняя его исходный код. - -Создавать и поддерживать класс контейнера вручную может быстро превратиться в кошмар. Поэтому в следующей главе мы поговорим о [Nette DI Container|nette-container], который умеет порождать и обновлять себя практически автоматически. - - -{{maintitle: Что такое контейнер внедрения зависимостей?}} diff --git a/dependency-injection/ru/extensions.texy b/dependency-injection/ru/extensions.texy deleted file mode 100644 index 1c8d894fec..0000000000 --- a/dependency-injection/ru/extensions.texy +++ /dev/null @@ -1,443 +0,0 @@ -Создание расширений для Nette DI -******************************** - -.[perex] -Расширение - это класс, который подключается к компиляции DI-контейнера. Он может программно регистрировать сервисы, проверять собственную секцию конфигурации, изменять сервисы, определённые другими, и даже править порождённый код контейнера. Эта страница научит вас писать такое расширение, объяснит, что и когда происходит и на что стоит обратить внимание. - -Расширения - это тот способ, которым пакеты встраиваются в Nette по-родному: их используют все пакеты `nette/*`, и ваш может тоже. Типичное расширение делает одно или несколько из этого: - -- **интегрирует библиотеку** - регистрирует её сервисы в контейнере и предоставляет удобную проверяемую секцию конфигурации (именно оттуда берутся секции `mail:` или `database:`) -- **автоматизирует регистрацию** - регистрирует множество похожих сервисов в цикле или по правилу, когда перечислять их в `services:` было бы утомительно -- **вносит сквозные изменения** - находит сервисы, зарегистрированные другими, и дополняет их, например прикрепляет логгер к каждому сервису с определённым тегом - -В повседневной работе над приложением расширение нужно редко: секция [services |services] конфигурации покрывает регистрацию и связывание ваших классов. Беритесь за расширение тогда, когда одной конфигурации перестаёт хватать. - -Расширение включается в секции `extensions`. Вот так вы добавите расширение, представленное классом `BlogExtension`, под именем `blog`: - -```neon -extensions: - blog: BlogExtension -``` - -Если его конструктор принимает аргументы, передайте их прямо там: - -```neon -extensions: - blog: BlogExtension(%debugMode%) -``` - - -Как работает компиляция -======================= - -Чтобы уверенно писать расширения, вам нужно знать одну ключевую вещь: **когда выполняется ваш код.** Nette не связывает сервисы во время обработки запросов. Вместо этого он *компилирует* контейнер заранее: читает все конфигурационные файлы, даёт расширениям сделать свою работу и порождает оптимизированный PHP-класс, который сохраняет на диск. Каждый следующий запрос лишь загружает этот готовый класс. Поэтому код вашего расширения выполняется только тогда, когда контейнер (пере)собирается, а не при каждом запросе. - -Отсюда важное следствие: во время компиляции сервисов ещё не существует. Существуют **определения** - рецепты, описывающие, какого класса будет каждый сервис, как его создать и что у него потом вызвать. Определения живут в объекте [ContainerBuilder |#ContainerBuilder]. Расширение - это, по сути, *конфигурация с возможностью писать код*: всё, что можно объявить в секции `services:`, можно собрать и на PHP - по условию, в цикле или в ответ на то, что зарегистрировали другие. - -Компиляция идёт фазами, и расширение может вступить в каждую из них: - -1) проверяются секции конфигурации всех расширений (`getConfigSchema()`) -2) каждое расширение регистрирует свои сервисы (`loadConfiguration()`); пользовательская секция `services:` обрабатывается последней, поэтому последнее слово всегда за приложением -3) когда все определения на месте и типы сервисов разрешены, расширения могут их изменить (`beforeCompile()`) -4) порождается класс контейнера; расширения ещё могут поправить его код (`afterCompile()`) и добавить код, который выполнится при старте приложения ([инициализация |#Код инициализации]) - -.[note] -В режиме разработки контейнер автоматически перекомпилируется при изменении конфигурационного файла или самого класса расширения: и то, и другое отслеживается как зависимость. Так что вы можете разрабатывать расширения, ни разу не очищая кеш. - -.[tip] -Чтобы глубже разобраться, что происходит в каждой фазе - когда разворачиваются параметры, когда `@service` становится ссылкой и когда именно безопасно искать сервисы по типу, - см. [Компиляцию в подробностях |compilation-internals]. - - -Первое расширение -================= - -Вот небольшое, но полноценное расширение. Мы включаем и настраиваем его в одном файле: - -```neon -extensions: - blog: BlogExtension - -blog: - postsPerPage: 5 -``` - -А вот и весь класс: - -```php -use Nette\Schema\Expect; - -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function getConfigSchema(): Nette\Schema\Schema - { - return Expect::structure([ - 'postsPerPage' => Expect::int(10), - 'allowComments' => Expect::bool(true), - ]); - } - - - public function loadConfiguration(): void - { - $builder = $this->getContainerBuilder(); - - $builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class, ['postsPerPage' => $this->config->postsPerPage]); - - if ($this->config->allowComments) { - $builder->addDefinition($this->prefix('comments')) - ->setFactory(Blog\Comments::class); - } - } -} -``` - -`getConfigSchema()` описывает, что может содержать секция `blog:` (названная по ключу, под которым мы зарегистрировали расширение), включая типы и значения по умолчанию; проверенные значения затем доступны в `$this->config`. В `loadConfiguration()` мы регистрируем сервисы. Обратите внимание на имена: `$this->prefix('articles')` даёт `blog.articles`, поэтому сервисы разных расширений не могут столкнуться. - -А последние несколько строк показывают, зачем расширения вообще нужны: сервис `comments` регистрируется только тогда, когда комментарии включены. Обычный конфигурационный файл таких решений принимать не может. - -Зарегистрированные так сервисы ведут себя ровно так же, как если бы были записаны в `services:`: они создаются лениво по требованию, а autowiring передаёт их везде, где объявлен тип `Blog\Articles`. - -Следующие главы подробно описывают жизненный цикл расширения, затем API [ContainerBuilder |#ContainerBuilder], которым вы будете пользоваться внутри расширения, и наконец [подводные камни |#Советы и подводные камни], о которых стоит знать. - - -Жизненный цикл расширения -========================= - -Расширение наследует от [api:Nette\DI\CompilerExtension] и переопределяет некоторые из четырёх методов `getConfigSchema()`, `loadConfiguration()`, `beforeCompile()` и `afterCompile()`, которые компилятор вызывает в этом порядке во время компиляции. - - -getConfigSchema(): Nette\Schema\Schema .[method] ------------------------------------------------- - -Определяет схему секции конфигурации расширения. Благодаря ей пользователи бесплатно получают проверку и понятные сообщения об ошибках: опечатка или неверный тип в секции `blog:` сопровождается вразумительным сообщением, а вы не пишете ни одной проверки. - -Схема описывается с помощью библиотеки [Schema |schema:] и может выражать типы, значения по умолчанию, допустимые значения и многое другое: - -```php -public function getConfigSchema(): Nette\Schema\Schema -{ - return Expect::structure([ - 'postsPerPage' => Expect::int(10), - 'storage' => Expect::anyOf('files', 'database')->firstIsDefault(), - ]); -} -``` - -Проверенная конфигурация доступна в `$this->config` как объект `stdClass` (или как массив, если добавить к схеме `castTo('array')`). - -Если значение параметра нельзя знать во время компиляции - например, оно приходит из переменной окружения, - пометьте его через `dynamic()`, например `Expect::int()->dynamic()`. Подробнее в разделе [динамические параметры |application:bootstrapping#Динамические параметры]. - - -loadConfiguration() .[method] ------------------------------ - -Место, где расширение регистрирует свои сервисы с помощью [ContainerBuilder |#ContainerBuilder]: - -```php -public function loadConfiguration(): void -{ - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class); -} -``` - -Если сервис должен быть доступен и под коротким именем, добавьте псевдоним. По соглашению это делается только тогда, когда расширение зарегистрировано под своим обычным именем, чтобы несколько экземпляров расширения не спорили за него: - -```php -if ($this->name === 'blog') { - $builder->addAlias('articles', $this->prefix('articles')); -} -``` - -Когда сервисов много, удобнее бывает определить их в отдельном файле NEON привычным синтаксисом [services |services]. Префикс `@extension` отсылает к текущему расширению: - -```neon -services: - articles: - create: MyBlog\ArticlesModel(@connection) - - comments: - create: MyBlog\CommentsModel(@connection, @extension.articles) -``` - -Эти определения мы загружаем через `loadDefinitionsFromConfig()`; имена получают префикс автоматически, а файл отслеживается как зависимость, поэтому его изменение вызывает перекомпиляцию: - -```php -public function loadConfiguration(): void -{ - $this->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/services.neon')['services'], - ); -} -``` - - -beforeCompile() .[method] -------------------------- - -Когда вызывается этот метод, в билдере уже есть **все** определения: ваши, других расширений и из пользовательских конфигурационных файлов. Типы сервисов тоже разрешены, поэтому поиск по типу надёжен. Благодаря этому данная фаза идеальна для исследования и дополнения окончательного графа сервисов. - -Обычно вы ищете сервисы по тегу или по типу и дополняете найденные определения: - -```php -public function beforeCompile(): void -{ - $builder = $this->getContainerBuilder(); - - foreach ($builder->findByTag('logaware') as $name => $attrs) { - $builder->getDefinition($name)->addSetup('setLogger'); - } -} -``` - -У вызова `setLogger()` нет явных аргументов: их подставит autowiring, как и в фабриках. - -Вы можете сотрудничать и с другими зарегистрированными расширениями, полученными через `$this->compiler->getExtensions()`, при желании отфильтрованными по классу или интерфейсу: - -```php -foreach ($this->compiler->getExtensions(FooExtension::class) as $extension) { - // ... -} -``` - - -afterCompile(Nette\PhpGenerator\ClassType $class) .[method] ------------------------------------------------------------ - -В последней фазе класс контейнера порождается как объект [ClassType |php-generator:#Классы] библиотеки [PHP Generator |php-generator:]. Он содержит фабричный метод для каждого сервиса и вот-вот будет записан в кеш. Вы ещё можете изменить его код: - -```php -public function afterCompile(Nette\PhpGenerator\ClassType $class): void -{ - $method = $class->getMethod('__construct'); - // ... -} -``` - -Эта фаза понадобится вам лишь изредка. Чтобы добавить код, выполняющийся при старте приложения, используйте вместо этого инициализацию: - - -Код инициализации ------------------ - -Все предыдущие фазы влияют на то, как контейнер *собирается*. Кроме того, расширение может выдать код, который выполняется *во время выполнения*, сразу после создания контейнера, например чтобы стартовать сессию или запустить сервисы. Код записывается в объект `$this->initialization` его методом [addBody() |php-generator:#Тела методов и функций]: - -```php -public function loadConfiguration(): void -{ - // сервисы с тегом 'run' должны быть созданы сразу после старта контейнера - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $attrs) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } -} -``` - -Сам Nette использует инициализацию, например, чтобы автоматически стартовать сессию или отправить защитные HTTP-заголовки. И помните: в отличие от всего остального в расширении, этот код выполняется при **каждом запросе**, поэтому держите его небольшим. - - -ContainerBuilder -================ - -[api:Nette\DI\ContainerBuilder] - объект, через который расширение общается с компилятором. Он хранит [определения |#Как работает компиляция] всех сервисов и предлагает методы для их добавления, поиска и изменения. Вы получаете его в `loadConfiguration()` и `beforeCompile()`: - -```php -$builder = $this->getContainerBuilder(); -``` - - -Добавление сервисов -------------------- - -Регистрация сервиса - то же самое, что вы делаете в секции `services:` файла NEON, только записанное на PHP. Каждому ключу конфигурации соответствует метод определения, поэтому эти две записи равнозначны: - -```neon -services: - articles: - create: Blog\Articles(@connection) - setup: - - setLogger(@logger) - tags: [logaware] -``` - -```php -$builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class, ['@connection']) - ->addSetup('setLogger', ['@logger']) - ->addTag('logaware'); -``` - -Определение, возвращаемое `addDefinition()`, - это [ServiceDefinition |#Типы определений], предлагающее соответствия ключей конфигурации: `setType()` (класс сервиса), `setFactory()` (как его создать), `setArguments()`, `addSetup()`, `addTag()` и `setAutowired()`. - -`addSetup()` отражает список `setup:` и принимает те же формы: вызов метода `addSetup('setLogger', ['@logger'])`, присваивание свойства `addSetup('$cache', ['@cache'])` или вызов у другого сервиса `addSetup('@Tracy\Bar::addPanel', [$panel])`. - -Помимо обычных сервисов билдер умеет регистрировать [генерируемые |factory] фабрики, аксессоры и локаторы, у каждого свой метод, возвращающий соответствующий [тип определения |#Типы определений]: - -| Метод | Регистрирует -|--------|---------- -| `addDefinition()` | обычный сервис (возвращает `ServiceDefinition`) -| `addFactoryDefinition()` | генерируемую [фабрику |factory] (интерфейс с методом `create()`) -| `addAccessorDefinition()` | генерируемый [аксессор |factory#Аксессор] (интерфейс с методом `get()`) -| `addLocatorDefinition()` | [мультифабрику или локатор |factory#Мультифабрика и мультиаксессор], объединяющие несколько фабрик -| `addImportedDefinition()` | сервис, передаваемый в контейнер извне во время выполнения -| `addAlias()` | второе имя для существующего сервиса - -У фабрики объект, который она создаёт, настраивается через `getResultDefinition()`; аксессор же указывает на существующий сервис через `setReference()`: - -```php -$builder->addFactoryDefinition($this->prefix('latteFactory')) - ->setImplement(LatteFactory::class) - ->getResultDefinition() - ->setFactory(Latte\Engine::class) - ->addSetup('setStrictTypes', [true]); -``` - -`addLocatorDefinition()` и `addImportedDefinition()` нужны редко: такие сервисы обычно возникают из ключей `implement:` и импортируемых сервисов в NEON, а не пишутся вручную. - - -Поиск и изменение сервисов --------------------------- - -Для поиска и обхода существующих определений билдер предлагает: - -| Метод | Описание -|--------|------------ -| `getDefinition(string $name)` | определение с заданным именем (выбрасывает исключение, если его нет) -| `hasDefinition(string $name)` | существует ли определение или псевдоним с таким именем -| `getDefinitions()` | все определения -| `removeDefinition(string $name)` | удаляет определение -| `getByType(string $type)` | имя autowired-сервиса этого типа или `null` -| `getDefinitionByType(string $type)` | autowired-определение этого типа -| `findByType(string $type)` | все определения этого типа парами `имя => определение` -| `findByTag(string $tag)` | сервисы с этим тегом парами `имя => значение тега` -| `addExcludedClasses(array $types)` | исключает классы и интерфейсы из autowiring - -Удобный приём - использовать `getByType()`, чтобы узнать, существует ли сервис вообще, например чтобы подключиться к логгеру, только если он в приложении есть: - -```php -if ($builder->getByType(Psr\Log\LoggerInterface::class)) { - $builder->getDefinition($this->prefix('articles')) - ->addSetup('setLogger'); -} -``` - - -Типы определений ----------------- - -Каждый метод `add*Definition()` возвращает свой вид определения. Все они наследуют от общего предка `Nette\DI\Definitions\Definition`: - -- **`ServiceDefinition`** - обычный сервис; настраивается через `setType()`, `setFactory()`, `addSetup()`, `addTag()` и `setAutowired()` -- **`FactoryDefinition`** - [генерируемая фабрика |factory]: интерфейс, метод `create()` которого при каждом вызове возвращает новый объект -- **`AccessorDefinition`** - [генерируемый аксессор |factory#Аксессор]: интерфейс, метод `get()` которого возвращает существующий сервис -- **`LocatorDefinition`** - [мультифабрика или локатор |factory#Мультифабрика и мультиаксессор], объединяющие несколько фабрик или аксессоров в одном интерфейсе -- **`ImportedDefinition`** - сервис, который контейнер не создаёт сам, а получает извне во время выполнения - -Помните, что `getDefinition()` возвращает тот вид определения, который живёт под заданным именем. Если ваш код может столкнуться с генерируемой фабрикой, сначала проверьте тип и настройте создаваемый объект через `getResultDefinition()`: - -```php -$def = $builder->getDefinition($name); -if ($def instanceof Nette\DI\Definitions\FactoryDefinition) { - $def = $def->getResultDefinition(); -} -$def->addSetup('setLogger'); -``` - - -Советы и подводные камни -======================== - - -Время компиляции и время выполнения ------------------------------------ - -Самый частый источник путаницы: код расширения выполняется тогда, когда контейнер **компилируется**, а не когда приложение обрабатывает запросы. На практике это значит: - -- Расширение никогда не работает с экземплярами сервисов: их ещё нет. Не создавайте сервисы через `new`; зарегистрируйте определение и позвольте контейнеру их создать. -- Все значения конфигурации впекаются в порождённый код. Значение, которое может различаться в разных средах (путь, пароль из `getenv()`), нужно пометить как [динамическое |application:bootstrapping#Динамические параметры], иначе оно застынет во время компиляции. -- Строки, передаваемые в `$this->initialization->addBody()`, сейчас не выполняются: это PHP-код, помещаемый в контейнер и выполняемый при каждом запросе. - - -Зависимости от файлов ---------------------- - -Контейнер перекомпилируется при изменении конфигурационных файлов или классов расширений. Но если ваше расширение читает какой-то другой файл - список сущностей, XML-конфигурацию библиотеки, - контейнер об этом никак не узнает. Регистрируйте такие файлы через: - -```php -$builder->addDependency($file); -``` - -Иначе вас ждёт классическая загадка: вы правите файл, а приложение продолжает вести себя по-старому, и изменение проявляется только тогда, когда контейнер пересобирается по какой-то другой причине. (Файлы, прочитанные через `loadFromFile()`, отслеживаются автоматически.) - - -Условная регистрация --------------------- - -Расширение может подстраиваться под окружение. Необязательные интеграции обычно оборачивают в `class_exists()`: - -```php -if (class_exists(Symfony\Component\Console\Command\Command::class)) { - $builder->addDefinition($this->prefix('command')) - ->setFactory(Blog\Console\SitemapCommand::class); -} -``` - -А значения вроде `%debugMode%` лучше всего передавать через конструктор расширения: - -```neon -extensions: - blog: BlogExtension(%debugMode%) -``` - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function __construct( - private bool $debugMode = false, - ) {} -} -``` - -Типичный случай применения - регистрация панели Tracy только в режиме разработки. - - -Сложные аргументы ------------------ - -Иногда аргумент фабрики или вызова setup - не обычное значение, не имя класса и не ссылка `@service`. Для таких случаев есть: - -- `new Nette\DI\Definitions\Statement(Blog\Panel::class, [$args])` - объект, создаваемый на месте, "анонимный сервис", используемый как аргумент -- `new Nette\DI\Definitions\Reference('blog.articles')` - ссылка на сервис, объектный аналог строки `@name` -- `$builder::literal('PHP_SAPI')` - кусок сырого PHP-кода, вставляемый в порождённый контейнер как есть - -Пример - регистрация панели Tracy: - -```php -$builder->getDefinition($this->prefix('articles')) - ->addSetup('@Tracy\Bar::addPanel', [ - new Nette\DI\Definitions\Statement(Blog\ArticlesPanel::class), - ]); -``` - - -Экспортируемые теги и типы --------------------------- - -[Экспорт метаданных |configuration#Экспорт метаданных] можно ограничить в конфигурации так, чтобы скомпилированный контейнер сохранял только те теги и типы autowiring, которые приложение действительно использует. Если ваше расширение во время выполнения получает сервисы через `$container->findByTag()` или `$container->getByType()`, такое ограничение может убрать как раз те метаданные, на которые вы полагаетесь. - -Чтобы этого не случилось, скажите компилятору, какие теги и типы должны экспортироваться всегда: - -```php -public function loadConfiguration(): void -{ - // этот тег будет экспортирован всегда, даже при ограниченном экспорте - $this->compiler->addExportedTag('event.subscriber'); - - // этот тип будет всегда доступен для getByType() - $this->compiler->addExportedType(Nette\Database\Connection::class); -} -``` - -Оба метода только добавляют к экспортируемым метаданным, они никогда не перебивают конфигурацию `di › export` приложения. Поэтому, когда приложение ограничивает экспорт списком, нужные вашему расширению теги и типы остаются включёнными; только полное отключение экспорта тегов (`tags: false`) отбрасывает их вместе со всем остальным. diff --git a/dependency-injection/ru/factory.texy b/dependency-injection/ru/factory.texy deleted file mode 100644 index dffdf7fc06..0000000000 --- a/dependency-injection/ru/factory.texy +++ /dev/null @@ -1,233 +0,0 @@ -Генерируемые фабрики -******************** - -.[perex] -Nette DI умеет автоматически порождать код фабрик на основе интерфейсов, избавляя вас от написания кода. - -Фабрика - это класс, отвечающий за создание объектов и передачу их зависимостей. Не путайте это с шаблоном проектирования *factory method*, который описывает конкретный способ использования фабрик и с этой темой не связан. - -Как выглядит такая фабрика, мы показали во [вводной главе |introduction#Фабрика]: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Nette DI умеет порождать код фабрики автоматически. Вам достаточно создать интерфейс, а реализацию сгенерирует Nette DI. У интерфейса должен быть ровно один метод с именем `create` и объявленным типом возвращаемого значения: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Итак, у фабрики `ArticleFactory` есть метод `create`, создающий объекты `Article`. Класс `Article` может выглядеть, например, так: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } -} -``` - -Добавьте фабрику в конфигурационный файл: - -```neon -services: - - ArticleFactory -``` - -Nette DI сгенерирует соответствующую реализацию фабрики. - -В коде, который использует фабрику, запрашивайте объект по его интерфейсу, и Nette DI предоставит порождённую реализацию: - -```php -class UserController -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function foo() - { - // пусть фабрика создаст объект - $article = $this->articleFactory->create(); - } -} -``` - - -Фабрика с параметрами -===================== - -Метод фабрики `create` может принимать параметры, которые он затем передаёт в конструктор. Например, добавим в класс `Article` идентификатор автора статьи: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - private int $authorId, - ) { - } -} -``` - -Добавим параметр и в фабрику: - -```php -interface ArticleFactory -{ - function create(int $authorId): Article; -} -``` - -Поскольку имя параметра в конструкторе (`$authorId`) совпадает с именем параметра в методе фабрики, Nette DI передаёт его автоматически. - - -Расширенное определение -======================= - -Определение можно записать и в многострочной форме с помощью ключа `implement`: - -```neon -services: - articleFactory: - implement: ArticleFactory -``` - -Такая более длинная форма позволяет указать дополнительные аргументы конструктора через ключ `arguments` и дальнейшую настройку через `setup`, как у обычных определений сервисов. - -Пример: если бы метод `create()` не принимал параметр `$authorId`, мы могли бы задать в конфигурации фиксированное значение, передаваемое в конструктор `Article`: - -```neon -services: - articleFactory: - implement: ArticleFactory - arguments: - authorId: 123 -``` - -И наоборот, если бы `create()` принимал `$authorId`, но тот не входил бы в конструктор, а передавался бы методом вроде `Article::setAuthorId()`, мы сослались бы на параметр в секции `setup`: - -```neon -services: - articleFactory: - implement: ArticleFactory - setup: - - setAuthorId($authorId) -``` - - -Аксессор -======== - -Помимо фабрик Nette умеет порождать и так называемые аксессоры. Это объекты с методом `get()`, возвращающим определённый сервис из DI-контейнера. Повторные вызовы `get()` всегда возвращают один и тот же экземпляр. - -Аксессоры дают отложенную загрузку зависимостей. Представьте класс, который записывает ошибки в отдельную базу данных. Если бы этот класс получал соединение с базой через внедрение в конструктор, соединение устанавливалось бы всегда, даже если ошибки случаются редко и соединение почти всё время не используется. Вместо этого класс может получить аксессор. Объект базы данных (соединение) создаётся только тогда, когда метод `get()` аксессора вызывается впервые. - -Как создать аксессор? Достаточно написать интерфейс, а реализацию сгенерирует Nette DI. У интерфейса должен быть ровно один метод с именем `get`, без параметров и с объявленным типом возвращаемого значения: - -```php -interface PDOAccessor -{ - function get(): PDO; -} -``` - -Добавьте аксессор в конфигурационный файл вместе с определением сервиса, который он должен возвращать: - -```neon -services: - - PDOAccessor - - PDO(%dsn%, %user%, %password%) -``` - -Поскольку аксессор возвращает сервис `PDO`, а такой сервис в конфигурации определён только один, аксессор вернёт именно его. Если сервисов такого типа несколько, укажите по имени, какой из них должен вернуть аксессор, например `- PDOAccessor(@db1)`. - - -Мультифабрика и мультиаксессор -============================== - -До сих пор наши фабрики и аксессоры умели создавать или возвращать только один тип объектов. Однако вы легко можете создать мультифабрики, сочетающие возможности фабрик и аксессоров. Интерфейс такой составляющей может содержать несколько методов с именами `create<Name>()` и `get<Name>()`, например: - -```php -interface MultiFactory -{ - function createArticle(): Article; - function getDb(): PDO; -} -``` - -Так что вместо внедрения нескольких отдельных фабрик и аксессоров вы можете внедрить одну более всеобъемлющую составляющую. - -Как вариант, вместо нескольких методов можно использовать `get()` с параметром: - -```php -interface MultiFactoryAlt -{ - function get($name): PDO; -} -``` - -Тогда `MultiFactory::getDb()` делает то же самое, что `MultiFactoryAlt::get('db')`. Однако у этой альтернативной записи есть недостаток: из сигнатуры интерфейса не видно явно, какие значения `$name` поддерживаются. Кроме того, в интерфейсе нельзя задать разные типы возвращаемых значений для разных значений `$name`. - -Вместо `get($name)` интерфейс может объявить `create($name)`, который при каждом вызове возвращает новый экземпляр (тогда как `get()` возвращает общий). В интерфейсе может быть только один такой метод с параметром. Если тип возвращаемого значения метода nullable (например, `?PDO`), для неизвестного `$name` он возвращает `null` вместо выбрасывания исключения. - - -Определение списком -------------------- -Мультифабрику можно определить в конфигурации списком, записав сервисы прямо в нём: .{data-version:3.2.0} - -```neon -services: - - MultiFactory( - article: Article() # задаёт createArticle() - db: PDO(%dsn%, %user%, %password%) # задаёт getDb() - ) -``` - -Как вариант, в определении мультифабрики можно сослаться на существующие сервисы через ссылки: - -```neon -services: - article: Article - - PDO(%dsn%, %user%, %password%) - - MultiFactory( - article: @article # задаёт createArticle() - db: @\PDO # задаёт getDb() - ) -``` - - -Определение тегами ------------------- - -Ещё один способ определить мультифабрику - использовать [теги |services#Теги]. Значение тега определяет имя соответствующего метода: - -```neon -services: - article: - create: Article - tags: {multi: article} # задаёт createArticle() - db: - create: PDO(%dsn%, %user%, %password%) - tags: {multi: db} # задаёт getDb() - - - MultiFactory(tagged: multi) -``` diff --git a/dependency-injection/ru/faq.texy b/dependency-injection/ru/faq.texy deleted file mode 100644 index ce5e8ae1a7..0000000000 --- a/dependency-injection/ru/faq.texy +++ /dev/null @@ -1,123 +0,0 @@ -Часто задаваемые вопросы о DI (FAQ) -*********************************** - - -DI - это другое название IoC? ------------------------------ - -*Inversion of Control* (IoC) - принцип, описывающий поток управления в программе: ваш код вызывает внешний код или внешний код (например, фреймворк) вызывает ваш? IoC - широкое понятие, охватывающее [события |nette:glossary#События], так называемый [голливудский принцип |application:components#Голливудский стиль] и другие аспекты. К этому понятию относятся и фабрики, о которых говорится в разделе [Правило № 3: пусть этим займётся фабрика |introduction#Правило № 3: пусть этим займётся фабрика], представляющие собой инверсию оператора `new`. - -*Dependency Injection* (DI) сосредоточен на том, как объекты получают свои зависимости (то есть другие объекты, с которыми им нужно работать). Это шаблон проектирования, призывающий передавать зависимости объектам явно, а не заставлять объекты создавать или разыскивать их. - -Поэтому DI можно считать частным случаем IoC. Однако не все формы IoC способствуют чистоте кода. Например, антипаттернами являются приёмы, опирающиеся на [глобальное состояние|global-state] или на шаблон [Service Locator |#Что такое Service Locator?]. - - -Что такое Service Locator? --------------------------- - -Это альтернативный подход к Dependency Injection. Он состоит в том, что есть центральный объект (локатор), в котором зарегистрированы все доступные сервисы (зависимости). Когда объекту нужна зависимость, он запрашивает её у Service Locator. - -Однако по сравнению с DI ему не хватает прозрачности. Зависимости спрятаны внутри кода объекта (в вызовах локатора), а не выражены явно в его API (в конструкторе или методах), поэтому, чтобы разобраться в связях, приходится читать код. Тестирование тоже усложняется: вы не можете просто передать mock-зависимости при создании объекта, часто приходится вмешиваться в сам Service Locator. Кроме того, Service Locator вносит лишнюю зависимость: объекты становятся связаны с локатором, тогда как при DI объекты в идеале о контейнере ничего не знают. - - -Когда лучше не использовать DI? -------------------------------- - -Известных существенных недостатков правильного использования шаблона Dependency Injection нет. Наоборот, получение зависимостей из глобально доступных мест (таких как статические свойства или синглтоны) приводит к [множеству осложнений|global-state], как и использование Service Locator. Поэтому использовать DI, как правило, всегда уместно. Это не догма; просто ничего лучшего для чистого управления зависимостями пока широко не прижилось. - -Однако есть отдельные, ограниченные ситуации, когда глобальное обращение к объектам может быть допустимо. Например, при отладке, когда нужно вывести значение переменной, измерить время выполнения или записать сообщение в определённом месте. В этих случаях, связанных с временными действиями, которые позже уберут из кода, использование глобально доступного дампера, таймера или логгера может быть законным. Эти инструменты не входят в основную архитектуру приложения. - - -Есть ли у DI недостатки? ------------------------- - -Влечёт ли использование Dependency Injection недостатки вроде увеличения объёма кода или снижения производительности? Что мы теряем, начиная писать код в соответствии с DI? - -Сам DI пренебрежимо влияет на производительность во время выполнения и на расход памяти. Производительность DI-контейнера может иметь значение, но [Nette DI |nette-container] компилирует контейнер в обычный PHP-код, поэтому накладные расходы при выполнении приложения практически нулевые. - -Когда вы пишете код по принципам DI, вам часто приходится создавать конструкторы, принимающие зависимости. Раньше это могло казаться утомительным, но современные IDE и такие возможности, как [продвижение свойств конструктора |https://blog.nette.org/ru/php-8-0-complete-overview-of-news#toc-constructor-property-promotion] в PHP 8, делают это очень быстрым. Фабрики Nette DI часто может генерировать автоматически, что ещё сильнее сокращает шаблонный код. С другой стороны, отпадает необходимость писать синглтоны и статические аксессоры. - -В целом хорошо спроектированное приложение с DI обычно не заметно короче и не заметно длиннее того, что опирается на синглтоны или глобальный доступ. Код, отвечающий за создание и связывание зависимостей, просто переезжает из отдельных классов в специально отведённые места: конфигурацию DI-контейнера и фабрики. - - -Как переписать старое приложение на DI? ---------------------------------------- - -Переход старого приложения на Dependency Injection может оказаться непростым, особенно для больших и сложных приложений. Важно подойти к этому процессу систематически. - -- При переходе на Dependency Injection важно, чтобы все члены команды понимали используемые принципы и практики. -- Сначала проанализируйте существующее приложение, чтобы выявить ключевые составляющие и их зависимости. Составьте план, какие части и в каком порядке будут переработаны. -- Реализуйте DI-контейнер или, что лучше, используйте существующую библиотеку, например Nette DI. -- Постепенно переводите части приложения на Dependency Injection. Это может означать изменение конструкторов или методов так, чтобы они принимали зависимости параметрами. -- Обновите код в местах создания объектов, чтобы получать их из контейнера или использовать фабрики, предоставляемые контейнером. - -Помните, что переход на Dependency Injection - это вложение в качество кода и долгосрочную поддерживаемость приложения. Внести эти изменения может быть непросто, но результатом должен стать более чистый, модульный и легко тестируемый код, готовый к будущим расширениям и поддержке. - - -Почему композиция предпочтительнее наследования? ------------------------------------------------- -Для переиспользования кода [композиция |nette:introduction-to-object-oriented-programming#Композиция] обычно предпочтительнее [наследования |nette:introduction-to-object-oriented-programming#Наследование], потому что она даёт более слабую связанность. При композиции вы реже сталкиваетесь с тем, что изменение базового класса ломает зависимые подклассы. Типичный пример - ситуация, которую называют [адом конструкторов |passing-dependencies#Ад конструкторов]. - - -Можно ли использовать Nette DI Container вне Nette? ---------------------------------------------------- - -Разумеется. Nette DI Container входит в состав Nette, но спроектирован как самостоятельная библиотека, которую можно использовать независимо от других частей фреймворка. Достаточно установить её через Composer, создать конфигурационный файл с описанием ваших сервисов, а затем несколькими строками PHP-кода создать DI-контейнер. И вы сразу можете начать пользоваться преимуществами Dependency Injection в своих проектах. - -В главе [Nette DI Container |nette-container] описан конкретный сценарий использования с примерами кода. - - -Почему конфигурация в файлах NEON? ----------------------------------- - -NEON - простой и легко читаемый язык конфигурации, разработанный внутри Nette для настройки приложений, сервисов и их зависимостей. По сравнению с JSON или YAML он даёт для этой цели куда более интуитивные и гибкие возможности. В NEON можно естественно описать определения сервисов и связи, которые в JSON или YAML выразить так же наглядно было бы трудно или невозможно. - - -Не замедляет ли разбор файлов NEON приложение? ----------------------------------------------- - -Хотя файлы NEON разбираются очень быстро, скорость их разбора в продакшене по большому счёту не важна. Дело в том, что конфигурационные файлы разбираются только один раз, при первом запуске приложения (или при их изменении). После разбора порождается код DI-контейнера, он кешируется (сохраняется на диск), и при каждом последующем запросе выполняется уже этот скомпилированный PHP-код, так что повторный разбор не нужен. - -Так это работает в производственной среде. Во время разработки файлы NEON разбираются каждый раз при изменении их содержимого, благодаря чему у разработчика всегда актуальный DI-контейнер. Как уже сказано, сам разбор происходит очень быстро. - - -Как обратиться в своём классе к параметрам из конфигурационного файла? ----------------------------------------------------------------------- - -Помните [Правило № 1: пусть вам это передадут |introduction#Правило № 1: пусть вам это передадут]. Если классу нужны сведения из конфигурационного файла, не пытайтесь придумать, как класс может их *раздобыть*. Вместо этого просто запросите их, например через конструктор класса. А затем передайте это значение в конфигурационном файле. - -В этом примере `%myParameter%` - подстановка значения параметра `myParameter`, которое будет передано в конструктор `MyClass`: - -```neon -# config.neon -parameters: - myParameter: Some value - -services: - - MyClass(%myParameter%) -``` - -Если вы хотите передать несколько параметров или использовать autowiring, полезно [упаковать параметры в объект |best-practices:passing-settings-to-presenters]. - - -Поддерживает ли Nette интерфейс контейнера PSR-11? --------------------------------------------------- - -[Nette DI Container |api:Nette\DI\Container] не поддерживает PSR-11 напрямую. Однако если вам нужна совместимость Nette DI Container с библиотеками или фреймворками, ожидающими интерфейс контейнера PSR-11, вы можете создать [простой адаптер |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f], который послужит мостом между Nette DI Container и PSR-11. - - -Что означают термины container, compiler, definition и прочие? --------------------------------------------------------------- - -Краткий словарь слов, которые постоянно встречаются вокруг Nette DI, чаще всего при [написании расширений |extensions]: - -- **Container** (контейнер) - скомпилированный объект (`Nette\DI\Container`), который создаёт сервисы по требованию и держит их во время выполнения. Он порождается однажды в виде оптимизированного PHP-кода. -- **Compiler** (компилятор) - механизм, который превращает конфигурационные файлы и расширения в этот класс контейнера. -- **ContainerBuilder** - изменяемая модель контейнера, используемая во время компиляции; она хранит определения сервисов до того, как появится хотя бы один настоящий сервис. См. [Создание расширений |extensions#ContainerBuilder]. -- **Service** (сервис) - объект, управляемый контейнером, обычно создаваемый однажды и общий для всех (синглтон): соединение с базой данных, сервис отправки почты, логгер. -- **Definition** (определение) - рецепт сервиса: его тип, способ создания и то, что нужно сделать после. Nette превращает определения в фабричные методы контейнера; видов определений несколько (см. [типы определений |extensions#Типы определений]). -- **Type** (тип) - класс или интерфейс сервиса, который autowiring использует, чтобы сопоставить сервисы с местами, где они нужны. -- **Autowiring** - автоматическая передача сервисов в конструкторы и методы по их типу, чтобы вам не приходилось связывать зависимости вручную. -- **Tag** (тег) - метка, прикреплённая к определению (при желании со значением); расширение затем может найти все сервисы с этой меткой через `findByTag()`. -- **Setup** (настройка) - дополнительные действия над сервисом сразу после его создания: вызовы методов или присваивание свойств, добавляемые через `addSetup()`. -- **Alias** (псевдоним) - альтернативное имя существующего сервиса. diff --git a/dependency-injection/ru/global-state.texy b/dependency-injection/ru/global-state.texy deleted file mode 100644 index ef66900eaa..0000000000 --- a/dependency-injection/ru/global-state.texy +++ /dev/null @@ -1,294 +0,0 @@ -Глобальное состояние и синглтоны -******************************** - -.[perex] -Внимание: следующие конструкции - симптомы плохо спроектированного кода: - -- `Foo::getInstance()` -- `DB::insert(...)` -- `Article::setDb($db)` -- `ClassName::$var` или `static::$var` - -Встречаются ли какие-нибудь из этих конструкций в вашем коде? Если да, у вас есть возможность его улучшить. Вы можете подумать, что это обычные конструкции, которые встречаются в примерах решений из разных библиотек и фреймворков. Если так, то их код спроектирован ошибочно. - -Речь здесь не о какой-то академической чистоте. У всех этих конструкций есть одна общая черта: они используют глобальное состояние. А глобальное состояние губительно сказывается на качестве кода. Классы начинают вводить в заблуждение относительно своих зависимостей. Код становится непредсказуемым. Он сбивает разработчиков с толку и снижает их эффективность. - -В этой главе мы объясним, почему так происходит и как избежать глобального состояния. - - -Глобальные связи ----------------- - -В идеальном мире объект должен общаться только с объектами, которые ему [передали напрямую |passing-dependencies]. Если я создам два объекта `A` и `B` и никогда не передам ссылку между ними, то ни `A`, ни `B` не смогут обратиться к состоянию другого или изменить его. Это крайне желательное свойство кода. Это похоже на батарейку и лампочку: лампочка не загорится, пока вы не соедините её с батарейкой проводом. - -Однако для глобальных (статических) переменных или синглтонов это не так. Объект `A` может *по воздуху* обратиться к объекту `C` и изменить его без всякой передачи ссылки, вызвав `C::changeSomething()`. Если объект `B` тоже подключается к глобальному `C`, то `A` и `B` могут влиять друг на друга через `C`. - -Использование глобальных переменных вводит новый вид *беспроводной* связанности, невидимой снаружи. Оно создаёт дымовую завесу, из-за которой код становится сложнее понимать и использовать. Чтобы по-настоящему разобраться в зависимостях, разработчикам приходится читать каждую строку исходного кода, а не полагаться на интерфейсы классов. Кроме того, эта связанность совершенно не нужна. Глобальное состояние используют потому, что оно легко доступно откуда угодно и позволяет, например, писать в базу данных через глобальный (статический) метод `DB::insert()`. Однако, как мы покажем, кажущееся удобство ничтожно по сравнению с тяжёлыми осложнениями, которые оно приносит. - -.[note] -С точки зрения поведения между глобальной и статической переменной нет разницы. Они одинаково вредны. - - -Жуткое действие на расстоянии ------------------------------ - -"Жуткое действие на расстоянии" - так Альберт Эйнштейн знаменито назвал явление квантовой физики, которое приводило его в содрогание. -Речь о квантовой запутанности, когда измерение свойства одной частицы мгновенно влияет на другую запутанную частицу, независимо от разделяющего их расстояния, даже в миллионы световых лет, что как будто нарушает основополагающий закон Вселенной о том, что ничто не может двигаться быстрее света. - -В мире программ "жуткое действие на расстоянии" описывает ситуацию, когда мы запускаем процесс, который считаем изолированным (ведь никаких зависимостей явно не передавали), а в отдалённых частях системы неожиданно происходят взаимодействия и изменения состояния, о которых мы не подозреваем. Такое возможно только через глобальное состояние. - -Представьте, что вы приходите в команду разработки проекта с большой зрелой кодовой базой. Ваш новый руководитель просит реализовать новую возможность, и вы, как хороший разработчик, начинаете с написания теста. Но поскольку вы в проекте новичок, вы много экспериментируете в духе "что будет, если вызвать этот метод". И вы пробуете написать такой тест: - -```php -function testCreditCardCharge() -{ - $cc = new CreditCard('1234567890123456', 5, 2028); // номер вашей карты - $cc->charge(100); -} -``` - -Вы запускаете код, возможно несколько раз, и через некоторое время замечаете на телефоне уведомления банка: с вашей кредитной карты при каждом запуске списывали 100 долларов! 🤦‍♂️ - -Как вообще тест мог вызвать настоящее списание? Работа с кредитной картой - дело непростое. Нужно обратиться к стороннему веб-сервису, знать его URL, пройти аутентификацию и так далее. Ничего этого в тесте нет. Хуже того, вы не знаете, где эти сведения находятся, поэтому не можете подменить внешние зависимости заглушками, чтобы не терять 100 долларов при каждом запуске теста. И как вы, новый разработчик, должны были догадаться, что то, что вы собираетесь сделать, оставит вас на 100 долларов беднее? - -Вот это и есть жуткое действие на расстоянии! - -Вы вынуждены перелопачивать огромный исходный код и советоваться со старшими коллегами, чтобы понять взаимосвязи в проекте. Трудность возникает потому, что интерфейс класса `CreditCard` не выдаёт необходимой инициализации глобального состояния. Даже изучение исходного кода класса может не подсказать, какой метод инициализации вызвать. В лучшем случае вы найдёте, к какой глобальной переменной он обращается, и попробуете вывести, как её инициализировать. - -Классы в таком проекте - патологические лжецы. Класс `CreditCard` делает вид, что его можно просто создать и вызвать метод `charge()`. Однако втайне он взаимодействует с другим классом `PaymentGateway`, представляющим платёжный шлюз. Даже интерфейс `PaymentGateway` может намекать на самостоятельную инициализацию, а на деле он может вытаскивать учётные данные из конфигурационного файла и так далее. Исходные разработчики понимают, что `CreditCard` требует `PaymentGateway`. Они так написали код. Но для новичков это полная загадка, мешающая им учиться и вносить полезный вклад. - -Как исправить положение? Легко. **Пусть API объявляет зависимости.** - -```php -function testCreditCardCharge() -{ - $gateway = new PaymentGateway(/* ... */); - $cc = new CreditCard('1234567890123456', 5, 2028); - $cc->charge($gateway, 100); -} -``` - -Обратите внимание, как взаимозависимости в коде сразу становятся очевидны. Поскольку метод `charge()` объявляет, что ему нужен `PaymentGateway`, вам больше не нужно догадываться об этой зависимости или спрашивать о ней. Вы знаете, что нужно создать экземпляр, и при этом обнаружите нужные параметры доступа. Без них код просто не запустится. - -И, что важнее всего, теперь вы можете подменить платёжный шлюз заглушкой, чтобы с вас не списывали 100 долларов при каждом запуске теста. - -Глобальное состояние позволяет объектам тайно обращаться к зависимостям, не объявленным в их API, фактически превращая ваши API в патологических лжецов. - -Возможно, раньше вы об этом так не думали, но всякий раз, используя глобальное состояние, вы создаёте тайные беспроводные каналы связи. Это жуткое действие на расстоянии заставляет разработчиков читать каждую строку кода, чтобы понять возможные взаимодействия, снижает продуктивность и сбивает с толку новых членов команды. Если код создали вы, вы знаете настоящие зависимости, но тот, кто придёт после вас, останется в неведении. - -Не пишите код, опирающийся на глобальное состояние; предпочитайте явную передачу зависимостей. Возьмите на вооружение внедрение зависимостей. - - -Хрупкость глобального состояния -------------------------------- - -В коде, использующем глобальное состояние и синглтоны, вы никогда не можете быть уверены, когда и кем состояние было изменено. Этот риск проявляется даже при инициализации. Следующий код собирается создать соединение с базой данных и инициализировать платёжный шлюз, но раз за разом выбрасывает исключения, а искать причину крайне утомительно: - -```php -PaymentGateway::init(); -DB::init('mysql:', 'user', 'password'); -``` - -Вам придётся дотошно проследить код, чтобы обнаружить, что объект `PaymentGateway` по воздуху обращается к другим объектам, часть которых требует соединения с базой данных. Поэтому базу нужно инициализировать раньше `PaymentGateway`. Однако дымовая завеса глобального состояния скрывает это от вас. Сколько времени было бы сэкономлено, если бы API этих классов были честны и объявляли свои зависимости? - -```php -$db = new DB('mysql:', 'user', 'password'); -$gateway = new PaymentGateway($db, /* ... */); -``` - -Похожая проблема возникает при глобальном доступе к соединению с базой данных: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public function save(): void - { - DB::insert(/* ... */); - } -} -``` - -При вызове метода `save()` неясно, установлено ли соединение с базой данных и кто отвечает за его установку. Если нам нужно динамически поменять соединение с базой (например, для тестирования), мы можем скатиться к добавлению методов вроде `DB::reconnect(...)` или `DB::reconnectForTest()`. - -Рассмотрим пример: - -```php -$article = new Article; -// ... -DB::reconnectForTest(); -Foo::doSomething(); -$article->save(); -``` - -Как нам убедиться, что при вызове `$article->save()` действительно используется тестовая база данных? Что, если метод `Foo::doSomething()` изменил глобальное соединение с базой? Чтобы это выяснить, нам пришлось бы изучить исходный код `Foo` и, возможно, множества других классов. И это расследование дало бы лишь временный ответ, потому что позже положение может измениться. - -А что, если мы перенесём соединение с базой данных в статическую переменную внутри класса `Article`? - -```php -class Article -{ - private static DB $db; - - public static function setDb(DB $db): void - { - self::$db = $db; - } - - public function save(): void - { - self::$db->insert(/* ... */); - } -} -``` - -Это ничего не меняет. Проблема в самом глобальном состоянии, независимо от того, внутри какого класса оно спрятано. В этом случае, как и в предыдущем, при вызове `$article->save()` у нас нет уверенности, в какую базу данных запишутся данные. Кто угодно и где угодно в приложении мог в любой момент поменять базу через `Article::setDb()`. Без нашего ведома. - -Глобальное состояние делает наше приложение **чрезвычайно хрупким**. - -Однако с этой проблемой можно справиться просто. Достаточно, чтобы API объявлял зависимости, нужные для правильной работы. - -```php -class Article -{ - public function __construct( - private DB $db, - ) { - } - - public function save(): void - { - $this->db->insert(/* ... */); - } -} - -$article = new Article($db); -// ... -Foo::doSomething(); -$article->save(); -``` - -Такой подход снимает опасения по поводу скрытых или неожиданных изменений соединения с базой данных. Теперь мы точно знаем, куда сохраняется статья, и изменения в несвязанных классах на это уже не повлияют. Код больше не хрупкий, а устойчивый. - -Не пишите код, опирающийся на глобальное состояние; предпочитайте явную передачу зависимостей. Возьмите на вооружение внедрение зависимостей. - - -Синглтон --------- - -Синглтон - шаблон проектирования, который, по [определению |https://ru.wikipedia.org/wiki/Одиночка_(шаблон_проектирования)] из знаменитой книги "банды четырёх", ограничивает класс одним экземпляром и предоставляет к нему глобальный доступ. Реализация этого шаблона обычно похожа на такой код: - -```php -class Singleton -{ - private static self $instance; - - public static function getInstance(): self - { - self::$instance ??= new self; - return self::$instance; - } - - // и другие методы, выполняющие задачи класса -} -``` - -К сожалению, синглтон вносит в приложение глобальное состояние. А как мы показали выше, глобальное состояние нежелательно. Поэтому синглтон считается антипаттерном. - -Не используйте синглтоны в своём коде и заменяйте их другими механизмами. Синглтоны вам действительно не нужны. Однако если вам нужно обеспечить, чтобы во всём приложении существовал только один экземпляр класса, поручите эту обязанность [DI-контейнеру |container]. Так возникнет синглтон в пределах приложения, который обычно называют сервисом. Сам класс тогда освобождается от заботы о собственной уникальности (то есть у него не будет метода `getInstance()` или статического свойства с экземпляром) и может сосредоточиться исключительно на своих обязанностях. Тем самым он перестанет нарушать принцип единственной ответственности. - - -Глобальное состояние и тесты ----------------------------- - -Когда мы пишем тесты, мы в идеале исходим из того, что каждый тест - изолированная единица, в которую не входит и из которой не выходит никакое внешнее состояние. После завершения теста всё связанное с ним состояние должно автоматически убираться сборщиком мусора. Это делает тесты изолированными. Поэтому мы можем запускать тесты в любом порядке. - -Однако при наличии глобального состояния или синглтонов эти полезные предположения рушатся. Состояние может утекать в тесты и из них. Внезапно порядок тестов начинает иметь значение. - -Чтобы вообще протестировать код с синглтонами, разработчикам часто приходится поступаться их цельностью, например разрешая подменять экземпляр синглтона. Такие решения в лучшем случае остаются костылями, приводящими к коду, который трудно поддерживать и понимать. Любой тест (или его метод `tearDown()`), меняющий глобальное состояние, обязан дотошно откатывать эти изменения. - -Глобальное состояние - главная головная боль модульного тестирования! - -Как это исправить? Просто. Не пишите код, использующий синглтоны; предпочитайте явную передачу зависимостей. Возьмите на вооружение внедрение зависимостей. - - -Глобальные константы --------------------- - -Глобальное состояние не ограничивается синглтонами и статическими переменными, оно может относиться и к глобальным константам. - -Константы, значения которых выражают всеобщие истины (`M_PI`) или несут самодостаточные сведения (`PREG_BACKTRACK_LIMIT_ERROR`), в целом допустимы. И наоборот, константы, используемые как способ *по воздуху* подсунуть сведения в код, фактически являются скрытыми зависимостями. Как `LOG_FILE` в следующем примере. Использование константы `FILE_APPEND` совершенно корректно. - -```php -const LOG_FILE = '...'; - -class Foo -{ - public function doSomething() - { - // ... - file_put_contents(LOG_FILE, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Вместо этого нам следует объявить путь к файлу лога параметром конструктора класса `Foo`, сделав его явной частью его API: - -```php -class Foo -{ - public function __construct( - private string $logFile, - ) { - } - - public function doSomething() - { - // ... - file_put_contents($this->logFile, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Теперь мы передаём путь к файлу лога явно. Мы легко можем изменить его при необходимости, что упрощает тестирование и поддержку кода. - - -Глобальные функции и статические методы ---------------------------------------- - -Мы хотим подчеркнуть, что использование статических методов и глобальных функций само по себе не проблема. Мы объяснили сложности с методами вроде `DB::insert()`, но корнем проблемы всегда было лежащее в основе глобальное состояние, обычно хранящееся в статической переменной. Метод `DB::insert()` опирается на статическую переменную, хранящую соединение с базой данных. Без этой переменной реализовать метод было бы невозможно. - -Использование детерминированных статических методов и функций вроде `Closure::fromCallable()`, `strlen()` и множества других полностью совместимо с внедрением зависимостей. Эти функции предсказуемы, потому что при одних и тех же входных параметрах всегда возвращают один и тот же результат. Они не используют никакого глобального состояния. - -Однако в PHP есть и недетерминированные функции. К ним относится, например, функция `htmlspecialchars()`. Её третий параметр `$encoding`, если его опустить, по умолчанию берёт значение параметра конфигурации `default_charset` (`ini_get('default_charset')`). Поэтому рекомендуется всегда указывать этот параметр, чтобы избежать возможного непредсказуемого поведения. Nette последовательно так и делает. - -Некоторые функции, такие как `strtolower()` и `strtoupper()`, ещё недавно вели себя недетерминированно в зависимости от настройки локали (`setlocale()`). Это вызывало множество осложнений, чаще всего при работе с турецким языком. Дело в том, что в турецком различаются "I" с точкой и без точки как в нижнем, так и в верхнем регистре. В результате `strtolower('I')` возвращала `ı` (строчную i без точки), а `strtoupper('i')` - `İ` (заглавную I с точкой), что приводило к множеству загадочных ошибок в приложениях. Однако в PHP версии 8.2 эта проблема исправлена, и функции больше не зависят от локали. - -Это хороший пример того, как глобальное состояние (настройка локали) досаждало тысячам разработчиков по всему миру. Решением в итоге стало сделать функции независимыми от локали, фактически убрав скрытую зависимость. - - -Когда глобальное состояние всё же можно использовать? ------------------------------------------------------ - -Есть отдельные, ограниченные ситуации, когда использование глобального состояния может быть допустимо. Например, при отладке, когда нужно вывести значение переменной или измерить время выполнения определённого участка кода. В этих случаях, связанных с временными действиями, которые позже уберут из кода, использование глобально доступного дампера или таймера может быть законным. Эти инструменты не входят в основную архитектуру приложения. - -Другой пример - функции регулярных выражений PHP (`preg_*`), которые внутренне кешируют скомпилированные регулярные выражения в статической памяти. Когда вы многократно вызываете функции с одним и тем же регулярным выражением в разных местах кода, выражение компилируется только один раз. Такое кеширование повышает производительность и совершенно невидимо для пользователя, поэтому такое использование внутреннего статического состояния в целом допустимо. - - -Итоги ------ - -Мы обсудили, почему имеет смысл: - -1) убрать из кода все изменяемые статические свойства (глобальное состояние) -2) явно объявлять зависимости -3) и использовать внедрение зависимостей - -Проектируя код, помните, что каждое изменяемое `static $foo` - потенциальный источник проблем. Чтобы создать среду, дружественную к DI, принципиально важно полностью убрать глобальное состояние и заменить его внедрением зависимостей. - -В ходе этого вы можете обнаружить, что классы с несколькими обязанностями нужно разделить. Не стесняйтесь это делать, стремитесь к принципу единственной ответственности. - -*Хочу поблагодарить Miško Hevery, чьи статьи, такие как [Flaw: Brittle Global State & Singletons |https://web.archive.org/web/20230321084133/http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/], легли в основу этой главы.* diff --git a/dependency-injection/ru/introduction.texy b/dependency-injection/ru/introduction.texy deleted file mode 100644 index f1c8ca28a3..0000000000 --- a/dependency-injection/ru/introduction.texy +++ /dev/null @@ -1,525 +0,0 @@ -Что такое Dependency Injection? -******************************* - -.[perex] -Эта глава знакомит с базовыми приёмами программирования, которых стоит придерживаться при написании любого приложения. Это основы, необходимые для чистого, понятного и поддерживаемого кода. - -Если вы примете и будете соблюдать эти правила, Nette поддержит вас на каждом шагу. Он возьмёт на себя рутину и обеспечит максимальное удобство, чтобы вы могли сосредоточиться на самой логике. - -Принципы, которые мы здесь покажем, довольно просты. Бояться нечего. - - -Помните свою первую программу? ------------------------------- - -Мы не знаем, на каком языке вы её написали, но если на PHP, она, вероятно, выглядела примерно так: - -```php -function addition(float $a, float $b): float -{ - return $a + $b; -} - -echo addition(23, 1); // выводит 24 -``` - -Несколько тривиальных строк кода, а сколько ключевых понятий в них скрыто. Что существуют переменные. Что код делится на меньшие единицы, например на функции. Что мы передаём в них входные аргументы, а они возвращают результаты. Не хватает только условий и циклов. - -То, что мы передаём в функцию входные данные, а она возвращает результат, - совершенно понятное представление, используемое и в других областях, например в математике. - -У функции есть сигнатура, состоящая из её имени, списка параметров с их типами и, наконец, типа возвращаемого значения. Как пользователей нас интересует сигнатура; о внутренней реализации нам обычно знать не нужно. - -А теперь представьте, что сигнатура функции выглядела бы так: - -```php -function addition(float $x): float -``` - -Сложение с одним параметром? Странно… А как насчёт такого? - -```php -function addition(): float -``` - -Вот это уже совсем странно, правда? Как эта функция используется? - -```php -echo addition(); // что она выведет? -``` - -Глядя на такой код, мы бы растерялись. Не только новичок его не понял бы, но и опытный программист не разобрался бы в таком коде. - -Задумались, как такая функция выглядела бы внутри? Откуда бы она брала числа для сложения? Вероятно, она *как-нибудь* раздобыла бы их сама, скажем так: - -```php -function addition(): float -{ - $a = Input::get('a'); - $b = Input::get('b'); - return $a + $b; -} -``` - -В теле функции мы обнаружили скрытые зависимости от других глобальных функций или статических методов. Чтобы выяснить, откуда на самом деле берутся числа, нам нужно копать дальше. - - -Только не так! --------------- - -Только что показанное решение - средоточие множества отрицательных свойств: - -- Сигнатура функции делала вид, что числа для сложения ей не нужны, и это сбило нас с толку. -- Мы понятия не имеем, как заставить функцию сложить два других числа. -- Нам пришлось лезть в код, чтобы выяснить, откуда она берёт числа. -- Мы обнаружили скрытые зависимости. -- Полное понимание требует изучить и эти зависимости. - -И вообще, дело ли функции сложения - добывать входные данные? Разумеется, нет. Её обязанность - только само сложение. - - -Мы не хотим встречать такой код и уж точно не хотим его писать. Исправление простое: вернуться к основам и просто использовать параметры: - - -```php -function addition(float $a, float $b): float -{ - return $a + $b; -} -``` - - -Правило № 1: пусть вам это передадут ------------------------------------- - -Самое важное правило гласит: **все данные, которые нужны функциям или классам, должны быть им переданы**. - -Вместо того чтобы придумывать скрытые способы, которыми они раздобудут данные, просто передайте параметры. Вы сэкономите время, потраченное на выдумывание скрытых путей, которые точно не улучшат ваш код. - -Если вы будете всегда и везде соблюдать это правило, вы на пути к коду без скрытых зависимостей. К коду, понятному не только автору, но и всякому, кто прочитает его позже. Где всё понятно из сигнатур функций и классов и не нужно разыскивать скрытые подробности в реализации. - -Профессионально этот приём называется **Dependency Injection** (внедрение зависимостей). А сами данные называются **зависимостями**. Это просто передача параметров, ничего больше. - -.[note] -Пожалуйста, не путайте Dependency Injection, который является шаблоном проектирования, с "контейнером внедрения зависимостей", который является инструментом, то есть чем-то принципиально другим. О контейнерах мы поговорим позже. - - -От функций к классам --------------------- - -А как это относится к классам? Класс - сущность посложнее простой функции, но правило № 1 полностью применимо и здесь. Просто [способов передать аргументы |passing-dependencies] больше. Например, довольно похоже на случай с функцией: - -```php -class Math -{ - public function sum(float $a, float $b): float - { - return $a + $b; - } -} - -$math = new Math; -echo $math->sum(23, 1); // 24 -``` - -Или другими методами, или прямо через конструктор: - -```php -class Sum -{ - public function __construct( - private float $a, - private float $b, - ) { - } - - public function calculate(): float - { - return $this->a + $this->b; - } -} - -$sum = new Sum(23, 1); -echo $sum->calculate(); // 24 -``` - -Оба примера полностью соответствуют Dependency Injection. - - -Примеры из жизни ----------------- - -В реальном мире вы не будете писать классы для сложения чисел. Перейдём к практическим примерам. - -Пусть у нас будет класс `Article`, представляющий статью блога: - -```php -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - // сохраняем статью в базу данных - } -} -``` - -а использование будет таким: - -```php -$article = new Article; -$article->title = '10 Things You Need to Know About Losing Weight'; -$article->content = 'Every year millions of people in ...'; -$article->save(); -``` - -Метод `save()` сохранит статью в таблицу базы данных. Реализовать его с помощью [Nette Database |database:] было бы просто, если бы не одна загвоздка: откуда `Article` возьмёт соединение с базой данных, то есть объект класса `Nette\Database\Connection`? - -Кажется, что вариантов много. Он мог бы взять его из статической переменной. Или наследуясь от класса, предоставляющего соединение с базой. Или использовать [синглтон |global-state#Синглтон]. Или так называемые фасады, как в Laravel: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - DB::insert( - 'INSERT INTO articles (title, content) VALUES (?, ?)', - [$this->title, $this->content], - ); - } -} -``` - -Отлично, задачу мы решили. - -Или всё же нет? - -Вспомним [#Правило № 1: пусть вам это передадут]: все зависимости, которые нужны классу, должны быть ему переданы. Потому что если мы нарушим правило, мы вступим на путь запутанного кода со скрытыми зависимостями и недостатком ясности, а в итоге получим приложение, которое трудно поддерживать и развивать. - -Пользователь класса `Article` понятия не имеет, куда метод `save()` сохраняет статью. В таблицу базы данных? В какую именно, в производственную или тестовую? И как это поменять? - -Пользователю приходится смотреть, как реализован метод `save()`, и он находит использование метода `DB::insert()`. Значит, ему нужно копать дальше, чтобы понять, как этот метод получает соединение с базой данных. А скрытые зависимости могут образовывать довольно длинную цепочку. - -В чистом и хорошо спроектированном коде никогда не бывает скрытых зависимостей, фасадов Laravel или статических переменных. В чистом и хорошо спроектированном коде аргументы передаются: - -```php -class Article -{ - public function save(Nette\Database\Connection $db): void - { - $db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -Ещё практичнее, как мы увидим позже, использовать конструктор: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function save(): void - { - $this->db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -.[note] -Если вы опытный программист, вы можете подумать, что у `Article` вообще не должно быть метода `save()`: он должен представлять чистую структуру данных, а сохранением должен заниматься отдельный репозиторий. И это разумно. Но это увело бы нас далеко за пределы темы, которой является Dependency Injection, и цели давать простые примеры. - -Если вы пишете класс, которому для работы нужна, например, база данных, не выдумывайте, откуда её взять, а попросите, чтобы её вам передали. Скажем, параметром конструктора или другого метода. Признайте зависимости. Признайте их в API своего класса. Вы получите понятный и предсказуемый код. - -А как насчёт этого класса, который записывает сообщения об ошибках? - -```php -class Logger -{ - public function log(string $message): void - { - $file = LOG_DIR . '/log.txt'; - file_put_contents($file, $message . "\n", FILE_APPEND); - } -} -``` - -Как вы думаете, соблюли ли мы [#Правило № 1: пусть вам это передадут]? - -Нет. - -Ключевые сведения, каталог с файлом лога, класс *добывает сам* из константы. - -Посмотрите на пример использования: - -```php -$logger = new Logger; -$logger->log('Temperature is 23 °C'); -$logger->log('Temperature is 10 °C'); -``` - -Не зная реализации, смогли бы вы ответить, куда пишутся сообщения? Пришло бы вам в голову, что для его работы нужно существование константы `LOG_DIR`? И смогли бы вы создать второй экземпляр, который писал бы в другое место? Уж точно нет. - -Исправим класс: - -```php -class Logger -{ - public function __construct( - private string $file, - ) { - } - - public function log(string $message): void - { - file_put_contents($this->file, $message . "\n", FILE_APPEND); - } -} -``` - -Класс теперь куда понятнее, настраиваемее и потому полезнее. - -```php -$logger = new Logger('/path/to/log.txt'); -$logger->log('Temperature is 15 °C'); -``` - - -Но мне всё равно! ------------------ - -*"Когда я создаю объект Article и вызываю save(), я не хочу возиться с базой данных; я просто хочу, чтобы он сохранился в той, которую я настроил."* - -*"Когда я использую Logger, я просто хочу, чтобы сообщение записалось, и не хочу разбираться, куда. Пусть используются глобальные настройки."* - -Это справедливые замечания. - -Для примера покажем класс, который рассылает новостные письма и записывает результат в лог: - -```php -class NewsletterDistributor -{ - public function distribute(): void - { - $logger = new Logger(/* ... */); - try { - $this->sendEmails(); - $logger->log('Emails have been sent out'); - - } catch (Exception $e) { - $logger->log('An error occurred during sending'); - throw $e; - } - } -} -``` - -Улучшенный `Logger`, который больше не использует константу `LOG_DIR`, требует путь к файлу в конструкторе. Как это решить? Класс `NewsletterDistributor` не заботит, куда пишутся сообщения, он просто хочет их записывать. - -Решение снова [#Правило № 1: пусть вам это передадут]: мы передаём все данные, которые нужны классу. - -Значит ли это, что мы передадим путь к логу через конструктор, а затем используем его при создании объекта `Logger`? - -```php -class NewsletterDistributor -{ - public function __construct( - private string $file, // ⛔ НЕ ТАК! - ) { - } - - public function distribute(): void - { - $logger = new Logger($this->file); -``` - -Не так! Потому что путь - это **не** данные, которые нужны классу `NewsletterDistributor`; они нужны `Logger`. Улавливаете разницу? Классу `NewsletterDistributor` нужен сам логгер. Поэтому мы передадим сам логгер: - -```php -class NewsletterDistributor -{ - public function __construct( - private Logger $logger, // ✅ - ) { - } - - public function distribute(): void - { - try { - $this->sendEmails(); - $this->logger->log('Emails have been sent out'); - - } catch (Exception $e) { - $this->logger->log('An error occurred during sending'); - throw $e; - } - } -} -``` - -Теперь из сигнатуры класса `NewsletterDistributor` ясно, что ведение лога входит в его работу. А задача подменить логгер другим, скажем ради тестирования, становится совершенно тривиальной. Более того, если конструктор класса `Logger` изменится, на наш класс это никак не повлияет. - - -Правило № 2: берите своё ------------------------- - -Не путайтесь и не принимайте зависимости своих зависимостей. Принимайте только собственные зависимости. - -Благодаря этому код, использующий другие объекты, будет полностью независим от изменений их конструкторов. Его API станет точнее. А главное, подменить эти зависимости другими будет проще простого. - - -Новый член семьи ----------------- - -Команда разработки решила создать второй логгер, который пишет в базу данных. Итак, мы создаём класс `DatabaseLogger`. Теперь у нас два класса, `Logger` и `DatabaseLogger`; один пишет в файл, другой в базу данных... не кажутся ли имена странноватыми? Не лучше ли переименовать `Logger` в `FileLogger`? Определённо. - -Но сделаем это с умом. Мы создадим интерфейс с исходным именем: - -```php -interface Logger -{ - function log(string $message): void; -} -``` - -… который реализуют оба логгера: - -```php -class FileLogger implements Logger -// ... - -class DatabaseLogger implements Logger -// ... -``` - -И благодаря этому в остальном коде, где используется логгер, ничего менять не придётся. Например, конструктор класса `NewsletterDistributor` по-прежнему будет доволен требованием параметра `Logger`. А уже от нас будет зависеть, какой экземпляр мы ему предоставим. - -**Именно поэтому мы никогда не добавляем к именам интерфейсов суффикс `Interface` или префикс `I`.** Иначе расширить код так изящно было бы невозможно. - - -Хьюстон, у нас проблема ------------------------ - -Во всём приложении мы можем обойтись одним экземпляром логгера, файлового или базы данных, и просто передавать его туда, где ведётся лог, но с классом `Article` дело обстоит совсем иначе. Мы создаём его экземпляры по мере надобности, даже многократно. Как быть с зависимостью от базы данных в его конструкторе? - -Примером может служить контроллер, который должен сохранить статью в базу данных после отправки формы: - -```php -class EditController extends Controller -{ - public function formSubmitted($data) - { - $article = new Article(/* ... */); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Возможное решение кажется очевидным: передадим объект базы данных через конструктор в `EditController` и используем `$article = new Article($this->db)`. - -Как и в предыдущем случае с `Logger` и путём к файлу, это неправильный подход. База данных - зависимость не `EditController`, а `Article`. Передача базы данных тем самым нарушает [Правило № 2: берите своё |#Правило № 2: берите своё]. Если конструктор класса `Article` изменится (добавится новый параметр), вам придётся править код во всех местах, где создаются экземпляры. Ох. - -Хьюстон, что предлагаете? - - -Правило № 3: пусть этим займётся фабрика ----------------------------------------- - -Убрав скрытые зависимости и передавая все зависимости аргументами, мы получили более настраиваемые и гибкие классы. Поэтому нам нужно что-то ещё, что будет создавать и настраивать эти более гибкие классы за нас. Мы назовём это фабриками. - -Правило гласит: если у класса есть зависимости, поручите создание его экземпляров фабрике. - -Фабрики - более умная альтернатива оператору `new` в мире Dependency Injection. - -.[note] -Пожалуйста, не путайте это с шаблоном проектирования *factory method*, который описывает конкретный способ использования фабрик и с этой темой не связан. - - -Фабрика -------- - -Фабрика - это метод или класс, который создаёт и настраивает объекты. Класс, производящий `Article`, мы назовём `ArticleFactory`, и он может выглядеть так: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Его использование в контроллере будет таким: - -```php -class EditController extends Controller -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function formSubmitted($data) - { - // пусть фабрика создаст объект - $article = $this->articleFactory->create(); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Теперь, если сигнатура конструктора класса `Article` изменится, единственная часть кода, которая должна отреагировать, - сама `ArticleFactory`. Весь остальной код, работающий с объектами `Article`, например `EditController`, останется нетронутым. - -Возможно, вы сейчас чешете затылок, размышляя, действительно ли мы улучшили положение. Объём кода вырос, и всё вместе начинает выглядеть подозрительно сложно. - -Не переживайте, вскоре мы доберёмся до DI-контейнера Nette. А у него в рукаве припасено несколько приёмов, которые сильно упростят построение приложений с Dependency Injection. Например, вместо класса `ArticleFactory` достаточно будет [написать только интерфейс |factory]: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Но мы забегаем вперёд, оставайтесь с нами :-) - - -Итоги ------ - -В начале этой главы мы пообещали показать порядок проектирования чистого кода. Достаточно позаботиться о том, чтобы классам: - -1) [передавались зависимости, которые им нужны |#Правило № 1: пусть вам это передадут] -2) [и наоборот, не передавалось то, что им напрямую не нужно |#Правило № 2: берите своё] -3) [а объекты с зависимостями лучше всего создавать в фабриках |#Правило № 3: пусть этим займётся фабрика] - -На первый взгляд это может быть неочевидно, но эти три правила имеют далеко идущие последствия. Они приводят к принципиально другому взгляду на проектирование кода. Стоит ли оно того? Программисты, которые оставили старые привычки и стали последовательно использовать Dependency Injection, считают этот шаг поворотным в своей профессиональной карьере. Он открыл им мир ясных и поддерживаемых приложений. - -А что, если код не использует Dependency Injection последовательно? Что, если он построен на статических методах или синглтонах? Приводит ли это к проблемам? [Да, и к весьма серьёзным |global-state]. diff --git a/dependency-injection/ru/nette-container.texy b/dependency-injection/ru/nette-container.texy deleted file mode 100644 index 29d2e44d14..0000000000 --- a/dependency-injection/ru/nette-container.texy +++ /dev/null @@ -1,117 +0,0 @@ -Nette DI Container -****************** - -.[perex] -Nette DI - одна из самых интересных библиотек Nette. Она умеет порождать и автоматически обновлять скомпилированные DI-контейнеры, которые исключительно быстры и на удивление просты в настройке. - -Вид сервисов, которые должен создавать DI-контейнер, обычно задаётся в конфигурационных файлах [формата NEON|neon:format]. Контейнер, который мы вручную создали в [предыдущей главе|container], записывался бы так: - -```neon -parameters: - db: - dsn: 'mysql:' - user: root - password: '***' - -services: - - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - - ArticleFactory - - EditController -``` - -Запись очень краткая. - -Все зависимости, объявленные в конструкторах классов `ArticleFactory` и `EditController`, Nette DI обнаруживает и передаёт автоматически благодаря так называемому [autowiring|autowiring], поэтому в конфигурационном файле указывать ничего не нужно. Так что даже при изменении параметров менять в конфигурации ничего не придётся. Во время разработки Nette автоматически перегенерирует контейнер. Вы можете сосредоточиться исключительно на разработке приложения. - -Если мы хотим передавать зависимости через сеттеры, для этого служит секция [setup |services#Setup]. - -Nette DI порождает PHP-код контейнера напрямую. Результатом становится файл `.php`, который вы можете открыть и изучить. Благодаря этому вы видите в точности, как работает контейнер. Вы можете и отлаживать его в своей IDE, проходя по шагам. И, что важнее всего, порождённый PHP-код исключительно быстр. - -Nette DI умеет порождать и код [фабрики|factory] на основе заданного интерфейса. Поэтому вместо класса `ArticleFactory` нам достаточно создать в приложении только интерфейс: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Полный пример вы найдёте [на GitHub|https://github.com/nette-examples/di-example-doc]. - - -Самостоятельное использование ------------------------------ - -Встроить библиотеку Nette DI в приложение очень просто. Сначала установим её через Composer (потому что скачивать zip-файлы давно вышло из моды): - -```shell -composer require nette/di -``` - -Следующий код использует [Compiler |api:Nette\DI\Compiler], чтобы создать экземпляр DI-контейнера по конфигурации, хранящейся в файле `config.neon`: - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp'); -$class = $loader->load(function ($compiler) { - $compiler->loadConfig(__DIR__ . '/config.neon'); -}); -$container = new $class; -``` - -Контейнер порождается только один раз, его код записывается в кеш (каталог `__DIR__ . '/temp'`), а при последующих запросах только оттуда загружается. - -Сам по себе `Compiler` включает в конфигурации только секции `services` и `parameters`. Чтобы использовать остальные, такие как `search`, `decorator`, `di` или `inject`, сначала зарегистрируйте их расширения. А чтобы можно было регистрировать расширения из секции `extensions` конфигурации, добавьте `ExtensionsExtension`: - -```php -$compiler->addExtension('search', new Nette\DI\Extensions\SearchExtension($tempDir)); -$compiler->addExtension('extensions', new Nette\DI\Extensions\ExtensionsExtension); -``` - -[Configurator |application:bootstrapping], используемый в полноценных приложениях Nette, регистрирует всё это автоматически. - -Если вы держите несколько разных контейнеров в одном каталоге кеша, различайте их ключом, передаваемым вторым аргументом в `load()`; он становится частью имени порождаемого класса: - -```php -$class = $loader->load( - fn($compiler) => $compiler->loadConfig(__DIR__ . '/config.neon'), - 'my-key', -); -``` - -Для создания и получения сервисов служат методы `getService()` или `getByType()`. Вот так мы создаём объект `EditController`: - -```php -$controller = $container->getByType(EditController::class); -$controller->someMethod(); -``` - -Во время разработки полезно включить режим автообновления, когда контейнер автоматически перегенерируется при изменении любого класса или конфигурационного файла. Достаточно передать `true` вторым аргументом в конструктор [ContainerLoader |api:Nette\DI\ContainerLoader]. - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp', true); -``` - - -Работа с контейнером --------------------- - -Помимо `getService()` и `getByType()`, объект контейнера предлагает ещё несколько полезных методов: - -- `getByType(string $type, bool $throw = true): ?object` возвращает сервис заданного типа. Если передать вторым аргументом `false`, при отсутствии такого сервиса вместо исключения возвращается `null`. -- `hasService(string $name): bool` и `isCreated(string $name): bool` сообщают, определён ли сервис и был ли он уже создан. -- `getParameters(): array` возвращает все параметры контейнера, `getParameter($key)` возвращает один из них. -- `createInstance(string $class, array $args = []): object` создаёт новый экземпляр заданного класса и передаёт зависимости его конструктора через autowiring. -- `callMethod(callable $function, array $args = []): mixed` вызывает заданный callable и передаёт его аргументы через autowiring. -- `callInjects(object $service): void` вызывает у заданного объекта все методы `inject*()` и передаёт им зависимости. - -Конструктор контейнера принимает также массив параметров, дополняющих те, что заданы в конфигурации: - -```php -$container = new $class(['host' => 'localhost']); -``` - - -Использование с Nette Framework -------------------------------- - -Как мы показали, использование Nette DI не ограничено приложениями на Nette Framework: встроить его можно куда угодно всего тремя строками кода. Однако если вы разрабатываете приложения на Nette Framework, конфигурацией и созданием контейнера занимается [Bootstrap |application:bootstrapping#Конфигурация DI-контейнера]. diff --git a/dependency-injection/ru/passing-dependencies.texy b/dependency-injection/ru/passing-dependencies.texy deleted file mode 100644 index 686187b5ed..0000000000 --- a/dependency-injection/ru/passing-dependencies.texy +++ /dev/null @@ -1,215 +0,0 @@ -Передача зависимостей -********************* - -<div class=perex> - -Аргументы, или, в терминологии DI, зависимости, можно передавать классам следующими основными способами: - -* внедрение через конструктор -* внедрение через метод (так называемое внедрение через сеттер) -* внедрение в свойство -* с помощью метода `inject*()` или атрибута `#[Inject]` - -</div> - -Покажем каждый вариант на конкретных примерах. - - -Внедрение через конструктор -=========================== - -Зависимости передаются как аргументы конструктора в момент создания объекта: - -```php -class MyClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -$obj = new MyClass($cache); -``` - -Этот подход подходит для обязательных зависимостей, без которых класс совершенно не может работать, потому что без них экземпляр создать нельзя. - -Начиная с PHP 8.0 мы можем использовать более краткую запись ([продвижение свойств конструктора |https://blog.nette.org/ru/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), функционально равнозначную: - -```php -// PHP 8.0 -class MyClass -{ - public function __construct( - private Cache $cache, - ) { - } -} -``` - -Начиная с PHP 8.1 свойство можно пометить флагом `readonly`, который объявляет, что значение свойства после инициализации меняться не будет: - -```php -// PHP 8.1 -class MyClass -{ - public function __construct( - private readonly Cache $cache, - ) { - } -} -``` - -DI-контейнер передаёт зависимости в конструктор автоматически через [autowiring |autowiring]. Аргументы, которые так передать нельзя (например, строки, числа, логические значения), [указываются в конфигурации |services#Аргументы]. - - -Ад конструкторов ----------------- - -Термином *ад конструкторов* описывают ситуацию, когда дочерний класс наследует от родительского, конструктору которого нужны зависимости, и самому дочернему классу тоже нужны зависимости. Тогда он вынужден принимать и передавать дальше и зависимости родителя: - -```php -abstract class BaseClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass extends BaseClass -{ - private Database $db; - - // ⛔ АД КОНСТРУКТОРОВ - public function __construct(Cache $cache, Database $db) - { - parent::__construct($cache); - $this->db = $db; - } -} -``` - -Проблема возникает, когда мы хотим изменить конструктор `BaseClass`, например когда добавляется новая зависимость. Тогда приходится менять и все конструкторы дочерних классов. Что превращает такое изменение в ад. - -Как этого избежать? Решение - **предпочитать [композицию наследованию |faq#Почему композиция предпочтительнее наследования?]**. - -Итак, мы проектируем код иначе. Мы обойдёмся без [абстрактных |nette:introduction-to-object-oriented-programming#Абстрактные классы] классов `Base*`. Вместо того чтобы `MyClass` получал определённую функциональность наследованием от `BaseClass`, эта функциональность будет передана ему как зависимость: - -```php -final class SomeFunctionality -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass -{ - private SomeFunctionality $sf; - private Database $db; - - public function __construct(SomeFunctionality $sf, Database $db) // ✅ - { - $this->sf = $sf; - $this->db = $db; - } -} -``` - - -Внедрение через сеттер -====================== - -Зависимости передаются вызовом метода, который сохраняет их в приватное свойство. Общепринятое соглашение об именовании таких методов - шаблон `set*()`, отсюда и название "сеттеры", но, разумеется, они могут называться иначе. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - $this->cache = $cache; - } -} - -$obj = new MyClass; -$obj->setCache($cache); -``` - -Этот подход подходит для необязательных зависимостей, без которых класс может работать, потому что нет гарантии, что объект действительно получит зависимость (то есть что вызывающий вызовет метод). - -При этом такой способ позволяет вызывать сеттер многократно и менять зависимость. Если это нежелательно, добавьте в метод проверку или, начиная с PHP 8.1, пометьте свойство `$cache` флагом `readonly`. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - if (isset($this->cache)) { - throw new RuntimeException('The dependency has already been set'); - } - $this->cache = $cache; - } -} -``` - -Вызов сеттера задаётся в конфигурации DI-контейнера в [ключе setup |services#Setup]. Здесь тоже используется автоматическая передача зависимостей через autowiring: - -```neon -services: - - create: MyClass - setup: - - setCache -``` - - -Внедрение в свойство -==================== - -Зависимости передаются записью прямо в свойство объекта: - -```php -class MyClass -{ - public Cache $cache; -} - -$obj = new MyClass; -$obj->cache = $cache; -``` - -Этот способ считается неудачным, потому что свойство должно быть объявлено как `public`. В результате мы теряем контроль над тем, действительно ли переданная зависимость нужного типа (особенно это было верно до появления объявлений типов свойств в PHP 7.4), и теряем возможность отреагировать на присвоенную зависимость собственной логикой, например запретить последующее изменение. При этом свойство становится частью публичного API класса, чего может и не подразумеваться. - -Присваивание свойства задаётся в конфигурации DI-контейнера в [секции setup |services#Setup]: - -```neon -services: - - create: MyClass - setup: - - $cache = @\Cache -``` - - -Inject -====== - -Три предыдущих подхода применимы вообще во всех объектно-ориентированных языках, а внедрение через методы `inject*()` или атрибут `#[Inject]` обычно используется с презентерами Nette, где оно включено по умолчанию; любой другой сервис может подключить его через [`inject: true` |services#Режим inject]. О них говорится в [отдельной главе |best-practices:inject-method-attribute]. - - -Какой способ выбрать? -===================== - -- Конструктор подходит для обязательных зависимостей, без которых класс совершенно не может работать. -- Сеттер, наоборот, подходит для необязательных зависимостей или тех, которые позже может понадобиться поменять. -- Публичные свойства в целом не рекомендуются. diff --git a/dependency-injection/ru/services.texy b/dependency-injection/ru/services.texy deleted file mode 100644 index ea79c864c7..0000000000 --- a/dependency-injection/ru/services.texy +++ /dev/null @@ -1,480 +0,0 @@ -Определение сервисов -******************** - -.[perex] -В конфигурации мы указываем DI-контейнеру, как создавать отдельные сервисы и как связывать их с зависимостями. Nette предлагает для этого очень наглядный и изящный способ. - -Секция `services` в конфигурационном файле NEON - это место, где мы определяем собственные сервисы и их настройку. Посмотрим на простой пример, определяющий сервис с именем `database`, который представляет экземпляр класса `PDO`: - -```neon -services: - database: PDO('sqlite::memory:') -``` - -Приведённая конфигурация порождает в [DI-контейнере|container] такой фабричный метод: - -```php -public function createServiceDatabase(): PDO -{ - return new PDO('sqlite::memory:'); -} -``` - -Имена сервисов позволяют ссылаться на них в других частях конфигурационного файла в форме `@имяСервиса`. Если давать сервису имя не нужно, можно просто использовать маркер списка (`-`): - -```neon -services: - - PDO('sqlite::memory:') -``` - -Чтобы получить сервис из DI-контейнера, можно использовать метод `getService()` с именем сервиса в параметре или метод `getByType()` с типом сервиса: - -```php -$database = $container->getService('database'); -$database = $container->getByType(PDO::class); -``` - - -Создание сервиса -================ - -Обычно мы создаём сервис просто созданием экземпляра конкретного класса. Например: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Если нам нужно расширить конфигурацию дополнительными ключами, определение можно разбить на несколько строк: - -```neon -services: - database: - create: PDO('sqlite::memory:') - setup: ... -``` - -У ключа `create` есть псевдоним `factory`; оба варианта встречаются часто. Однако мы рекомендуем использовать `create`. - -Аргументы конструктора или фабричного метода можно указать и через ключ `arguments`: - -```neon -services: - database: - create: PDO - arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] -``` - -Сервисы не обязательно должны создаваться простым созданием экземпляра класса: они могут быть и результатом вызова статических методов или методов других сервисов: - -```neon -services: - database: DatabaseFactory::create() - router: @routerFactory::create() -``` - -Обратите внимание, что ради простоты вместо `->` используется `::`, см. [#Язык выражений]. Будут порождены такие фабричные методы: - -```php -public function createServiceDatabase(): PDO -{ - return DatabaseFactory::create(); -} - -public function createServiceRouter(): RouteList -{ - return $this->getService('routerFactory')->create(); -} -``` - -DI-контейнеру нужно знать тип создаваемого сервиса. Если мы создаём сервис методом, у которого не указан тип возвращаемого значения, мы должны явно объявить этот тип в конфигурации: - -```neon -services: - database: - create: DatabaseFactory::create() - type: PDO -``` - - -Аргументы -========= - -Аргументы в конструкторы и методы мы передаём очень похоже на то, как это делается в самом PHP: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Ради читаемости аргументы можно перечислить на отдельных строках. В этом случае запятые становятся необязательны: - -```neon -services: - database: PDO( - 'mysql:host=127.0.0.1;dbname=test' - root - secret - ) -``` - -Аргументы можно и именовать, тогда о их порядке заботиться не нужно: - -```neon -services: - database: PDO( - username: root - password: secret - dsn: 'mysql:host=127.0.0.1;dbname=test' - ) -``` - -Если вы хотите опустить какие-то аргументы и использовать их значения по умолчанию либо получить сервис через [autowiring|autowiring], используйте подчёркивание (`_`): - -```neon -services: - foo: Foo(_, %appDir%) -``` - -Аргументами могут быть сервисы, параметры и многое другое, см. [#Язык выражений]. - - -Setup -===== - -В секции `setup` мы задаём методы, которые нужно вызвать при создании сервиса. - -```neon -services: - database: - create: PDO(%dsn%, %user%, %password%) - setup: - - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) -``` - -В PHP это выглядело бы так: - -```php -public function createServiceDatabase(): PDO -{ - $service = new PDO('...', '...', '...'); - $service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - return $service; -} -``` - -Помимо вызова методов можно присваивать значения свойствам. Поддерживается и добавление элементов в массивы, для чего обращение к массиву нужно заключить в кавычки, чтобы не столкнуться с синтаксисом NEON: - -```neon -services: - foo: - create: Foo - setup: - - $value = 123 - - '$onClick[]' = [@bar, clickHandler] -``` - -В PHP-коде это выглядело бы так: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - $service->value = 123; - $service->onClick[] = [$this->getService('bar'), 'clickHandler']; - return $service; -} -``` - -Впрочем, в setup можно вызывать и статические методы или методы других сервисов. Если вам нужно передать аргументом сам текущий сервис, сошлитесь на него через `@self`: - -```neon -services: - foo: - create: Foo - setup: - - My\Helpers::initializeFoo(@self) - - @anotherService::setFoo(@self) -``` - -Обратите внимание, что ради простоты вместо `->` используется `::`, см. [#Язык выражений]. Будет порождён такой фабричный метод: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - My\Helpers::initializeFoo($service); - $this->getService('anotherService')->setFoo($service); - return $service; -} -``` - - -Язык выражений -============== - -Nette DI предлагает исключительно богатый язык выражений, с помощью которого можно определить почти что угодно. В конфигурационных файлах мы можем использовать [параметры |configuration#Параметры]: - -```neon -# параметр -%wwwDir% - -# значение параметра по ключу -%mailer.user% - -# параметр внутри строки -'%wwwDir%/images' -``` - -Кроме того, создавать объекты, вызывать методы и функции: - -```neon -# создание объекта -DateTime() - -# вызов статического метода -Collator::create(%locale%) - -# вызов функции PHP -::getenv(DB_USER) -``` - -Ссылаться на сервисы по имени или по типу: - -```neon -# сервис по имени -@database - -# сервис по типу -@Nette\Database\Connection -``` - -Использовать синтаксис first-class callable: .{data-version:3.2.0} - -```neon -# создание callback, равнозначно [@user, logout] -@user::logout(...) -``` - -Использовать константы: - -```neon -# константа класса -FilesystemIterator::SKIP_DOTS - -# получение глобальной константы функцией PHP constant() -::constant(\PHP_VERSION) -``` - -Обращаться к публичным свойствам и константам сервиса через `@service::member`. Является ли имя свойством или константой, решает его первая буква: строчная означает публичное свойство, заглавная - константу: - -```neon -# публичное свойство сервиса (начинается со строчной буквы) -@settings::apiUrl - -# константа класса сервиса (начинается с заглавной буквы) -@settings::Version -``` - -Вызовы методов можно объединять в цепочку, как в PHP. Ради простоты вместо `->` используется `::`: - -```neon -DateTime()::format('Y-m-d') -# PHP: (new DateTime())->format('Y-m-d') - -@http.request::getUrl()::getHost() -# PHP: $this->getService('http.request')->getUrl()->getHost() -``` - -Эти выражения можно использовать где угодно: при [создании сервисов |#Создание сервиса], в [аргументах |#Аргументы], в секции [setup |#Setup] или в [параметрах |configuration#Параметры]: - -```neon -parameters: - ipAddress: @http.request::getRemoteAddress() - -services: - database: - create: DatabaseFactory::create( @anotherService::getDsn() ) - setup: - - initialize( ::getenv('DB_USER') ) -``` - - -Специальные функции -------------------- - -В конфигурационных файлах можно использовать следующие специальные функции: - -- `not()` отрицает значение -- `bool()`, `int()`, `float()`, `string()` приведение к указанному типу без потерь .{data-version:3.0.5} -- `typed()` создаёт массив всех сервисов указанного типа -- `tagged()` создаёт массив всех сервисов с заданным тегом - -```neon -services: - - Foo( - id: int(::getenv('ProjectId')) - productionMode: not(%debugMode%) - ) -``` - -В отличие от обычного приведения PHP вроде `(int)`, приведение без потерь выбрасывает исключение для нечисловых значений. - -Функция `typed()` создаёт массив всех сервисов указанного типа (класса или интерфейса). Она пропускает сервисы, у которых отключён autowiring. Можно указать и несколько типов через запятую. - -```neon -services: - - BarsDependent( typed(Bar) ) -``` - -Массив сервисов определённого типа можно передать аргументом и автоматически через [autowiring |autowiring#Набор сервисов]. - -Функция `tagged()` создаёт массив всех сервисов с определённым тегом. Здесь тоже можно указать несколько тегов через запятую. - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - - -Autowiring -========== - -Ключ `autowired` позволяет повлиять на поведение autowiring для конкретного сервиса. Подробности см. в [главе об autowiring|autowiring]. - -```neon -services: - foo: - create: Foo - autowired: false # сервис foo исключён из autowiring -``` - - -Ленивые сервисы .{data-version:3.2.4} -===================================== - -Ленивая загрузка - приём, откладывающий создание сервиса до момента, когда он действительно понадобится. В глобальной конфигурации вы можете [включить ленивое создание |configuration#Ленивые сервисы] сразу для всех сервисов. Для отдельных сервисов это поведение можно переопределить: - -```neon -services: - foo: - create: Foo - lazy: false -``` - -Когда сервис определён как ленивый, при запросе из DI-контейнера мы получаем специальный объект-заместитель. Этот заместитель выглядит и ведёт себя точно как настоящий сервис, но настоящая инициализация (вызов конструктора и настроек) происходит только при первом обращении к какому-либо из его методов или свойств. - -Учтите, что раз сервис создаётся позже, то и ошибки в его конфигурации проявляются позже. Например, неверные учётные данные базы данных обнаружатся не при старте приложения, а только при первом запросе. - -Ленивое создание к тому же смягчает круговые зависимости, то есть ситуацию, когда сервис A требует сервис B, а B одновременно требует A. Без него контейнер сообщает об ошибке `Circular reference detected`. С ленивым заместителем сервис A получает лишь заместителя сервиса B, который инициализируется тогда, когда действительно используется, в момент, когда A уже существует. Тем не менее круговая зависимость сигнализирует об ошибочной архитектуре, и от неё лучше избавиться. - -.[note] -Ленивая загрузка требует PHP 8.4 или новее и работает только для сервисов, создаваемых прямым созданием экземпляра класса (например, `create: Foo`), но не для тех, что создаются фабричным методом. Её также нельзя использовать для классов, которые в конечном счёте наследуют от внутреннего класса PHP. Когда ленивую загрузку применить нельзя, флаг `lazy: true` молча игнорируется. - - -Теги -==== - -Теги служат для добавления к сервисам дополнительных сведений. Сервису можно назначить один или несколько тегов: - -```neon -services: - foo: - create: Foo - tags: - - cached -``` - -Теги могут нести и значения: - -```neon -services: - foo: - create: Foo - tags: - logger: monolog.logger.event -``` - -Чтобы получить все сервисы, связанные с определёнными тегами, можно использовать функцию `tagged()`: - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - -Внутри DI-контейнера имена всех сервисов с определённым тегом можно получить методом `findByTag()`: - -```php -$names = $container->findByTag('logger'); -// $names - массив, где ключи это имена сервисов, а значения это значения тега -// например, ['foo' => 'monolog.logger.event', ...] -``` - - -Режим inject -============ - -Флаг `inject: true` включает внедрение зависимостей через публичные свойства с атрибутом [Inject |best-practices:inject-method-attribute#Атрибуты Inject] и через методы [inject*() |best-practices:inject-method-attribute#Методы inject*()]. - -```neon -services: - articles: - create: App\Model\Articles - inject: true -``` - -По умолчанию режим `inject` включён только для презентеров. - - -Изменение сервисов -================== - -DI-контейнер содержит множество сервисов, добавленных встроенными или [пользовательскими расширениями|extensions]. Определения этих существующих сервисов можно изменить прямо в конфигурации. Например, вы можете поменять класс сервиса `application.application`, которым по умолчанию является `Nette\Application\Application`, на другой: - -```neon -services: - application.application: - create: MyApplication - alteration: true -``` - -Флаг `alteration` указывает, что мы лишь изменяем существующий сервис. Он же служит страховкой: если изменяемого сервиса не существует, компиляция завершится исключением. - -Мы можем и дополнить setup: - -```neon -services: - application.application: - create: MyApplication - alteration: true - setup: - - '$onStartup[]' = [@resource, init] -``` - -Определять сервис по его внутреннему имени необязательно, вместо этого можно сослаться на него по типу. Предыдущий пример можно записать и так: - -```neon -services: - @Nette\Application\Application: - create: MyApplication -``` - -Изменяя сервис, мы можем захотеть убрать исходные аргументы, элементы setup или теги с помощью ключа `reset`: - -```neon -services: - application.application: - create: MyApplication - alteration: true - reset: - arguments: true - setup: true - tags: true -``` - -Если вы хотите удалить сервис, добавленный расширением, это делается так: - -```neon -services: - cache.journal: false -``` diff --git a/dependency-injection/ru/upgrading.texy b/dependency-injection/ru/upgrading.texy deleted file mode 100644 index 7a5e6806e9..0000000000 --- a/dependency-injection/ru/upgrading.texy +++ /dev/null @@ -1,49 +0,0 @@ -Обновление -********** - - -Обновление до версии 3.1 -======================== - -- autowiring больше не передаёт `null` в nullable-параметр без значения по умолчанию; передайте аргумент явно или задайте параметру значение по умолчанию -- поддержка аннотации `@return` убрана; используйте тип возвращаемого значения или укажите тип в определении сервиса через `type:` -- ключ `dynamic` переименован в `imported`, а `class` в `type` -- обозначение пропущенного аргумента изменилось с `...` на `_`, например `MyService(_, 123)` -- в файлах NEON символ `@` в начале строки больше не нужно экранировать -- ключ `parameters` внутри определений генерируемых фабрик объявлен устаревшим -- метод `Nette\DI\Config\Loader::save()` объявлен устаревшим; экспортируйте конфигурацию через `Nette\DI\Config\Adapters\NeonAdapter::dump()` - -Версия 3.1 - переходная: она не приносит новых возможностей, но предупреждает уведомлениями обо всём, что позже будет работать иначе. См. статью [Nette DI 3.1: переходный выпуск |https://blog.nette.org/en/nette-di-3-1-transition-release]. - - -Обновление до версии 3.0 -======================== - -- поддержка файлов INI убрана -- прямая запись PHP-кода в конфигурацию через вопросительные знаки (например, `"$service->onError[] = ?"(...)`) убрана; используйте вместо этого запись массивом `'$onError[]' = [...]` -- в конфигурационных файлах используйте `factory: PDO(...)` вместо `class: PDO(...)` -- тег `nette.presenter` для презентеров больше не используется - - -Для авторов расширений компилятора ----------------------------------- - -В Nette 2.4 каждый сервис внутренне описывался как `Nette\DI\ServiceDefinition`, а теперь есть несколько типов определений: `Nette\DI\Definitions\ImportedDefinition` для импортированных (динамических) сервисов, `Nette\DI\Definitions\FactoryDefinition` для генерируемых фабрик на основе интерфейсов, `Nette\DI\Definitions\AccessorDefinition` для генерируемых аксессоров и `Nette\DI\Definitions\ServiceDefinition` для обычных сервисов. - -Поэтому, помимо `ContainerBuilder::addDefinition()`, для создания нового определения есть несколько других методов: `addFactoryDefinition()`, `addAccessorDefinition()` и `addImportedDefinition()`. - - -Обновление до версии 2.4 -======================== - -- секции конфигурации (например, production, development) в одном конфигурационном файле объявлены устаревшими; используйте пару файлов `config.neon` и `config.local.neon` -- наследование определений сервисов объявлено устаревшим -- `Statement::setEntity()` объявлен устаревшим - - -Обновление до версии 2.3 -======================== - -- поддержка размещения сервисов внутри секции расширения в конфигурационном файле убрана -- поддержка динамически добавляемых расширений убрана -- при динамической замене сервиса (через `removeService()`, `addService()`) новый сервис должен быть экземпляром того же интерфейса или класса, что и исходный diff --git a/dependency-injection/tr/@home.texy b/dependency-injection/tr/@home.texy deleted file mode 100644 index 7767318013..0000000000 --- a/dependency-injection/tr/@home.texy +++ /dev/null @@ -1,22 +0,0 @@ -Nette DI -******** - -.[perex] -Bağımlılık enjeksiyonu (Dependency Injection), koda ve geliştirmeye bakışınızı kökten değiştirecek bir tasarım desenidir. Temiz tasarlanmış ve sürdürülebilir uygulamalar dünyasının kapısını aralar. - -- [Bağımlılık enjeksiyonu nedir? |introduction] -- [Genel durum ve singleton'lar |global-state] -- [Bağımlılıkların aktarılması |passing-dependencies] -- [DI container nedir? |container] -- [Sıkça sorulan sorular |faq] - - -`nette/di` paketi, PHP için son derece gelişmiş, derlenen bir DI container sunar. - -- [Nette DI Container |nette-container] -- [Yapılandırma |configuration] -- [Servis tanımları |services] -- [Autowiring |autowiring] -- [Üretilen factory'ler |factory] -- [Nette DI için extension yazma |extensions] -- [Container derlemesi nasıl işler |compilation-internals] diff --git a/dependency-injection/tr/@left-menu.texy b/dependency-injection/tr/@left-menu.texy deleted file mode 100644 index 4c5192e6e2..0000000000 --- a/dependency-injection/tr/@left-menu.texy +++ /dev/null @@ -1,28 +0,0 @@ -Bağımlılık Enjeksiyonu -********************** -- [DI nedir? |introduction] -- [Genel durum ve singleton'lar |global-state] -- [Bağımlılıkların aktarılması |passing-dependencies] -- [DI container nedir? |container] -- [Sıkça sorulan sorular |faq] - - -Nette DI --------- -- [Nette DI Container |nette-container] -- [Yapılandırma |configuration] -- [Servis tanımları |services] -- [Autowiring |autowiring] -- [Üretilen factory'ler |factory] -- [Nette DI için extension yazma |extensions] -- [Derleme nasıl işler |compilation-internals] -- [Yükseltme|upgrading] - - -Daha Fazla Okuma -**************** -- [Nette dokümantasyonu |nette:] -- [Nette Application |application:how-it-works] -- [Yardımcı araçlar |utils:] -- [En iyi uygulamalar |best-practices:] -- [Sorun giderme |nette:troubleshooting] diff --git a/dependency-injection/tr/@meta.texy b/dependency-injection/tr/@meta.texy deleted file mode 100644 index 8dfe82f311..0000000000 --- a/dependency-injection/tr/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokümantasyonu}} diff --git a/dependency-injection/tr/autowiring.texy b/dependency-injection/tr/autowiring.texy deleted file mode 100644 index 5409f3bd11..0000000000 --- a/dependency-injection/tr/autowiring.texy +++ /dev/null @@ -1,279 +0,0 @@ -Autowiring -********** - -.[perex] -Autowiring, gereken servisleri yapıcıya ve diğer metotlara otomatik olarak aktaran harika bir özelliktir; böylece onları açıkça belirtmemiz gerekmez. Size bolca zaman kazandırır. - -Bu sayede servis tanımlarını yazarken argümanların büyük çoğunluğunu atlayabiliriz. Şunun yerine: - -```neon -services: - articles: Model\ArticleRepository(@database, @cache.storage) -``` - -Yalnızca şunu yazın: - -```neon -services: - articles: Model\ArticleRepository -``` - -Autowiring türlerle yönlendirilir; bu yüzden çalışması için `ArticleRepository` sınıfının kabaca şöyle tanımlanması gerekir: - -```php -namespace Model; - -class ArticleRepository -{ - public function __construct(\PDO $db, \Nette\Caching\Storage $storage) - {} -} -``` - -Autowiring asla servis adlarını kullanmaz. Yalnızca PHP'nin tür sistemiyle yönlendirilir; dolayısıyla bir sınıfın, uyguladığı arayüzleri ve türediği sınıfları da karşıladığını bilir. Bu sayede servisin adı yalnızca yardımcı bir tanımlayıcıdır ve onu yeniden adlandırmak uygulamada hiçbir şeyi bozmaz. - -Autowiring'i kullanabilmek için container'da her türden **tam olarak bir servis** bulunmalıdır. Daha fazlası olsaydı autowiring hangisini aktaracağını bilemez ve istisna fırlatırdı: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - tempDb: PDO('sqlite::memory:') - articles: Model\ArticleRepository # İSTİSNA FIRLATIR, hem mainDb hem tempDb uyuyor -``` - -Bir çözüm, autowiring'i atlayıp servis adını açıkça belirtmektir (örneğin `articles: Model\ArticleRepository(@mainDb)`). Ancak daha elverişli bir yaklaşım, ya servislerden biri için autowiring'i [kapatmak |#Autowiring'i Kapatma] ya da bir servisi diğerlerine [yeğlemektir |#Autowiring Yeğlemesi]. - - -Autowiring'i Kapatma --------------------- - -Bir servis için autowiring'i `autowired: false` seçeneğiyle kapatabiliriz: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - - tempDb: - create: PDO('sqlite::memory:') - autowired: false # tempDb servisi autowiring'den çıkarılır - - articles: Model\ArticleRepository # bu yüzden yapıcıya mainDb aktarılır -``` - -`articles` servisi, yapıcı için uyan iki `PDO` servisinin (`mainDb` ve `tempDb`) bulunduğuna dair istisna fırlatmaz; çünkü yalnızca `mainDb` servisini dikkate alır. - -Autowiring, [`di › excluded` |configuration#DI] yapılandırma seçeneğiyle tüm türler için genel olarak da kapatılabilir; bu seçenek, hiçbir zaman autowiring'e girmemesi gereken türleri (ve torunlarını) listeler. - -.[note] -Nette'deki autowiring yapılandırması Symfony'dekinden farklıdır. Symfony'de `autowire: false`, autowiring'in servisin yapıcı argümanları için kullanılmaması gerektiği anlamına gelir. Nette'de autowiring, yapıcı argümanları ile container aracılığıyla çağrılan diğer metotlar (setter injection gibi) için geçerlidir. `autowired: false` seçeneği, container'ın bu servis örneğini başka servislere bağımlılık olarak otomatik aktarmasını engeller. - - -Autowiring Yeğlemesi --------------------- - -Aynı türde birden çok servisimiz varsa ve bunlardan biri için `autowired` seçeneğini belirtirsek, o servis yeğlenen servis olur: - -```neon -services: - mainDb: - create: PDO(%dsn%, %user%, %password%) - autowired: PDO # yeğlenen olur - - tempDb: - create: PDO('sqlite::memory:') - - articles: Model\ArticleRepository -``` - -`articles` servisi, birden çok `PDO` servisinin (`mainDb` ve `tempDb`) uyduğuna dair istisna fırlatmaz; yeğlenen servisi, yani `mainDb` servisini kullanır. - - -Servis Koleksiyonu ------------------- - -Autowiring, belirli bir türdeki servislerin dizilerini de aktarabilir. PHP tür bildirimlerinde dizi öğelerinin türünü belirtmeyi doğrudan desteklemediğinden, `array` tür bildirimini öğe türünü belirten `ClassName[]` gibi bir phpDoc yorumuyla tamamlamalısınız: - -```php -namespace Model; - -class ShipManager -{ - /** - * @param Shipper[] $shippers - */ - public function __construct(array $shippers) - {} -} -``` - -DI container o zaman verilen türe karşılık gelen servislerden oluşan bir diziyi otomatik olarak aktarır. [Autowiring'i kapatılmış |#Autowiring'i Kapatma] servisleri dışarıda bırakır ve o an oluşturulmakta olan servisi asla kendi koleksiyonuna katmaz. Tek bir servis aktarmanın aksine, autowiring'i belirli bir türe [daraltmanın |#Autowiring'i Daraltma] ya da bir servisi [yeğlenen |#Autowiring Yeğlemesi] işaretlemenin burada etkisi yoktur; dizi her zaman verilen türdeki tüm servisleri içerir. - -Yorumdaki tür `array<int, Class>` ya da `list<Class>` biçiminde de olabilir. phpDoc yorumunun biçimini denetleyemiyorsanız, servis dizisini yapılandırmada doğrudan [`typed()` |services#Özel Fonksiyonlar] ile aktarabilirsiniz. - - -Skaler Argümanlar ------------------ - -Autowiring yalnızca nesnelerde ve nesne dizilerinde çalışır. Skaler argümanlar (örneğin dizeler, sayılar, boolean değerler) [yapılandırmada belirtilmelidir |services#Argümanlar]. Bir alternatif, skaler değeri (ya da birden çok değeri) içine alan bir [ayar nesnesi|best-practices:passing-settings-to-presenters] oluşturmaktır. Bu nesne sonra autowiring ile aktarılabilir. - -```php -class MySettings -{ - public function __construct( - // readonly, PHP 8.1'den beri kullanılabilir - public readonly bool $value, - ) - {} -} -``` - -Onu yapılandırmaya ekleyerek servis olarak kaydedersiniz: - -```neon -services: - - MySettings('any value') -``` - -Diğer sınıflar sonra onu autowiring ile isteyebilir. - - -İsteğe Bağlı Bağımlılıklar --------------------------- - -Bir yapıcının ya da metodun parametresinin varsayılan değeri varsa ve container'da gereken türde bir servis yoksa, autowiring istisna fırlatmaz; argümanı yalnızca atlar, böylece varsayılan değer kullanılır. İsteğe bağlı bağımlılıkları böyle bildirirsiniz: - -```php -class Foo -{ - public function __construct( - private ?Logger $logger = null, - ) {} -} -``` - -Buna karşılık varsayılan değeri olmayan bir parametrede, eksik servis her zaman istisnaya yol açar. - - -Autowiring'i Daraltma ---------------------- - -Tek tek servislerde autowiring, belirli sınıflara ya da arayüzlere daraltılabilir. - -Autowiring normalde bir servisi, türü servisle uyuşan her metot parametresine aktarır. Daraltma, servisin aktarılabilmesi için metot parametrelerinde belirtilen türlerin karşılaması gereken koşulları koymak demektir. - -Bir örnek alalım: - -```php -class ParentClass -{} - -class ChildClass extends ParentClass -{} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Hepsini servis olarak kaydetseydik autowiring başarısız olurdu: - -```neon -services: - parent: ParentClass - child: ChildClass - parentDep: ParentDependent # İSTİSNA FIRLATIR, hem parent hem child uyuyor - childDep: ChildDependent # autowiring yapıcıya child servisini aktarır -``` - -`parentDep` servisi `Multiple services of type ParentClass found: child, parent` istisnasını fırlatır; çünkü hem `parent` hem de `child` servisleri onun yapıcısına uyar ve autowiring hangisini seçeceğine karar veremez. - -Bu yüzden `child` servisi için autowiring'i `ChildClass` türüne daraltabiliriz: - -```neon -services: - parent: ParentClass - child: - create: ChildClass - autowired: ChildClass # 'autowired: self' olarak da yazılabilir - - parentDep: ParentDependent # autowiring yapıcıya parent servisini aktarır - childDep: ChildDependent # autowiring yapıcıya child servisini aktarır -``` - -Artık `parentDep` servisinin yapıcısına `parent` servisi aktarılır; çünkü uyan tek nesne odur. `child` servisi artık autowiring ile oraya aktarılmaz. Evet, `child` servisi hâlâ `ParentClass` türündedir, ama `autowired: ChildClass` daraltma koşulu, yalnızca açıkça `ChildClass` (ya da alt türleri) olarak türlenen parametrelere aktarılacağı anlamına gelir. `ParentDependent` sınıfı `ParentClass` istediğinden, `child` servisi orada artık autowiring adayı sayılmaz. - -`child` servisi için `autowired: ChildClass` yerine `autowired: self` de yazılabilirdi; çünkü `self`, geçerli servisin sınıfı için bir yer tutucudur. - -`autowired` anahtarında dizi olarak birden çok sınıf ya da arayüz belirtmek de olanaklıdır: - -```neon -autowired: [ParentClass, FooInterface] -``` - -Örneğe arayüzler eklemeyi deneyelim: - -```php -interface FooInterface -{} - -interface BarInterface -{} - -class ParentClass implements FooInterface -{} - -class ChildClass extends ParentClass implements BarInterface -{} - -class FooDependent -{ - function __construct(FooInterface $obj) - {} -} - -class BarDependent -{ - function __construct(BarInterface $obj) - {} -} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -`child` servisini hiçbir şekilde kısıtlamazsak, `FooDependent`, `BarDependent`, `ParentDependent` ve `ChildDependent` sınıflarının tüm yapıcılarına uyar ve autowiring onu oralara aktarır. - -Ancak autowiring'ini `autowired: ChildClass` (ya da `self`) ile `ChildClass` türüne daraltırsak, autowiring onu yalnızca `ChildDependent` yapıcısına aktarır; çünkü o `ChildClass` türünde bir argüman ister ve `ChildClass`, `ChildClass` *türündendir*. Diğer parametrelerin istediği türlerin hiçbiri `ChildClass` ya da onun alt türü değildir, dolayısıyla servis onlara aktarılmaz. - -Onu `autowired: ParentClass` ile `ParentClass` türüne kısıtlarsak, autowiring onu yine `ChildDependent` yapıcısına (çünkü istenen `ChildClass`, `ParentClass` türünün alt türüdür) ve artık `ParentDependent` yapıcısına da aktarır; çünkü istenen `ParentClass` türü de uygundur. - -Onu `FooInterface` türüne kısıtlarsak, `ParentDependent` (istenen `ParentClass`, `FooInterface` türünün alt türüdür) ve `ChildDependent` sınıflarına yine autowiring ile aktarılır; buna ek olarak `FooDependent` yapıcısına da aktarılır, ama `BarDependent` sınıfına aktarılmaz; çünkü `BarInterface`, `FooInterface` türünün alt türü değildir. - -```neon -services: - child: - create: ChildClass - autowired: FooInterface - - fooDep: FooDependent # autowiring yapıcıya child servisini aktarır - barDep: BarDependent # İSTİSNA FIRLATIR, uyan servis yok - parentDep: ParentDependent # autowiring yapıcıya child servisini aktarır - childDep: ChildDependent # autowiring yapıcıya child servisini aktarır -``` diff --git a/dependency-injection/tr/compilation-internals.texy b/dependency-injection/tr/compilation-internals.texy deleted file mode 100644 index cc71e33592..0000000000 --- a/dependency-injection/tr/compilation-internals.texy +++ /dev/null @@ -1,222 +0,0 @@ -Container Derlemesi Nasıl İşler -******************************* - -.[perex] -Bu sayfa container derlemesini açıyor: hangi aşamalardan geçtiğini, yapılandırma parametrelerinin ne zaman genişletildiğini, `@service` dizelerinin ne zaman gerçek referanslara dönüştüğünü ve extension yazarlarının en çok sorduğu soruyu: hangi aşamada servisleri türe göre güvenle arayabileceğinizi. [Extension yazma |extensions] bölümünün derinlemesine tamamlayıcısıdır. - -Sıradan bir uygulama, hatta sıradan bir extension yazmak için bunların hiçbirine ihtiyacınız yok. Ama extension'ınız servis grafiğini incelemeye ya da yeniden biçimlendirmeye başladığında zamanlama her şey olur: aynı `getByType()` çağrısı bir aşamada güvenilir, başka bir aşamada yanıltıcı bir yanıt verir. Bu sayfa nedenini açıklıyor; böylece kodunuzun nereye ait olduğunu her zaman bilirsiniz. - - -İki Dünya: Derleme ve Çalışma Zamanı -==================================== - -Anlaşılması en önemli şey, bir Nette container'ının **her istekte kurulmadığıdır**. Bir kez, iyileştirilmiş bir PHP sınıfı olarak kurulur, bu sınıf diske yazılır ve sonraki her istek yalnızca bitmiş dosyayı `include` eder. Aşağıda anlatılan tüm düzenek (extension'lar, çözücüler, kod üreteci) **yalnızca (yeniden) derleme sırasında** çalışır. - -Bu, dünyayı asla bir arada bulunmayan iki temsile ayırır: - -| | derleme sırasında | çalışma zamanında -|---|---|--- -| Var olan | `ContainerBuilder` içindeki **tanımlar** (tarifler) | `Container` içindeki servis **örnekleri** -| Temel sınıflar | `Compiler`, `ContainerBuilder`, `Resolver`, `PhpGenerator` | `Container` (üretilen sınıfın atası) -| `%param%`, `@service` | hâlâ çevrilen metinsel işaretler | çevrilmiş / koda gömülmüş - -Üretilen sınıf `Nette\DI\Container` sınıfını genişletir ve her servis için bir `createServiceXxx()` metodu içerir. Parametreleri ve autowiring meta verileri önceden hesaplanır; böylece çalışma zamanında çözülecek bir şey kalmaz, yalnızca servisler istendikçe örneklenir. - -.[note] -Geliştirici kipinde container, bir yapılandırma dosyası ya da extension sınıfı değiştiğinde otomatik olarak yeniden kurulur; her ikisi de bağımlılık olarak izlenir. Üretimde bir kez derlenir ve bir daha denetlenmez; hız da buradan gelir. - - -Aşamalara Kuşbakışı -=================== - -Derlemeyi `Compiler::compile()` yönetir ve üç adıma iner: - -```php -public function compile(): string -{ - $this->processExtensions(); // AŞAMA A: şemalar + loadConfiguration() - $this->processBeforeCompile(); // AŞAMA B: resolve + beforeCompile() + complete - return $this->generateCode(); // AŞAMA C: kod üretimi + afterCompile() -} -``` - -Tüm zihinsel model tek bir düşünceye sığar: **her aşama bir öncekinden daha çok şey bilir.** - -- **Aşama A** grafiği tanımlarla doldurur. Servis **türleri henüz güvenilir biçimde bilinmez**; çünkü bir tür, henüz kimsenin bakmadığı bir factory'nin dönüş değerinden gelebilir. -- **Aşama B** önce tüm türleri çözer (`resolve`), sonra extension'ların grafiği yeniden biçimlendirmesine izin verir (`beforeCompile`) ve en sonunda argümanları [autowiring |autowiring] ile bağlar (`complete`). -- **Aşama C** bitmiş grafiği PHP'ye çevirir ve extension'ların üretilen koda dokunmasına izin verir. - -Bilginin böyle büyümesi, aynı işlemin neden bir aşamada güvenli, başka bir aşamada güvenilmez olduğunun tam nedenidir. Sayfanın geri kalanı aşamaları bu düşünceyle geziyor. - - -Aşama A: Tanımların Kaydedilmesi -================================ - -Bu aşamada Nette her extension'da üç metot çağırır (`getConfigSchema()`, sonra `setConfig()`, sonra `loadConfiguration()`), ama **özenle denetlenen bir sırayla**; çünkü burada sıra gerçekten önemlidir. - - -Sıra Neden Önemli ------------------ - -- **`ParametersExtension` ve `ExtensionsExtension` önce gelir.** Birincisi, `%param%` ifadelerini yapılandırmanın tamamında genişletebilmek için her şeyden önce çalışmalıdır; diğer her extension kendi bölümünü değerleri doldurulmuş olarak alır. İkincisi, `extensions:` bölümünde listelenen başka extension'ları kaydeder; dolayısıyla o da geri kalanlar işlenmeden önce var olmalıdır. -- **`ServicesExtension` en sona kalır.** Böylece kullanıcının `services:` bölümü her zaman son sözü söyler ve extension'ların kurduğu her şeyi geçersiz kılabilir. -- **`InjectExtension` en sona taşınır**; böylece işi, diğer tüm extension'ların eklediği setup'ları görür. - -Sizin için çıkarım şu: extension'ınızın `loadConfiguration()` metodu çalıştığında parametreler zaten genişletilmiştir, ama kullanıcının servisleri henüz ortada yoktur. Aşağıdaki zamanlama kurallarının çoğunu bu tek gerçek belirler. - - -services: Bölümünün Tanımlara Dönüşmesi ---------------------------------------- - -Kullanıcının `services:` bölümü, aşama A'nın son adımında burada [tanım nesnelerine |extensions#Tanım Türleri] dönüştürülür. Her NEON girdisi normalleştirilir (kısa yazımlar tek biçime getirilir), türü saptanır (sıradan servis, factory, accessor, ...) ve builder'da uygun bir tanım oluşturulur. Basit `@name` / `@Type` argümanlarının referansa dönüştüğü ilk an da budur; bkz. [aşağısı |#Referanslar: @service Ne Zaman Referans Olur]. - -Aşama A'nın sonunda tüm tanımlar yerindedir; her extension ve kullanıcı istediğini kaydetmiştir. Ama resim henüz net değildir: - -- türü bir factory'nin dönüş değerinden gelen tanımlarda **türler çözülmemiştir**, -- **argümanlar autowiring ile bağlanmamıştır**, -- bazı `@service` referansları hâlâ düz dizedir. - -Türe göre aramanın burada neden güvenilmez olduğunun nedeni tam da budur; ayrıntısı [aşağıda |#ContainerBuilder'ı İnceleme: Ne Zaman Güvenli]. - - -Parametreler: %param% Ne Zaman Genişletilir -=========================================== - -İki başlıca sorudan biri. Yanıt kısa: **bir kez, aşama A'nın en başında, yapılandırma ağacının tamamında.** - -`ParametersExtension` önce çalışır ve ilk yaptığı şeylerden biri `%param%` yer tutucularını genişletmektir; önce parametrelerin kendi içinde (bir parametre bir başkasına başvurabilir), sonra yapılandırmanın geri kalanında. Yani `ServicesExtension` dahil başka herhangi bir extension kendi bölümünü aldığında yer tutucular çoktan gitmiştir. Extension'lar somut değerlerle çalışır, asla `%...%` ile değil. - -Bir yer tutucu dizenin tamamıysa, değeri diziler ve nesneler dahil *olduğu gibi* döndürülür; dolayısıyla `%mailer%` bütün bir diziye genişleyebilir. Başka her yerde bir dizeye birleştirilir ve noktalı yazım `%foo.bar%` iç içe dizilere uzanır. - - -Statik ve Dinamik Parametreler ------------------------------- - -Her değer koda gömülemez. Değeri ortama göre değişen bir parametre (bir ortam değişkeni, istekten türetilen `baseUrl`) **dinamik** kalmalıdır. Böyle parametreleri `setDynamicParameterNames()` ile ya da bir şemada `Expect::...->dynamic()` ile bildirirsiniz; ayrıntısı [Dinamik parametreler |application:bootstrapping#Dinamik parametreler] bölümünde. - -Dinamik bir parametrenin yerine bir değer değil, onu *çalışma zamanında* okuyan bir ifade konur. Yani `%env.DB_HOST%` bir dizeye donmaz; üretilen container'da çalışma zamanı aramasına dönüşür. Geri kalan her şey statiktir ve derleme zamanında dondurulur; "`getenv()` değerim her ortamda aynı" şaşkınlığının olağan kaynağı da budur: parametre yalnızca statikti. - -Bunun tersi işlem **kaçışlamadır**: harfi harfine bir `%` ya da `@` karakterinin yorumlanmasını önlemek için iki katına çıkarılır (`%%`, `@@`). Nette bunu sizin için enjekte ettiği parametrelerde otomatik yapar; böylece değerleri asla yer tutucu ya da referans sanılmaz. - - -Referanslar: @service Ne Zaman Referans Olur -============================================ - -İkinci başlıca soru. `@service` çevirisi, dizenin ne kadar karmaşık olduğuna göre **farklı aşamalara yayılan birkaç adımda** gerçekleşir. Bunu elle izlemeniz nadiren gerekir, ama adımları bilmek bazı referansların neden diğerlerinden önce çözüldüğünü açıklar. - -- **Ayrıştırma (yapılandırmanın yüklenmesi).** *Varlık olarak* kullanılan bir `@service` (servisi oluşturan şey, `Foo(@bar)` örneğindeki gibi) hemen referansa dönüşür. *Argüman olarak* kullanılan bir `@service` şimdilik düz dize kalır. Tırnak içindeki bir `@`, `@@` olarak kaçışlanır; böylece referans değil, harfi harfine metin sayılır. -- **Aşama A (`loadConfiguration`).** Tanımlar işlenirken, temiz bir `@name` ya da `@Type` argümanı bir `Reference` nesnesine dönüştürülür. Bu yalnızca basit biçimleri yakalar; `@service::CONST` ya da daha büyük bir ifadenin içindeki bir `@` sonraya bırakılır. -- **Aşama B (`complete`).** Asıl "akıllı" çeviri burada olur: `@service` → referans, `@service::CONSTANT` → harfi harfine bir sınıf sabiti, `@service::property` → o özelliğin okunması, `@@x` → harfi harfine `@x` metni. - -*Referans* sözcüğünün kendisinde gizli ikinci bir çeviri daha var. Bir `Reference` ya **ada** ya da **türe** (`@Namespace\Type`) işaret edebilir. Bir tür referansı **henüz bir servis adı değildir**; somut bir ada autowiring tarafından çözülür ve bu yalnızca **complete** adımında, autowiring dizini kurulduktan sonra gerçekleşir. Bu, bir sonraki bölüme köprüdür: autowiring aramaları, dizin hazır olana dek bilinçli olarak ertelenir. - -| Biçim | Şurada referansa/ifadeye dönüşür | Şurada somut servise çözülür -|---|---|--- -| varlık (factory olarak `@foo`) | ayrıştırma | complete -| argüman `@foo`, `@Type` | aşama A | complete -| `@foo::CONST`, `@foo::prop` | aşama B | complete -| tür referansı `@Type` | aşama A/B | complete (autowiring) - - -ContainerBuilder'ı İnceleme: Ne Zaman Güvenli -============================================= - -Şimdi extension yazarlarının en çok sorduğu soru: **hangi metotta servisleri türe göre arayabilirim?** Yanıt, builder'ın kendi durumunu nasıl izlediğine dair basit bir kuraldan çıkar. - -**Türe göre** arama (`getByType()`, `getDefinitionByType()`, `findByType()`), servis grafiğinin *çözülmüş* olmasını gerektirir: her tür bilinmeli, autowiring dizini kurulmuş olmalı. Bu yüzden bunlardan birini çağırdığınızda ve grafik son çözümden bu yana değiştiyse, builder **bilinen grafiğin tamamını anında çözer**. Çözümün kendisi sırasında türe göre her arama yasaktır ve `NotAllowedDuringResolvingException` fırlatır. - -**Etikete göre** arama (`findByTag()`) böyle bir gereksinim taşımaz; etiketler türlere bağlı değildir, dolayısıyla **her aşamada** çalışır. - -Aşama aşama: - -- **`loadConfiguration()` (aşama A) - türe göre arama güvenilmez.** Grafik eksiktir: sonra çalışan extension'lar servislerini henüz kaydetmemiştir ve her şeyden önce kullanıcının `services:` bölümü (en sonda çalışır) ortada yoktur. Bir `getByType()` çağrısı işe yarar (kısmi grafiğin erken çözülmesini tetikler), ama yanıt eksik bir resimden gelir ve erken çözüm boşuna emek harcar. Kural: **`loadConfiguration()` içinde yalnızca tanım kaydedin; türe göre aramayın.** `findByTag()` sorun değil. -- **`beforeCompile()` (aşama B) - inceleme için doğru yer.** Bu noktada **tüm** tanımlar vardır (kullanıcınınkiler dahil), **türler çözülmüştür** ve **autowiring dizini kurulmuştur**; dolayısıyla `getByType()`, `findByType()` ve `findByTag()` **güvenilir** yanıtlar verir. Argümanlar *henüz* autowiring ile bağlanmamıştır; bu, tüm `beforeCompile()` çağrılarından sonraki adımdır (`complete`). Burada bir tanımı değiştirdiğinizde, sonraki `getByType()` grafiği saydam biçimde yeniden çözer; böylece düzenlemelerle sorguları rahatça değiştirebilirsiniz. -- **`afterCompile()` (aşama C) - yalnızca kod.** Builder üzerinde değil, üretilen sınıf üzerinde çalışır. Grafik bitmiştir; burada sonuç PHP kodunu biçimlendirirsiniz. - -| İstediğim... | Aşama -|---|--- -| bir servis kaydetmek | `loadConfiguration()` -| **etikete** göre arayıp tanımları değiştirmek | `loadConfiguration()` ya da `beforeCompile()` -| **türe** göre aramak (`getByType`/`findByType`) | **`beforeCompile()`** -| autowiring'in argümanlar için hangi servisleri seçtiğine dayanmak | derleme zamanında değil; çalışma zamanında inceleyin -| üretilen koda dokunmak | `afterCompile()` -| container başladıktan sonra kod çalıştırmak | [başlatma kodu |extensions#Başlatma Kodu] - - -Aşama B'nin İçi: resolve ve complete -==================================== - -Aşama B, aralarına `beforeCompile()` çağrıları sıkışmış iki geçiştir: - -```php -$this->builder->resolve(); // türler çözülür, autowiring dizini kurulur -foreach ($this->extensions as $extension) { - $extension->beforeCompile(); -} -$this->builder->complete(); // argümanlar ANCAK ŞİMDİ autowiring ile bağlanır -``` - -**`resolve()`**, her servisin türünü belirler (bildirilmiş `type` değerinden alır ya da factory'sinden çıkarır: bir factory metodunun dönüş türü, örneklediği sınıf ya da bir referansın işaret ettiği servis) ve ardından her türü (sınıfı, atalarını ve arayüzlerini) bir servis adına eşleyen autowiring dizinini kurar. `autowired: false` işaretli bir servis dizine alınmaz; `autowired: [A, B]` ise onun görünür olduğu türleri daraltır. Can alıcı nokta: resolve *türleri* belirler, *argümanları* değil; argümanların autowiring'i bitmiş bir dizin gerektirir ve bu dizin ancak bu geçişten sonra vardır. - -**`complete()`**, argümanların autowiring'inin asıl gerçekleştiği yerdir. Her tanım için eksik yapıcı ve setup argümanlarını, türlerini artık tamamlanmış dizinde arayarak doldurur. Tür referanslarının resolve sırasında çözülmeden bırakılmasının nedeni budur: arama buraya aittir, bakılacak güvenilir bir dizin oluştuktan sonra. - - -Aşama C: Kodun Üretilmesi -========================= - -`generateCode()`, bitmiş grafiği `PhpGenerator` sınıfına verir; o da `Container` sınıfını genişleten, her servis için bir `createServiceXxx()` metodu içeren ve önceden hesaplanmış `aliases`, `tags` ile `wiring` meta verilerini taşıyan bir sınıf üretir. Her `Statement` PHP metnine (`new Foo(...)`, metot çağrıları, özellik erişimi), her `Reference` ise bir `$this->getService(...)` çağrısına dönüşür. - -Extension'lar sonra üretilen sınıf üzerinde son bir `afterCompile()` geçişi elde eder; örneğin statik ve dinamik parametre getter'ları burada üretilir. Ayrıca her istekte çalışan [başlatma kodu |extensions#Başlatma Kodu] ekleme fırsatı da burada doğar. - - -Zaman Çizelgesi Tek Bir Resimde -=============================== - -``` -DERLEME (bir kez, önbelleğe) -│ -├─ yapılandırma dosyalarını yükle NEON -> Statement/dizi; dosyaları birleştir -│ tırnaklı @ -> @@ ; varlıklar -> Statement -│ -▼ Compiler::compile() -│ -├─ AŞAMA A processExtensions() -│ ├─ ParametersExtension (İLK) ─── %param% tüm yapılandırmada GENİŞLETİLİR -│ │ dinamik olanlar -> çalışma zamanı ifadesi -│ ├─ ExtensionsExtension (İLK) ──── başka extension'ları kaydeder -│ ├─ ...diğer extension'lar... ── loadConfiguration(): yalnızca tanım kaydet -│ └─ ServicesExtension (SON) ── services: -> Definition nesneleri -│ @name/@Type -> Reference -│ [grafik sayıca tam; TÜRLER ve ARGÜMANLAR değil; türe göre arama güvenilmez] -│ -├─ AŞAMA B processBeforeCompile() -│ ├─ builder.resolve() ── tüm türleri çöz; autowiring dizinini kur -│ │ [türler hazır; dizin hazır] -│ ├─ beforeCompile() extension ── getByType/findByType/findByTag BURADA GÜVENLİ -│ │ (argümanlar henüz bağlanmadı) -│ └─ builder.complete() ── ARGÜMANLARI bağla; referans çevirisini bitir -│ tür referansları -> servis adları -│ -└─ AŞAMA C generateCode() - ├─ PhpGenerator.generate() ── Statement -> PHP; createServiceXxx() metotları - ├─ afterCompile() extension ── kodu ayarla; parametre getter'larını üret - └─ toString() ── son PHP kodu -> önbellek - -──────────────────────────────────────────────────────────── - -ÇALIŞMA ZAMANI (her istek) -│ -├─ new Container($dynamicParams) -├─ initialize() ── extension'ların açılış kodu (oturum, header, doğrulama) -└─ getService()/getByType() ── önceden hesaplanan meta veriden tembel örnekler -``` - - -Yaygın Yanlış Anlamalar -======================= - -- "`loadConfiguration()` içinde servisleri türe göre ararım." Hayır; grafik eksiktir (kullanıcının `services:` bölümü sizden sonra çalışır) ve `getByType()` kısmi bir grafiğin erken çözülmesini tetikler. Bunu `beforeCompile()` metoduna taşıyın. `findByTag()` burada bile sorun değil. -- "Bir parametredeki `getenv()` değeri her ortamda farklı olur." Yalnızca parametre dinamikse. Aksi hâlde derleme zamanında koda gömülür ve her yerde aynı kalır. -- "`@Type` referansı zaten bir servis adıdır." Değildir; bir tür referansıdır ve somut bir ada ancak complete adımında autowiring tarafından çözülür. -- "Extension'ım bir yardımcı dosyayı okuyor, ama değişiklikler görünmüyor." Onu `$builder->addDependency($file)` ile kaydedin; aksi hâlde önbellek ondan habersiz kalır ve yeniden kurmaz. -- "`resolve()` sırasında `getByType()` çağırabilirim." Hayır; `NotAllowedDuringResolvingException` fırlatır. Türe göre arama `beforeCompile()` metoduna ya da sonrasına aittir, asla çözümün ortasına değil. diff --git a/dependency-injection/tr/configuration.texy b/dependency-injection/tr/configuration.texy deleted file mode 100644 index 9970d0a514..0000000000 --- a/dependency-injection/tr/configuration.texy +++ /dev/null @@ -1,335 +0,0 @@ -DI Container Yapılandırması -*************************** - -.[perex] -Nette DI container'ının yapılandırma seçeneklerine genel bakış. - - -Yapılandırma Dosyası -==================== - -Nette DI container, yapılandırma dosyalarıyla kolayca denetlenir. Bunlar genellikle [NEON biçiminde|neon:format] yazılır. Bu biçim için [destek sunan düzenleyicileri |tools:ide] kullanmanızı öneririz. - -<pre> -"decorator .[prism-token prism-atrule]":[#Decorator]: "Decorator .[prism-token prism-comment]"<br> -"di .[prism-token prism-atrule]":[#DI]: "DI Container .[prism-token prism-comment]"<br> -"extensions .[prism-token prism-atrule]":[#Extension'lar]: "Ek DI extension'ları kur .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#Dosyaları Dahil Etme]: "Dosyaları dahil etme .[prism-token prism-comment]"<br> -"parameters .[prism-token prism-atrule]":[#Parametreler]: "Parametreler .[prism-token prism-comment]"<br> -"search .[prism-token prism-atrule]":[#Search]: "Otomatik servis kaydı .[prism-token prism-comment]"<br> -"services .[prism-token prism-atrule]":[services]: "Servisler .[prism-token prism-comment]" -</pre> - -.[note] -İçinde `%` karakteri geçen bir dize yazmak için onu iki katına çıkararak `%%` biçiminde kaçışlamalısınız. - - -Parametreler -============ - -Yapılandırmada, sonra servis tanımlarının bir parçası olarak kullanabileceğiniz parametreler tanımlayabilirsiniz. Bu, yapılandırmayı netleştirmenizi ya da değişebilecek değerleri tek bir yerde toplamanızı sağlar. - -```neon -parameters: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: secret -``` - -`dsn` parametresine yapılandırmanın herhangi bir yerinde `%dsn%` yazımıyla başvururuz. Parametreler `'%wwwDir%/images'` gibi dizelerin içinde de kullanılabilir. - -Parametreler yalnızca dize ya da sayı olmak zorunda değildir, dizi de içerebilirler: - -```neon -parameters: - mailer: - host: smtp.example.com - secure: ssl - user: franta@gmail.com - languages: [cs, en, de] -``` - -Belirli bir anahtara `%mailer.user%` olarak başvururuz. - -Kodunuzun (örneğin bir sınıfın) bir parametrenin değerine ihtiyacı varsa, onu sınıfa aktarın. Örneğin yapıcıda. Sınıfların parametre değerleri için sorgulayabileceği genel bir yapılandırma nesnesi yoktur. Bu, bağımlılık enjeksiyonu ilkesinin çiğnenmesi olurdu. - - -Servisler -========= - -Bkz. [ayrı bölüm|services]. - - -Decorator -========= - -Belirli bir türdeki birden çok servisi aynı anda nasıl değiştirirsiniz? Örneğin belirli bir temel sınıftan türeyen tüm presenter'larda belirli bir metot nasıl çağrılır? Decorator işte bunun içindir. - -```neon -decorator: - # bu sınıfın ya da arayüzün örneği olan tüm servisler için - App\Presentation\BasePresenter: - setup: - - setProjectId(10) # bu metodu çağır - - $absoluteUrls = true # ve değişkeni ayarla -``` - -Decorator'lar [etiket |services#Etiketler] koymak ya da [inject kipini |services#Inject Kipi] açmak için de kullanılabilir. - -```neon -decorator: - InjectableInterface: - tags: [mytag: 1] - inject: true -``` - - -DI -=== - -DI container'ının teknik ayarları. - -```neon -di: - # DIC, Tracy Bar'da gösterilsin mi? - debugger: ... # (bool) varsayılan otomatik saptama (Tracy varsa açık) - - # asla autowiring'e girmeyecek parametre türleri - excluded: ... # (string[]) - - # tembel servis oluşturma açık olsun mu? - lazy: ... # (bool) varsayılan false - - # DI container'ın türeyeceği sınıf - parentClass: ... # (string) varsayılan Nette\DI\Container -``` - - -Tembel Servisler .{data-version:3.2.4} --------------------------------------- - -`lazy: true` ayarı, servislerin tembel (ertelenmiş) oluşturulmasını etkinleştirir. Yani servisler, DI container'dan istendikleri anda değil, ilk kullanıldıkları anda oluşturulur. Bu, uygulamanın açılışını hızlandırabilir ve bellek kullanımını azaltabilir; çünkü yalnızca ilgili istek için gerçekten gereken servisler oluşturulur. - -Belirli bir servis için tembel oluşturma [ayarlanabilir |services#Tembel Servisler]. - -.[note] -Tembel nesneler yalnızca kullanıcı tanımlı sınıflarda kullanılabilir, PHP'nin iç sınıflarında kullanılamaz. PHP 8.4 ya da daha yenisini gerektirir. - - -Meta Veri Dışa Aktarımı ------------------------ - -DI container sınıfı bolca meta veri de içerir. Meta veri dışa aktarımını azaltarak boyutunu küçültebilirsiniz. - -```neon -di: - export: - # parametreler dışa aktarılsın mı? - parameters: false # (bool) varsayılan true - - # etiketler dışa aktarılsın mı, hangileri? - tags: # (string[]|bool) varsayılan hepsi - - event.subscriber - - # autowiring verileri dışa aktarılsın mı, hangileri? - types: # (string[]|bool) varsayılan hepsi - - Nette\Database\Connection - - Symfony\Component\Console\Application -``` - -`$container->getParameters()` kullanmıyorsanız parametre dışa aktarımını kapatabilirsiniz. Ayrıca yalnızca `$container->findByTag(...)` ile servis almak için gerçekten kullandığınız etiketleri dışa aktarabilirsiniz. Bu metodu hiç çağırmıyorsanız etiket dışa aktarımını `false` ile tümüyle kapatabilirsiniz. - -[Autowiring|autowiring] için meta veriyi, yalnızca `$container->getByType()` ile gerçekten istediğiniz sınıfları listeleyerek belirgin biçimde azaltabilirsiniz. Yine, bu metodu hiç çağırmıyorsanız (ya da yalnızca [bootstrap|application:bootstrapping] dosyasında, örneğin `Nette\Application\Application` almak için çağırıyorsanız), tür dışa aktarımını `false` ile tümüyle kapatabilirsiniz. - - -Extension'lar -============= - -Ek DI extension'larının kaydı. Örneğin `Dibi\Bridges\Nette\DibiExtension3` DI extension'ını `dibi` adıyla böyle eklersiniz: - -```neon -extensions: - dibi: Dibi\Bridges\Nette\DibiExtension3 -``` - -Sonra onu `dibi` bölümünde yapılandırırsınız: - -```neon -dibi: - host: localhost -``` - -Extension olarak parametreli bir sınıf da ekleyebilirsiniz: - -```neon -extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, [%appDir%], %tempDir%/cache) -``` - - -Dosyaları Dahil Etme -==================== - -Ek yapılandırma dosyaları `includes` bölümünde dahil edilebilir: - -```neon -includes: - - parameters.php - - services.neon - - presenters.neon -``` - -`parameters.php` adı yazım hatası değil; yapılandırma, onu dizi olarak döndüren bir PHP dosyasında da yazılabilir: - -```php -<?php -return [ - 'database' => [ - 'main' => [ - 'dsn' => 'sqlite::memory:', - ], - ], -]; -``` - -Birden çok yapılandırma dosyasında aynı anahtarlara sahip öğeler bulunursa, üzerleri yazılır ya da dizilerde [birleştirilir |#Birleştirme]. Sonra dahil edilen dosyanın önceliği öncekinden yüksektir. `includes` bölümünün listelendiği dosyanın önceliği, içinde dahil edilen dosyalardan yüksektir. - - -Search -====== - -DI container'da servislerin otomatik kaydı geliştirmeyi belirgin biçimde kolaylaştırır. Nette presenter'ları container'a otomatik ekler, ama siz de başka herhangi bir sınıfı kolayca ekleyebilirsiniz. - -Yalnızca sınıfların hangi dizinlerde (ve alt dizinlerinde) aranacağını belirtin: - -```neon -search: - - in: %appDir%/Forms - - in: %appDir%/Model -``` - -Tek bir arama kuralına ihtiyacınız varsa listeyi atlayıp anahtarlarını doğrudan `search` altına yazabilirsiniz: - -```neon -search: - in: %appDir% -``` - -Ancak genellikle kesinlikle tüm sınıfları ve arayüzleri eklemek istemeyiz, bu yüzden onları filtreleyebiliriz: - -```neon -search: - - in: %appDir%/Forms - - # dosya adına göre filtreleme (string|string[]) - files: - - *Factory.php - - # sınıf adına göre filtreleme (string|string[]) - classes: - - *Factory -``` - -Ya da listelenen sınıflardan en az birinden türeyen veya onu uygulayan sınıfları seçebiliriz: - - -```neon -search: - - in: %appDir% - extends: - - App\*Form - implements: - - App\*FormInterface -``` - -Sınıf adı maskeleri ya da atalar kullanarak dışlama kuralları da tanımlayabilirsiniz. Bir sınıf dışlama kuralına uyarsa DI container'a eklenmez: - -```neon -search: - - in: %appDir% - exclude: - files: ... - classes: ... - extends: ... - implements: ... -``` - -Otomatik kaydedilen tüm servislere etiket atanabilir: - -```neon -search: - - in: %appDir% - tags: ... -``` - -Search, sınıfların yanı sıra tek bir `create()` ya da `get()` metodu olan arayüzleri de [üretilen factory ya da accessor |factory] olarak kaydeder. Container'da aynı türden bir servis zaten kayıtlıysa o sınıflar atlanır; böylece kopya oluşmaz. - - -Birleştirme -=========== - -Birden çok yapılandırma dosyasında aynı anahtarlara sahip öğeler bulunursa, üzerleri yazılır ya da dizilerde birleştirilir. Sonra dahil edilen dosyanın önceliği öncekinden yüksektir. - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>sonuç</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> - <td> -```neon -items: - - 1 - - 2 - - 3 -``` - </td> -</tr> -</table> - -Dizilerde birleştirme, anahtar adından sonra bir ünlem işareti eklenerek önlenebilir: - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>sonuç</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items!: - - 3 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> -</tr> -</table> - -{{maintitle: Bağımlılık Enjeksiyonu Yapılandırması}} diff --git a/dependency-injection/tr/container.texy b/dependency-injection/tr/container.texy deleted file mode 100644 index 6931f03e92..0000000000 --- a/dependency-injection/tr/container.texy +++ /dev/null @@ -1,142 +0,0 @@ -DI Container Nedir? -******************* - -.[perex] -Bağımlılık enjeksiyonu container'ı (DIC ya da DI container), başka nesneleri (servis denir) örneklemekten ve yapılandırmaktan sorumlu bir nesnedir. - -Sizi şaşırtabilir, ama çoğu durumda bağımlılık enjeksiyonunun (kısaca DI) yararlarından yararlanmak için bir bağımlılık enjeksiyonu container'ına ihtiyacınız yoktur. Nitekim [giriş bölümünde|introduction] somut DI örnekleri gösterdik ve hiçbirinde container gerekmedi. - -Ancak karmaşık bağımlılıkları olan çok sayıda nesneyi yönetirken DI container çok yararlı hâle gelir. Bir framework üzerine kurulmuş web uygulamalarında durum çoğu zaman böyledir. - -Önceki bölümde `Article` ve `EditController` sınıflarını tanıttık. İkisinin de bağımlılıkları var: veritabanı ve `ArticleFactory` factory'si. Şimdi bu sınıflar için bir container oluşturacağız. Elbette bu kadar basit bir örnek için container oluşturmak fazlasıyla abartılı. Ama nasıl göründüğünü ve nasıl çalıştığını göstermek için oluşturacağız. - -İşte yukarıdaki örnek için basit, sabit kodlanmış bir container: - -```php -class Container -{ - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection('mysql:', 'root', '***'); - } - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->createDatabase()); - } - - public function createEditController(): EditController - { - return new EditController($this->createArticleFactory()); - } -} -``` - -Kullanımı şöyle görünürdü: - -```php -$container = new Container; -$controller = $container->createEditController(); -``` - -Nesneyi container'dan yalnızca isteriz; onu nasıl oluşturacağımızı ya da bağımlılıklarının neler olduğunu bilmemize gerek yoktur, bunların hepsini container üstlenir. Bağımlılıklar container tarafından otomatik olarak enjekte edilir. Gücü buradan gelir. - -Şu an container'ın tüm bilgileri sabit kodlanmış durumda. Öyleyse bir sonraki adımı atalım ve container'ı gerçekten yararlı kılmak için parametre ekleyelim: - -```php -class Container -{ - public function __construct( - private array $parameters, - ) { - } - - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection( - $this->parameters['db.dsn'], - $this->parameters['db.user'], - $this->parameters['db.password'], - ); - } - - // ... -} - -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); -``` - -Keskin gözlü okurlar bir sorun fark edebilir. Her `EditController` nesnesi aldığımızda, `ArticleFactory` ile veritabanı bağlantısından da yeni örnekler oluşuyor. Bunu kesinlikle istemiyoruz. - -Bu yüzden, her zaman aynı örnekleri döndürecek bir `getService()` metodu ekleyeceğiz: - -```php -class Container -{ - private array $services = []; - - public function __construct( - private array $parameters, - ) { - } - - public function getService(string $name): object - { - if (!isset($this->services[$name])) { - // getService('Database') çağrısı createDatabase() metodunu çağırır - $method = 'create' . $name; - $this->services[$name] = $this->$method(); - } - return $this->services[$name]; - } - - // ... -} -``` - -İlk çağrıda, örneğin `$container->getService('Database')` çağrısında, veritabanı nesnesini oluşturmak için `createDatabase()` metodunu çağırır, onu `$services` dizisinde saklar ve döndürür. Sonraki çağrılarda ise doğrudan saklanan örneği döndürür. - -Container'ın geri kalanını da `getService()` kullanacak şekilde değiştiriyoruz: - -```php -class Container -{ - // ... - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->getService('Database')); - } - - public function createEditController(): EditController - { - return new EditController($this->getService('ArticleFactory')); - } -} -``` - -Bu arada, servis terimi container'ın yönettiği herhangi bir nesneyi anlatır. Metodun adı da bu yüzden `getService()`. - -Bitti. Tam işlevsel bir DI container'ımız var! Ve onu kullanabiliriz: - -```php -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); - -$controller = $container->getService('EditController'); -$database = $container->getService('Database'); -``` - -Gördüğünüz gibi bir DIC yazmak zor değil. Nesnelerin kendilerinin, onları bir container'ın oluşturduğundan habersiz olduğunu belirtmekte yarar var. Dolayısıyla herhangi bir PHP nesnesi, kaynak kodunu değiştirmeden bu şekilde oluşturulabilir. - -Container sınıfını elle oluşturup bakımını yapmak hızla bir kâbusa dönüşebilir. Bu yüzden bir sonraki bölümde, neredeyse tümüyle otomatik olarak kendini üretip güncelleyebilen [Nette DI Container|nette-container] konusunu ele alacağız. - - -{{maintitle: Bağımlılık Enjeksiyonu Container'ı Nedir?}} diff --git a/dependency-injection/tr/extensions.texy b/dependency-injection/tr/extensions.texy deleted file mode 100644 index fb35e44889..0000000000 --- a/dependency-injection/tr/extensions.texy +++ /dev/null @@ -1,443 +0,0 @@ -Nette DI İçin Extension Yazma -***************************** - -.[perex] -Extension, DI container'ın derlenmesine kancalanan bir sınıftır. Programlı olarak servis kaydedebilir, kendi yapılandırma bölümünü doğrulayabilir, başkalarının tanımladığı servisleri değiştirebilir ve hatta üretilen container kodunu değiştirebilir. Bu sayfa size bir extension'ı nasıl yazacağınızı, ne zaman ne olduğunu ve nelere dikkat etmeniz gerektiğini öğretiyor. - -Extension'lar, paketlerin Nette'ye doğal yoldan katılma biçimidir: tüm `nette/*` paketleri onları kullanır, sizinkiler de kullanabilir. Tipik bir extension şunlardan birini ya da birkaçını yapar: - -- **bir kütüphaneyi tümleştirir** - servislerini container'a kaydeder ve dost canlısı, doğrulanmış bir yapılandırma bölümü sunar (`mail:` ya da `database:` bölümleri buradan gelir) -- **kaydı otomatikleştirir** - birbirine benzeyen çok sayıda servisi bir döngüde ya da bir kurala göre kaydeder; bunları `services:` bölümünde tek tek yazmak zahmetli olurdu -- **kesişen değişiklikler yapar** - başkalarının kaydettiği servisleri bulup tamamlar, örneğin belirli bir etikete sahip her servise bir logger iliştirir - -Günlük uygulama işlerinde extension'a ender ihtiyaç duyarsınız; sınıflarınızı kaydetmeyi ve bağlamayı yapılandırmanın [services |services] bölümü karşılar. Yapılandırma tek başına yetmemeye başladığında extension'a yönelin. - -Extension `extensions` bölümünde etkinleştirilir. `BlogExtension` sınıfının temsil ettiği bir extension'ı `blog` adıyla böyle eklersiniz: - -```neon -extensions: - blog: BlogExtension -``` - -Yapıcısı argüman alıyorsa onları hemen orada verin: - -```neon -extensions: - blog: BlogExtension(%debugMode%) -``` - - -Derleme Nasıl İşler -=================== - -Extension yazarken kendinize güvenmek için bir şeyi bilmeniz gerekir: **kodunuzun ne zaman çalıştığını.** Nette, servisleri istekleri işlerken bağlamaz. Bunun yerine container'ı önceden *derler*: tüm yapılandırma dosyalarını okur, extension'ların işini yapmasına izin verir ve iyileştirilmiş bir PHP sınıfı üretip diske yazar. Sonraki her istek yalnızca bu bitmiş sınıfı yükler. Dolayısıyla extension kodunuz yalnızca container (yeniden) kurulurken çalışır, her istekte değil. - -Bunun önemli bir sonucu var: derleme sırasında henüz hiçbir servis yoktur. Var olan şey **tanımlardır**; her servisin hangi sınıf olacağını, nasıl oluşturulacağını ve sonrasında üzerinde ne çağrılacağını anlatan tarifler. Tanımlar [ContainerBuilder |#ContainerBuilder] nesnesinde durur. Extension aslında *betiklenebilir yapılandırmadır*: `services:` bölümünde bildirebildiğiniz her şeyi PHP'de de kurabilirsiniz; koşullu olarak, döngülerle ya da başkalarının kaydettiklerine tepki vererek. - -Derleme aşamalar hâlinde ilerler ve bir extension her aşamaya adım atabilir: - -1) tüm extension'ların yapılandırma bölümleri doğrulanır (`getConfigSchema()`) -2) her extension servislerini kaydeder (`loadConfiguration()`); kullanıcının `services:` bölümü en son işlenir, böylece son sözü her zaman uygulama söyler -3) tüm tanımlar yerine oturup servis türleri çözüldükten sonra extension'lar onları değiştirebilir (`beforeCompile()`) -4) container sınıfı üretilir; extension'lar kodunu hâlâ ayarlayabilir (`afterCompile()`) ve uygulama başladığında çalışacak kod üretebilir ([başlatma |#Başlatma Kodu]) - -.[note] -Geliştirici kipinde container, bir yapılandırma dosyasını ya da extension sınıfının kendisini değiştirdiğinizde otomatik olarak yeniden derlenir; her ikisi de bağımlılık olarak izlenir. Böylece extension'ları hiç önbellek temizlemeden geliştirebilirsiniz. - -.[tip] -Her aşamada ne olduğuna daha derin bakmak için (parametreler ne zaman genişletilir, `@service` ne zaman referansa dönüşür ve servisleri türe göre aramak tam olarak ne zaman güvenlidir) bkz. [Container derlemesi nasıl işler |compilation-internals]. - - -İlk Extension -============= - -İşte küçük ama eksiksiz bir extension. Onu aynı dosyada etkinleştirip yapılandırıyoruz: - -```neon -extensions: - blog: BlogExtension - -blog: - postsPerPage: 5 -``` - -Ve sınıfın tamamı şu: - -```php -use Nette\Schema\Expect; - -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function getConfigSchema(): Nette\Schema\Schema - { - return Expect::structure([ - 'postsPerPage' => Expect::int(10), - 'allowComments' => Expect::bool(true), - ]); - } - - - public function loadConfiguration(): void - { - $builder = $this->getContainerBuilder(); - - $builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class, ['postsPerPage' => $this->config->postsPerPage]); - - if ($this->config->allowComments) { - $builder->addDefinition($this->prefix('comments')) - ->setFactory(Blog\Comments::class); - } - } -} -``` - -`getConfigSchema()`, `blog:` bölümünün (extension'ı kaydettiğimiz anahtarın adını taşır) neler içerebileceğini, türleri ve varsayılan değerleri de kapsayacak şekilde anlatır; doğrulanan değerler sonra `$this->config` içinde bulunur. `loadConfiguration()` içinde servisleri kaydederiz. Adlara dikkat edin: `$this->prefix('articles')`, `blog.articles` üretir; böylece farklı extension'ların servisleri çakışamaz. - -Ve son birkaç satır, extension'ların neden var olduğunu gösterir: `comments` servisi yalnızca yorumlar açıkken kaydedilir. Düz bir yapılandırma dosyası böyle kararlar veremez. - -Bu şekilde kaydedilen servisler tam olarak `services:` bölümüne yazılmış gibi davranır; istendiğinde tembel olarak oluşturulurlar ve autowiring onları `Blog\Articles` türünün bildirildiği her yere aktarır. - -Sonraki bölümler önce extension yaşam döngüsünü ayrıntılı anlatıyor, sonra extension içinde kullanacağınız [ContainerBuilder |#ContainerBuilder] API'sini ve en sonunda bilinmeye değer [tuzakları |#İpuçları ve Tuzaklar]. - - -Extension Yaşam Döngüsü -======================= - -Bir extension [api:Nette\DI\CompilerExtension] sınıfından türer ve derleyicinin derleme sırasında bu sırayla çağırdığı dört metottan (`getConfigSchema()`, `loadConfiguration()`, `beforeCompile()` ve `afterCompile()`) bazılarını geçersiz kılar. - - -getConfigSchema(): Nette\Schema\Schema .[method] ------------------------------------------------- - -Extension'ın yapılandırma bölümünün şemasını tanımlar. Bu sayede kullanıcılar doğrulamayı ve anlaşılır hata mesajlarını bedava alır: `blog:` bölümündeki bir yazım hatası ya da yanlış tür, siz tek bir denetim yazmadan anlaşılır bir mesajla bildirilir. - -Şema, [Schema |schema:] kütüphanesiyle anlatılır ve türleri, varsayılan değerleri, izin verilen değerleri ve çok daha fazlasını ifade edebilir: - -```php -public function getConfigSchema(): Nette\Schema\Schema -{ - return Expect::structure([ - 'postsPerPage' => Expect::int(10), - 'storage' => Expect::anyOf('files', 'database')->firstIsDefault(), - ]); -} -``` - -Doğrulanan yapılandırma, `$this->config` içinde bir `stdClass` nesnesi olarak bulunur (şemaya `castTo('array')` eklerseniz dizi olarak). - -Bir seçeneğin değeri derleme zamanında bilinemiyorsa (örneğin bir ortam değişkeninden geliyorsa), onu `dynamic()` ile işaretleyin, örneğin `Expect::int()->dynamic()`. Ayrıntısı [dinamik parametreler |application:bootstrapping#Dinamik parametreler] bölümünde. - - -loadConfiguration() .[method] ------------------------------ - -Extension'ın servislerini [ContainerBuilder |#ContainerBuilder] kullanarak kaydettiği yer: - -```php -public function loadConfiguration(): void -{ - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class); -} -``` - -Bir servis kısa bir adla da erişilebilir olmalıysa bir alias ekleyin. Uzlaşıya göre bu yalnızca extension olağan adıyla kaydedildiğinde yapılır; böylece extension'ın birden çok örneği bunun için çekişemez: - -```php -if ($this->name === 'blog') { - $builder->addAlias('articles', $this->prefix('articles')); -} -``` - -Çok sayıda servis olduğunda, onları tanıdık [services |services] söz dizimiyle ayrı bir NEON dosyasında tanımlamak daha elverişli olabilir. `@extension` öneki geçerli extension'a başvurur: - -```neon -services: - articles: - create: MyBlog\ArticlesModel(@connection) - - comments: - create: MyBlog\CommentsModel(@connection, @extension.articles) -``` - -Bu tanımları `loadDefinitionsFromConfig()` ile yükleriz; adlara önek otomatik eklenir ve dosya bağımlılık olarak izlenir, böylece değiştirilmesi yeniden derlemeyi tetikler: - -```php -public function loadConfiguration(): void -{ - $this->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/services.neon')['services'], - ); -} -``` - - -beforeCompile() .[method] -------------------------- - -Bu metot çağrıldığında builder **tüm** tanımları çoktan tutuyordur: sizinkileri, diğer extension'larınkileri ve kullanıcının yapılandırma dosyalarından gelenleri. Servis türleri de çözülmüştür, dolayısıyla türe göre arama güvenilirdir. Bu da bu aşamayı, nihai servis grafiğini incelemek ve tamamlamak için ideal kılar. - -Genellikle servisleri etikete ya da türe göre arar ve bulunan tanımları tamamlarsınız: - -```php -public function beforeCompile(): void -{ - $builder = $this->getContainerBuilder(); - - foreach ($builder->findByTag('logaware') as $name => $attrs) { - $builder->getDefinition($name)->addSetup('setLogger'); - } -} -``` - -`setLogger()` çağrısının açık argümanı yok; tıpkı factory'lerde olduğu gibi onları autowiring sağlayacak. - -`$this->compiler->getExtensions()` ile alınan, isteğe bağlı olarak sınıfa ya da arayüze göre süzülen diğer kayıtlı extension'larla da işbirliği yapabilirsiniz: - -```php -foreach ($this->compiler->getExtensions(FooExtension::class) as $extension) { - // ... -} -``` - - -afterCompile(Nette\PhpGenerator\ClassType $class) .[method] ------------------------------------------------------------ - -Son aşamada container sınıfı, bir [ClassType |php-generator:#Sınıflar] nesnesi olarak üretilir; bu, [PHP Generator |php-generator:] kütüphanesinin bir parçasıdır. Her servis için bir factory metodu içerir ve önbelleğe yazılmak üzeredir. Kodunu hâlâ değiştirebilirsiniz: - -```php -public function afterCompile(Nette\PhpGenerator\ClassType $class): void -{ - $method = $class->getMethod('__construct'); - // ... -} -``` - -Bu aşamaya yalnızca ender ihtiyaç duyarsınız. Uygulama başladığında çalışacak kod eklemek için bunun yerine başlatmayı kullanın: - - -Başlatma Kodu -------------- - -Önceki aşamaların hepsi container'ın nasıl *kurulacağını* etkiler. Buna ek olarak bir extension, *çalışma zamanında*, container oluşturulduktan hemen sonra çalışan kod üretebilir; örneğin bir oturum başlatmak ya da servisleri ayağa kaldırmak için. Kod, `$this->initialization` nesnesine onun [addBody() |php-generator:#Metot ve Fonksiyon Gövdeleri] metoduyla yazılır: - -```php -public function loadConfiguration(): void -{ - // 'run' etiketli servisler container başlar başlamaz oluşturulmalı - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $attrs) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } -} -``` - -Nette'nin kendisi başlatmayı örneğin oturumu otomatik başlatmak ya da güvenlikle ilgili HTTP header'ları göndermek için kullanır. Ve şunu unutmayın: extension'daki her şeyin aksine bu kod **her istekte** çalışır, bu yüzden onu küçük tutun. - - -ContainerBuilder -================ - -[api:Nette\DI\ContainerBuilder], bir extension'ın derleyiciyle konuştuğu nesnedir. Tüm servislerin [tanımlarını |#Derleme Nasıl İşler] tutar ve onları eklemek, aramak ve değiştirmek için metotlar sunar. Onu `loadConfiguration()` ve `beforeCompile()` içinde alırsınız: - -```php -$builder = $this->getContainerBuilder(); -``` - - -Servis Ekleme -------------- - -Bir servisi kaydetmek, NEON dosyasının `services:` bölümünde yaptığınızın aynısıdır; yalnızca PHP'de yazılmıştır. Her yapılandırma anahtarının tanım üzerinde bir karşılığı vardır, dolayısıyla şu iki yazım eşdeğerdir: - -```neon -services: - articles: - create: Blog\Articles(@connection) - setup: - - setLogger(@logger) - tags: [logaware] -``` - -```php -$builder->addDefinition($this->prefix('articles')) - ->setFactory(Blog\Articles::class, ['@connection']) - ->addSetup('setLogger', ['@logger']) - ->addTag('logaware'); -``` - -`addDefinition()` metodunun döndürdüğü tanım, yapılandırma anahtarlarının karşılıklarını sunan bir [ServiceDefinition |#Tanım Türleri] nesnesidir: `setType()` (servisin sınıfı), `setFactory()` (nasıl oluşturulacağı), `setArguments()`, `addSetup()`, `addTag()` ve `setAutowired()`. - -`addSetup()`, `setup:` listesini yansıtır ve aynı biçimleri kabul eder: bir metot çağrısı `addSetup('setLogger', ['@logger'])`, bir özellik ataması `addSetup('$cache', ['@cache'])` ya da başka bir serviste çağrı `addSetup('@Tracy\Bar::addPanel', [$panel])`. - -Builder, sıradan servislerin yanı sıra [üretilen |factory] factory'leri, accessor'ları ve locator'ları da kaydedebilir; her birinin, ilgili [tanım türünü |#Tanım Türleri] döndüren kendi metodu vardır: - -| Metot | Kaydettiği -|--------|---------- -| `addDefinition()` | sıradan bir servis (`ServiceDefinition` döndürür) -| `addFactoryDefinition()` | üretilen bir [factory |factory] (`create()` metotlu arayüz) -| `addAccessorDefinition()` | üretilen bir [accessor |factory#Accessor] (`get()` metotlu arayüz) -| `addLocatorDefinition()` | birkaç factory'yi birleştiren bir [çoklu factory / locator |factory#Çoklu Factory/Accessor] -| `addImportedDefinition()` | container'a çalışma zamanında dışarıdan aktarılan bir servis -| `addAlias()` | var olan bir servis için ikinci bir ad - -Bir factory'de, oluşturduğu nesneyi `getResultDefinition()` ile yapılandırırsınız; accessor ise `setReference()` ile var olan bir servise işaret eder: - -```php -$builder->addFactoryDefinition($this->prefix('latteFactory')) - ->setImplement(LatteFactory::class) - ->getResultDefinition() - ->setFactory(Latte\Engine::class) - ->addSetup('setStrictTypes', [true]); -``` - -`addLocatorDefinition()` ve `addImportedDefinition()` metotlarına ender ihtiyaç duyulur; böyle servisler genellikle elle yazılmak yerine NEON'daki `implement:` ve içe aktarılan servis anahtarlarından gelir. - - -Servisleri Bulma ve Değiştirme ------------------------------- - -Var olan tanımları aramak ve dolaşmak için builder şunları sunar: - -| Metot | Açıklama -|--------|------------ -| `getDefinition(string $name)` | verilen addaki tanım (yoksa istisna fırlatır) -| `hasDefinition(string $name)` | bu adda bir tanım ya da alias var mı -| `getDefinitions()` | tüm tanımlar -| `removeDefinition(string $name)` | bir tanımı kaldırır -| `getByType(string $type)` | o türdeki autowiring servisinin adı ya da `null` -| `getDefinitionByType(string $type)` | o türdeki autowiring tanımı -| `findByType(string $type)` | o türdeki tüm tanımlar, `ad => tanım` çiftleri olarak -| `findByTag(string $tag)` | etiketi taşıyan servisler, `ad => etiket değeri` çiftleri olarak -| `addExcludedClasses(array $types)` | sınıfları ve arayüzleri autowiring'den çıkarır - -Kullanışlı bir kalıp, bir servisin var olup olmadığını öğrenmek için `getByType()` kullanmaktır; örneğin yalnızca uygulamada varsa bir logger'a kancalanmak için: - -```php -if ($builder->getByType(Psr\Log\LoggerInterface::class)) { - $builder->getDefinition($this->prefix('articles')) - ->addSetup('setLogger'); -} -``` - - -Tanım Türleri -------------- - -Her `add*Definition()` metodu farklı türde bir tanım döndürür. Hepsi ortak ata `Nette\DI\Definitions\Definition` sınıfından türer: - -- **`ServiceDefinition`** - sıradan bir servis; `setType()`, `setFactory()`, `addSetup()`, `addTag()` ve `setAutowired()` ile yapılandırılır -- **`FactoryDefinition`** - [üretilen bir factory |factory]: `create()` metodu her çağrıda yeni bir nesne döndüren arayüz -- **`AccessorDefinition`** - [üretilen bir accessor |factory#Accessor]: `get()` metodu var olan bir servisi döndüren arayüz -- **`LocatorDefinition`** - birkaç factory ya da accessor'ı tek arayüzde birleştiren [çoklu factory / locator |factory#Çoklu Factory/Accessor] -- **`ImportedDefinition`** - container'ın kendisinin oluşturmadığı, çalışma zamanında dışarıdan aldığı bir servis - -Şunu unutmayın: `getDefinition()`, verilen adın altında hangi türden tanım varsa onu döndürür. Kodunuz üretilen bir factory ile karşılaşabiliyorsa, önce türü denetleyin ve üretilen nesneyi `getResultDefinition()` ile yapılandırın: - -```php -$def = $builder->getDefinition($name); -if ($def instanceof Nette\DI\Definitions\FactoryDefinition) { - $def = $def->getResultDefinition(); -} -$def->addSetup('setLogger'); -``` - - -İpuçları ve Tuzaklar -==================== - - -Derleme Zamanı ve Çalışma Zamanı --------------------------------- - -En yaygın kafa karışıklığı kaynağı: extension kodu, uygulama istekleri işlerken değil, container **derlenirken** çalışır. Pratikte bu şu demektir: - -- Extension asla servis örnekleriyle çalışmaz; onlar henüz yoktur. Servisleri `new` ile örneklemeyin; bir tanım kaydedin ve onları container oluştursun. -- Tüm yapılandırma değerleri üretilen koda gömülür. Ortamlar arasında değişebilen bir değer (bir yol, `getenv()` ile alınan bir parola) [dinamik |application:bootstrapping#Dinamik parametreler] işaretlenmelidir; aksi hâlde derleme zamanında donar. -- `$this->initialization->addBody()` metoduna verilen dizeler şimdi çalıştırılmaz; container'a üretilen ve her istekte çalışan PHP kodudur. - - -Dosya Bağımlılıkları --------------------- - -Container, yapılandırma dosyaları ya da extension sınıfları değiştiğinde yeniden derlenir. Ama extension'ınız başka bir dosyayı okuyorsa (bir varlık listesi, bir kütüphanenin XML yapılandırması), container'ın bundan haberi olmaz. Böyle dosyaları şununla kaydedin: - -```php -$builder->addDependency($file); -``` - -Aksi hâlde klasik bir gizemle karşılaşırsınız: dosyayı düzenlersiniz, ama uygulama eski biçimde davranmayı sürdürür; değişiklik ancak container başka bir nedenle yeniden kurulduğunda ortaya çıkar. (`loadFromFile()` ile okunan dosyalar otomatik izlenir.) - - -Koşullu Kayıt -------------- - -Bir extension kendini ortamına uydurabilir. İsteğe bağlı tümleştirmeler tipik olarak `class_exists()` ile korunur: - -```php -if (class_exists(Symfony\Component\Console\Command\Command::class)) { - $builder->addDefinition($this->prefix('command')) - ->setFactory(Blog\Console\SitemapCommand::class); -} -``` - -`%debugMode%` gibi değerleri de en iyi extension'ın yapıcısı üzerinden aktarırsınız: - -```neon -extensions: - blog: BlogExtension(%debugMode%) -``` - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function __construct( - private bool $debugMode = false, - ) {} -} -``` - -Tipik bir kullanım, bir Tracy panelini yalnızca geliştirici kipinde kaydetmektir. - - -Karmaşık Argümanlar -------------------- - -Bazen bir factory'nin ya da setup çağrısının argümanı düz bir değer, bir sınıf adı ya da `@service` referansı değildir. Bu durumlar için şunlar vardır: - -- `new Nette\DI\Definitions\Statement(Blog\Panel::class, [$args])` - yerinde oluşturulan bir nesne; argüman olarak kullanılan "anonim servis" -- `new Nette\DI\Definitions\Reference('blog.articles')` - bir servise referans; `@name` dizesinin nesne karşılığı -- `$builder::literal('PHP_SAPI')` - üretilen container'a olduğu gibi eklenen ham PHP kodu parçası - -Örnek - bir Tracy paneli kaydetme: - -```php -$builder->getDefinition($this->prefix('articles')) - ->addSetup('@Tracy\Bar::addPanel', [ - new Nette\DI\Definitions\Statement(Blog\ArticlesPanel::class), - ]); -``` - - -Dışa Aktarılan Etiketler ve Türler ----------------------------------- - -[Meta veri dışa aktarımı |configuration#Meta Veri Dışa Aktarımı], derlenmiş container'ın yalnızca uygulamanın gerçekten kullandığı etiketleri ve autowiring türlerini tutması için yapılandırmada kısıtlanabilir. Extension'ınız çalışma zamanında `$container->findByTag()` ya da `$container->getByType()` ile servis alıyorsa, böyle bir kısıtlama tam da dayandığınız meta veriyi silebilir. - -Bunu önlemek için derleyiciye hangi etiketlerin ve türlerin her zaman dışa aktarılması gerektiğini söyleyin: - -```php -public function loadConfiguration(): void -{ - // bu etiket, dışa aktarım kısıtlansa bile her zaman dışa aktarılır - $this->compiler->addExportedTag('event.subscriber'); - - // bu tür getByType() için her zaman kullanılabilir olur - $this->compiler->addExportedType(Nette\Database\Connection::class); -} -``` - -Her iki metot da yalnızca dışa aktarılan meta veriye ekleme yapar; uygulamanın `di › export` yapılandırmasını asla geçersiz kılmaz. Yani uygulama dışa aktarımı bir listeyle kısıtladığında, extension'ınızın ihtiyaç duyduğu etiketler ve türler dahil kalır; yalnızca etiket dışa aktarımını tümüyle kapatmak (`tags: false`) onları da her şeyle birlikte atar. diff --git a/dependency-injection/tr/factory.texy b/dependency-injection/tr/factory.texy deleted file mode 100644 index d02415f06e..0000000000 --- a/dependency-injection/tr/factory.texy +++ /dev/null @@ -1,233 +0,0 @@ -Üretilen Factory'ler -******************** - -.[perex] -Nette DI, arayüzlere dayanarak factory kodunu otomatik üretebilir ve sizi kod yazmaktan kurtarır. - -Factory, nesneleri oluşturmaktan ve bağımlılıklarını aktarmaktan sorumlu bir sınıftır. Lütfen bunu, factory kullanımının belirli bir biçimini anlatan ve bu konuyla ilgisi olmayan *factory method* tasarım deseniyle karıştırmayın. - -Böyle bir factory'nin nasıl göründüğünü [giriş bölümünde |introduction#Factory] gösterdik: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Nette DI factory kodunu otomatik üretebilir. Tek yapmanız gereken bir arayüz oluşturmaktır; gerçekleştirimi Nette DI üretir. Arayüzde `create` adında tam olarak bir metot bulunmalı ve bir dönüş türü bildirilmelidir: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Yani `ArticleFactory` factory'sinin, `Article` nesneleri oluşturan bir `create` metodu vardır. `Article` sınıfı örneğin şöyle görünebilir: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } -} -``` - -Factory'yi yapılandırma dosyasına ekleyin: - -```neon -services: - - ArticleFactory -``` - -Nette DI ilgili factory gerçekleştirimini üretir. - -Factory'yi kullanan kodda nesneyi arayüzü üzerinden isteyin; Nette DI üretilen gerçekleştirimi verir: - -```php -class UserController -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function foo() - { - // bırakın nesneyi factory oluştursun - $article = $this->articleFactory->create(); - } -} -``` - - -Parametreli Factory -=================== - -`create` factory metodu, sonra yapıcıya aktardığı parametreler alabilir. Örneğin `Article` sınıfına makale yazarının ID'sini ekleyelim: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - private int $authorId, - ) { - } -} -``` - -Parametreyi factory'ye de ekleyeceğiz: - -```php -interface ArticleFactory -{ - function create(int $authorId): Article; -} -``` - -Yapıcıdaki parametre adı (`$authorId`) factory metodundaki parametre adıyla eşleştiğinden, Nette DI onu otomatik olarak aktarır. - - -Gelişmiş Tanım -============== - -Tanım, `implement` anahtarı kullanılarak çok satırlı biçimde de yazılabilir: - -```neon -services: - articleFactory: - implement: ArticleFactory -``` - -Bu uzun biçimi kullanmak, sıradan servis tanımlarında olduğu gibi `arguments` anahtarıyla yapıcı için ek argümanlar belirtmeyi ve `setup` ile daha fazla yapılandırma yapmayı sağlar. - -Örnek: `create()` metodu `$authorId` parametresini almasaydı, `Article` yapıcısına aktarılacak sabit bir değeri yapılandırmada verebilirdik: - -```neon -services: - articleFactory: - implement: ArticleFactory - arguments: - authorId: 123 -``` - -Tersine, `create()` metodu `$authorId` parametresini alsaydı ama bu parametre yapıcının bir parçası olmayıp `Article::setAuthorId()` gibi bir metotla aktarılsaydı, parametreye `setup` bölümünde başvururduk: - -```neon -services: - articleFactory: - implement: ArticleFactory - setup: - - setAuthorId($authorId) -``` - - -Accessor -======== - -Nette, factory'lerin yanı sıra accessor denen nesneleri de üretebilir. Bunlar, DI container'dan belirli bir servisi döndüren bir `get()` metoduna sahip nesnelerdir. `get()` metodunun yinelenen çağrıları her zaman aynı örneği döndürür. - -Accessor'lar, bağımlılıklar için tembel yükleme sağlar. Hataları özel bir veritabanına günlükleyen bir sınıf düşünün. Bu sınıf veritabanı bağlantısını yapıcı bağımlılık enjeksiyonuyla alsaydı, hatalar ender oluşsa ve bağlantı çoğu zaman kullanılmasa bile bağlantı her zaman kurulurdu. Bunun yerine sınıf bir accessor alabilir. Veritabanı nesnesi (bağlantı) yalnızca accessor'ın `get()` metodu ilk kez çağrıldığında oluşturulur. - -Accessor nasıl oluşturulur? Yalnızca bir arayüz yazın; gerçekleştirimi Nette DI üretir. Arayüzde `get` adında, parametre almayan ve dönüş türü bildiren tam olarak bir metot bulunmalıdır: - -```php -interface PDOAccessor -{ - function get(): PDO; -} -``` - -Accessor'ı, döndürmesi gereken servisin tanımıyla birlikte yapılandırma dosyasına ekleyin: - -```neon -services: - - PDOAccessor - - PDO(%dsn%, %user%, %password%) -``` - -Accessor bir `PDO` servisi döndürdüğünden ve yapılandırmada bu türden yalnızca bir servis tanımlı olduğundan, accessor o servisi döndürür. Bu türden birden çok servis varsa, accessor'ın hangisini döndüreceğini adıyla belirtin, örneğin `- PDOAccessor(@db1)`. - - -Çoklu Factory/Accessor -====================== - -Şimdiye kadar factory'lerimiz ve accessor'larımız yalnızca tek bir nesne türünü oluşturabiliyor ya da döndürebiliyordu. Ancak factory ve accessor özelliklerini birleştiren çoklu factory'ler de kolayca oluşturabilirsiniz. Böyle bir bileşenin arayüzü `create<Name>()` ve `get<Name>()` adlı birden çok metot içerebilir, örneğin: - -```php -interface MultiFactory -{ - function createArticle(): Article; - function getDb(): PDO; -} -``` - -Yani birden çok ayrı factory ve accessor enjekte etmek yerine tek ve daha kapsamlı bir bileşen enjekte edebilirsiniz. - -Alternatif olarak, birden çok metot yerine parametreli bir `get()` kullanılabilir: - -```php -interface MultiFactoryAlt -{ - function get($name): PDO; -} -``` - -O zaman `MultiFactory::getDb()`, `MultiFactoryAlt::get('db')` ile aynı işi yapar. Ancak bu alternatif yazımın sakıncası, `$name` için desteklenen değerlerin arayüz imzasından açıkça anlaşılmamasıdır. Ayrıca arayüzde farklı `$name` değerleri için farklı dönüş türleri tanımlayamazsınız. - -Arayüz, `get($name)` yerine her çağrıda yeni bir örnek döndüren `create($name)` metodunu da bildirebilir (oysa `get()` paylaşılan bir örnek döndürür). Arayüzde böyle parametreli yalnızca bir metot bulunabilir. Metodun dönüş türü nullable ise (örneğin `?PDO`), bilinmeyen bir `$name` değeri için istisna fırlatmak yerine `null` döndürür. - - -Listeyle Tanım --------------- -Çoklu factory'yi yapılandırmada bir liste kullanarak, servisleri satır içinde yazarak tanımlayabilirsiniz: .{data-version:3.2.0} - -```neon -services: - - MultiFactory( - article: Article() # createArticle() tanımlar - db: PDO(%dsn%, %user%, %password%) # getDb() tanımlar - ) -``` - -Alternatif olarak, çoklu factory tanımında var olan servislere referanslarla başvurabilirsiniz: - -```neon -services: - article: Article - - PDO(%dsn%, %user%, %password%) - - MultiFactory( - article: @article # createArticle() tanımlar - db: @\PDO # getDb() tanımlar - ) -``` - - -Etiketlerle Tanım ------------------ - -Çoklu factory tanımlamanın bir başka yolu da [etiketleri |services#Etiketler] kullanmaktır. Etiketin değeri, ilgili metodun adını belirler: - -```neon -services: - article: - create: Article - tags: {multi: article} # createArticle() tanımlar - db: - create: PDO(%dsn%, %user%, %password%) - tags: {multi: db} # getDb() tanımlar - - - MultiFactory(tagged: multi) -``` diff --git a/dependency-injection/tr/faq.texy b/dependency-injection/tr/faq.texy deleted file mode 100644 index b45ccdd923..0000000000 --- a/dependency-injection/tr/faq.texy +++ /dev/null @@ -1,123 +0,0 @@ -DI Hakkında Sıkça Sorulan Sorular (FAQ) -*************************************** - - -DI, IoC'nin başka bir adı mı? ------------------------------ - -*Inversion of Control* (IoC), bir programdaki denetim akışını anlatan bir ilkedir: kodunuz mu dış kodu çağırıyor, yoksa dış kod (örneğin bir framework) mu sizin kodunuzu çağırıyor? IoC; [olayları |nette:glossary#Olaylar], "Hollywood ilkesi" olarak bilinen yaklaşımı ([Hollywood Principle |application:components#Hollywood tarzı]) ve başka yönleri kapsayan geniş bir kavramdır. Bu kavram, [Kural #3: Bırakın factory halletsin |introduction#Kural #3: Bırakın Factory Halletsin] bölümünde anlatılan ve `new` operatörünün tersine çevrilmesini temsil eden factory'leri de içerir. - -*Dependency Injection* (DI) ise nesnelerin bağımlılıklarını (yani birlikte çalışmaları gereken diğer nesneleri) nasıl edindiğine odaklanır. Bağımlılıkları nesnelerin kendisinin oluşturması ya da bulması yerine, onlara açıkça aktarılmasını savunan bir tasarım desenidir. - -Dolayısıyla DI, IoC'nin özel bir biçimi sayılabilir. Ancak IoC'nin her biçimi temiz kodu desteklemez. Örneğin [genel duruma|global-state] ya da [Service Locator |#Service Locator nedir?] desenine dayanan teknikler birer anti-desendir. - - -Service Locator nedir? ----------------------- - -Bağımlılık enjeksiyonuna alternatif bir yaklaşımdır. Kullanılabilir tüm servislerin (bağımlılıkların) kaydedildiği merkezi bir nesne (locator) içerir. Bir nesnenin bir bağımlılığa ihtiyacı olduğunda onu Service Locator'dan ister. - -Ancak DI ile karşılaştırıldığında saydamlıktan yoksundur. Bağımlılıklar, nesnenin API'sinde (yapıcıda ya da metotlarda) açıkça görünmek yerine nesnenin kodunun içinde (locator çağrılarında) gizlidir; bağlantıları anlamak için kodu incelemek gerekir. Test etmek de daha karmaşıktır; nesneyi örneklerken sahte bağımlılıkları öylece aktaramazsınız, çoğu zaman Service Locator'ın kendisiyle uğraşmanız gerekir. Üstelik Service Locator gereksiz bir bağımlılık getirir: nesneler locator'a bağlanır; oysa DI'da nesneler ideal olarak container'dan habersiz kalır. - - -DI'yı ne zaman kullanmamak daha iyidir? ---------------------------------------- - -Bağımlılık enjeksiyonu tasarım deseninin doğru kullanımının bilinen kayda değer bir sakıncası yoktur. Tersine, bağımlılıkları her yerden erişilebilir yerlerden (statik özellikler ya da singleton'lar gibi) edinmek, tıpkı Service Locator kullanmak gibi [pek çok soruna|global-state] yol açar. Bu yüzden DI kullanmak genellikle her zaman önerilir. Bu bir dogma değildir; yalnızca bağımlılıkları temiz biçimde yönetmek için daha iyi bir alternatif yaygın olarak benimsenmemiştir. - -Yine de nesnelere her yerden erişmenin kabul edilebilir olduğu belirli, sınırlı durumlar vardır. Örneğin hata ayıklama sırasında bir değişkenin değerini dökmeniz, yürütme süresini ölçmeniz ya da belirli bir noktada bir mesaj günlüklemeniz gerektiğinde. Sonradan koddan kaldırılacak geçici işlemleri kapsayan bu durumlarda, her yerden erişilebilir bir dumper, zamanlayıcı ya da logger kullanmak meşru olabilir. Bu araçlar uygulamanın temel tasarımının parçası değildir. - - -DI kullanmanın sakıncaları var mı? ----------------------------------- - -Bağımlılık enjeksiyonu kullanmak, daha fazla kod yazma çabası ya da düşük başarım gibi sakıncalar getirir mi? DI'ya uygun kod yazmaya başladığımızda ne kaybederiz? - -DI'nın kendisi, çalışma zamanı başarımına ya da bellek kullanımına neredeyse hiç etki etmez. DI container'ının başarımı bir etken olabilir, ama [Nette DI |nette-container] container'ı düz PHP koduna derler; bu da uygulama çalışırken neredeyse sıfır ek yük demektir. - -DI ilkelerine göre kod yazarken, çoğu zaman bağımlılıkları alan yapıcılar oluşturmanız gerekir. Bu geçmişte zahmetli görünmüş olabilir, ama modern IDE'ler ve PHP 8'in [constructor property promotion |https://blog.nette.org/tr/php-8-0-complete-overview-of-news#toc-constructor-property-promotion] gibi özellikleri bunu çok hızlı kılıyor. Factory'ler de çoğu zaman Nette DI tarafından otomatik üretilebilir; bu da tekrar eden kodu daha da azaltır. Buna karşılık singleton ve statik accessor yazma gereğini ortadan kaldırırsınız. - -Genel olarak, DI kullanan iyi tasarlanmış bir uygulama, singleton'lara ya da genel erişime dayanan bir uygulamadan tipik olarak ne belirgin biçimde kısadır ne de uzun. Bağımlılıkların oluşturulması ve bağlanmasıyla ilgili kod, yalnızca tek tek sınıflardan bunun için ayrılmış yerlere taşınır: DI container yapılandırmasına ve factory'lere. - - -Eski bir uygulama DI'ya nasıl dönüştürülür? -------------------------------------------- - -Eski bir uygulamadan bağımlılık enjeksiyonuna geçmek, özellikle büyük ve karmaşık uygulamalarda zorlu bir süreç olabilir. Bu sürece dizgeli yaklaşmak önemlidir. - -- Bağımlılık enjeksiyonuna geçerken, kullanılan ilkeleri ve uygulamaları tüm ekip üyelerinin anlaması önemlidir. -- Önce var olan uygulamayı çözümleyerek temel bileşenleri ve bağımlılıklarını belirleyin. Hangi bölümlerin hangi sırayla yeniden düzenleneceğine dair bir plan yapın. -- Bir DI container gerçekleştirin ya da daha iyisi Nette DI gibi var olan bir kütüphaneyi kullanın. -- Uygulamanın bölümlerini bağımlılık enjeksiyonu kullanacak şekilde kademeli olarak yeniden düzenleyin. Bu, yapıcıları ya da metotları bağımlılıkları parametre olarak alacak şekilde değiştirmeyi içerebilir. -- Nesnelerin örneklendiği kodu, onları container'dan alacak ya da container'ın sunduğu factory'leri kullanacak şekilde güncelleyin. - -Bağımlılık enjeksiyonuna geçişin, kod kalitesine ve uygulamanın uzun vadeli bakımına yapılan bir yatırım olduğunu unutmayın. Bu değişiklikleri yapmak zorlu olabilse de, sonuç; gelecekteki genişletmelere ve bakıma hazır, daha temiz, daha modüler ve kolay test edilebilir bir kod olmalıdır. - - -Kompozisyon neden kalıtıma yeğlenir? ------------------------------------- -Kodu yeniden kullanmada [kompozisyon |nette:introduction-to-object-oriented-programming#Kompozisyon], daha gevşek bağlantıya yol açtığı için genellikle [kalıtıma |nette:introduction-to-object-oriented-programming#Kalıtım] yeğlenir. Kompozisyonla, bir temel sınıfı değiştirmenin ona bağlı alt sınıfları bozması sorunuyla karşılaşma olasılığınız daha düşüktür. Tipik örnek, [constructor hell |passing-dependencies#Constructor Hell] denen durumdur. - - -Nette DI Container, Nette dışında kullanılabilir mi? ----------------------------------------------------- - -Kesinlikle. Nette DI Container, Nette'nin bir parçasıdır, ama framework'ün diğer bölümlerinden bağımsız kullanılabilen ayrı bir kütüphane olarak tasarlanmıştır. Composer ile kurmanız, servislerinizi tanımlayan bir yapılandırma dosyası oluşturmanız ve birkaç satır PHP koduyla DI container'ı yaratmanız yeter. Ardından projelerinizde bağımlılık enjeksiyonundan hemen yararlanmaya başlayabilirsiniz. - -[Nette DI Container |nette-container] bölümü, kod örnekleriyle somut bir kullanım durumunu anlatır. - - -Yapılandırma neden NEON dosyalarında? -------------------------------------- - -NEON, uygulamaları, servisleri ve bağımlılıklarını ayarlamak için Nette içinde geliştirilmiş, basit ve kolay okunur bir yapılandırma dilidir. JSON ya da YAML ile karşılaştırıldığında, bu amaç için çok daha sezgisel ve esnek olanaklar sunar. NEON'da, JSON ya da YAML'de bu kadar anlaşılır biçimde ifade edilmesi zor ya da olanaksız olan servis tanımlarını ve ilişkileri doğal biçimde anlatabilirsiniz. - - -NEON dosyalarının ayrıştırılması uygulamayı yavaşlatır mı? ----------------------------------------------------------- - -NEON dosyaları çok hızlı ayrıştırılsa da, ayrıştırma hızları üretimde büyük ölçüde önemsizdir. Bunun nedeni, yapılandırma dosyalarının yalnızca uygulama ilk çalıştığında (ya da değiştiklerinde) bir kez ayrıştırılmasıdır. Ayrıştırmadan sonra DI container kodu üretilir, önbelleğe alınır (diske yazılır) ve sonraki her istekte bu derlenmiş PHP kodu çalıştırılır; böylece yeniden ayrıştırmaya gerek kalmaz. - -Üretim ortamında bu böyle işler. Geliştirme sırasında NEON dosyaları, içerikleri her değiştiğinde ayrıştırılır; böylece geliştiricinin elinde her zaman güncel bir DI container bulunur. Belirtildiği gibi, ayrıştırmanın kendisi çok hızlıdır. - - -Yapılandırma dosyasındaki parametrelere sınıfımdan nasıl erişirim? ------------------------------------------------------------------- - -[Kural #1: Bırakın size aktarılsın |introduction#Kural #1: Bırakın Size Aktarılsın] ilkesini akılda tutun. Bir sınıfın yapılandırma dosyasındaki bir bilgiye ihtiyacı varsa, sınıfın onu nasıl *alabileceğini* çözmeye çalışmayın. Bunun yerine yalnızca isteyin, örneğin sınıfın yapıcısı üzerinden. Sonra bu değeri yapılandırma dosyasında verin. - -Bu örnekte `%myParameter%`, `MyClass` yapıcısına aktarılacak `myParameter` parametresinin değerinin yer tutucusudur: - -```neon -# config.neon -parameters: - myParameter: Bir değer - -services: - - MyClass(%myParameter%) -``` - -Birden çok parametre aktarmak ya da autowiring kullanmak istiyorsanız, [parametreleri bir nesnenin içine almak |best-practices:passing-settings-to-presenters] yararlıdır. - - -Nette, PSR-11 Container arayüzünü destekliyor mu? -------------------------------------------------- - -[Nette DI Container |api:Nette\DI\Container] PSR-11'i doğrudan desteklemez. Ancak Nette DI Container ile PSR-11 Container arayüzünü bekleyen kütüphaneler ya da framework'ler arasında birlikte çalışabilirlik gerekiyorsa, Nette DI Container ile PSR-11 arasında köprü görevi görecek [basit bir adaptör |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f] yazabilirsiniz. - - -Container, compiler, definition gibi terimler ne anlama geliyor? ----------------------------------------------------------------- - -Nette DI çevresinde, çoğu da [extension yazarken |extensions] sürekli karşınıza çıkan sözcüklerin kısa bir sözlüğü: - -- **Container** - servisleri istendiğinde oluşturan ve çalışma zamanında tutan derlenmiş nesne (`Nette\DI\Container`). Bir kez, iyileştirilmiş PHP kodu olarak üretilir. -- **Compiler** - yapılandırma dosyalarını ve extension'ları bu container sınıfına dönüştüren düzenek. -- **ContainerBuilder** - derleme sırasında kullanılan, container'ın değiştirilebilir modeli; gerçek bir servis var olmadan önce servis tanımlarını tutar. Bkz. [Extension yazma |extensions#ContainerBuilder]. -- **Servis** - container'ın yönettiği, genellikle bir kez oluşturulup paylaşılan (singleton) nesne: veritabanı bağlantısı, mailer, logger. -- **Definition** - bir servisin tarifi: türü, nasıl oluşturulacağı ve sonrasında ne yapılacağı. Nette, tanımları container'ın factory metotlarına dönüştürür; birkaç türü vardır (bkz. [tanım türleri |extensions#Tanım Türleri]). -- **Tür** - servisin sınıfı ya da arayüzü; autowiring, servisleri onlara ihtiyaç duyan yerlerle eşleştirmek için bunu kullanır. -- **Autowiring** - servisleri türlerine göre yapıcılara ve metotlara otomatik olarak aktarma; böylece bağımlılıkları elle bağlamazsınız. -- **Etiket** - bir tanıma iliştirilen etiket (isteğe bağlı olarak bir değerle); bir extension daha sonra onu taşıyan tüm servisleri `findByTag()` ile bulabilir. -- **Setup** - bir servis oluşturulduktan hemen sonra üzerinde yapılan ek işlemler: `addSetup()` ile eklenen metot çağrıları ya da özellik atamaları. -- **Alias** - var olan bir servisin başka bir adı. diff --git a/dependency-injection/tr/global-state.texy b/dependency-injection/tr/global-state.texy deleted file mode 100644 index 282cba33f2..0000000000 --- a/dependency-injection/tr/global-state.texy +++ /dev/null @@ -1,294 +0,0 @@ -Genel Durum ve Singleton'lar -**************************** - -.[perex] -Uyarı: Aşağıdaki yapılar, kötü tasarlanmış kodun belirtileridir: - -- `Foo::getInstance()` -- `DB::insert(...)` -- `Article::setDb($db)` -- `ClassName::$var` ya da `static::$var` - -Bu yapılardan herhangi biri kodunuzda geçiyor mu? Öyleyse iyileştirme fırsatınız var. Bunların yaygın yapılar olduğunu, belki çeşitli kütüphane ve framework'lerin örnek çözümlerinde gördüğünüzü düşünebilirsiniz. Öyleyse o kodların tasarımı kusurludur. - -Burada akademik bir arılıktan söz etmiyoruz. Bu yapıların hepsi ortak bir özellik taşır: genel durumu (global state) kullanırlar. Genel durum ise kod kalitesine zarar verir. Sınıflar bağımlılıkları konusunda yanıltıcı hâle gelir. Kod öngörülemez olur. Geliştiricilerin kafasını karıştırır ve verimliliklerini düşürür. - -Bu bölümde bunun neden böyle olduğunu ve genel durumdan nasıl kaçınılacağını açıklayacağız. - - -Genel Bağlantılar ------------------ - -İdeal bir dünyada bir nesne, yalnızca [kendisine doğrudan aktarılmış |passing-dependencies] nesnelerle iletişim kurmalıdır. `A` ve `B` adında iki nesne oluşturur ve aralarında hiçbir referans aktarmazsam, ne `A` ne de `B` diğerinin durumuna erişebilir ya da onu değiştirebilir. Bu, kodun son derece istenen bir özelliğidir. Elinizde bir pil ve bir ampul olmasına benzer; ampulü telle pile bağlamadıkça yanmaz. - -Ancak bu, genel (statik) değişkenler ya da singleton'lar için geçerli değildir. `A` nesnesi, hiçbir referans aktarımı olmadan `C::changeSomething()` çağırarak `C` nesnesine *kablosuz* erişebilir ve onu değiştirebilir. `B` nesnesi de genel `C` nesnesine bağlanırsa, `A` ile `B` birbirini `C` üzerinden etkileyebilir. - -Genel değişken kullanmak, dışarıdan görünmeyen yeni bir *kablosuz* bağlantı biçimi getirir. Kodu anlamayı ve kullanmayı zorlaştıran bir sis perdesi yaratır. Bağımlılıkları gerçekten kavramak için geliştiricilerin, yalnızca sınıf arayüzlerine güvenmek yerine kaynak kodun her satırını okuması gerekir. Üstelik bu bağlantı tümüyle gereksizdir. Genel durum, her yerden kolayca erişilebilir olduğu ve örneğin genel (statik) bir `DB::insert()` metoduyla veritabanına yazmayı sağladığı için kullanılır. Ancak göstereceğimiz gibi, bu algılanan kolaylık, getirdiği ağır sorunların yanında çok küçüktür. - -.[note] -Davranış açısından genel değişkenle statik değişken arasında fark yoktur. İkisi de aynı ölçüde zararlıdır. - - -Uzaktan Ürkütücü Etki ---------------------- - -"Uzaktan ürkütücü etki" ("spooky action at a distance") - Albert Einstein'ın kuantum fiziğinde kendisini ürperten bir olguya verdiği ünlü ad budur. -Kuantum dolanıklığını anlatır: bir parçacığın bir özelliğinin ölçülmesi, aralarındaki uzaklık milyonlarca ışık yılı bile olsa, dolanık diğer parçacığı anında etkiler; bu da görünüşte evrenin hiçbir şeyin ışıktan hızlı gidemeyeceği temel yasasını çiğner. - -Yazılım dünyasında "uzaktan ürkütücü etki", yalıtılmış olduğunu sandığımız (çünkü açıkça hiçbir bağımlılık aktarılmamıştır) bir işlemi çalıştırdığımızda, sistemin uzak köşelerinde haberimiz olmadan beklenmedik etkileşimlerin ve durum değişikliklerinin olmasını anlatır. Bu ancak genel durumla gerçekleşebilir. - -Büyük ve olgun bir kod tabanı olan bir projede geliştirme ekibine katıldığınızı düşünün. Yeni yöneticiniz sizden yeni bir özellik gerçekleştirmenizi istiyor ve iyi bir geliştirici olarak işe bir test yazarak başlıyorsunuz. Ama projede yeni olduğunuz için "bu metodu çağırsam ne olur" türünden bolca keşif testi yapıyorsunuz. Ve şu testi yazmayı deniyorsunuz: - -```php -function testCreditCardCharge() -{ - $cc = new CreditCard('1234567890123456', 5, 2028); // sizin kart numaranız - $cc->charge(100); -} -``` - -Kodu çalıştırıyorsunuz, belki birkaç kez, ve bir süre sonra telefonunuzda banka bildirimlerini fark ediyorsunuz: her çalıştırmada kredi kartınızdan 100 dolar çekilmiş! 🤦‍♂️ - -Test nasıl olur da gerçek bir çekim yapar? Kredi kartıyla işlem yapmak basit değildir. Üçüncü taraf bir web servisiyle iletişim kurmanız, URL'sini bilmeniz, kimlik doğrulaması yapmanız vb. gerekir. Bu bilgilerin hiçbiri testte yok. Daha kötüsü, bu bilgilerin nerede durduğunu da bilmiyorsunuz; bu yüzden her test çalıştırmasındaki 100 dolarlık çekimi önlemek için dış bağımlılıkları taklit etmek olanaksız. Peki yeni bir geliştirici olarak, yapmak üzere olduğunuz şeyin sizi 100 dolar yoksullaştıracağını nereden bilecektiniz? - -İşte bu, uzaktan ürkütücü etkidir! - -Projenin iç bağlantılarını anlamak için upuzun kaynak kodu taramak ve kıdemli meslektaşlarınıza danışmak zorunda kalırsınız. Bu zorluk, `CreditCard` sınıfının arayüzünün gereken genel durum başlatmasını açığa vurmamasından doğar. Sınıfın kaynak kodunu incelemek bile hangi başlatma metodunun çağrılacağını göstermeyebilir. En iyi durumda, erişilen genel değişkeni bulup onu nasıl başlatacağınızı çıkarsamaya çalışırsınız. - -Böyle bir projedeki sınıflar patolojik yalancılardır. `CreditCard` sınıfı, öylece örneklenip `charge()` metodunun çağrılabileceği havasını verir. Oysa gizlice, ödeme geçidini temsil eden başka bir sınıfla, `PaymentGateway` ile etkileşir. `PaymentGateway` arayüzü bile bağımsız başlatma çağrıştırabilir, ama gerçekte kimlik bilgilerini bir yapılandırma dosyasından çekiyor olabilir vb. Özgün geliştiriciler `CreditCard` sınıfının `PaymentGateway` gerektirdiğini bilir. Kodu böyle yazmışlardır. Ama yeni gelenler için bu tam bir gizemdir ve öğrenmelerini, katkı vermelerini engeller. - -Durum nasıl düzeltilir? Kolay. **Bırakın API bağımlılıkları bildirsin.** - -```php -function testCreditCardCharge() -{ - $gateway = new PaymentGateway(/* ... */); - $cc = new CreditCard('1234567890123456', 5, 2028); - $cc->charge($gateway, 100); -} -``` - -Koddaki karşılıklı bağımlılıkların nasıl anında görünür olduğuna dikkat edin. `charge()` metodu bir `PaymentGateway` gerektirdiğini bildirdiğinden, artık bu bağımlılığı tahmin etmeniz ya da sormanız gerekmez. Bir örnek oluşturmanız gerektiğini bilirsiniz ve bunu yaparken gereken erişim parametrelerini keşfedersiniz. Onlar olmadan kod hiç çalışmazdı. - -Ve en önemlisi, artık ödeme geçidini taklit edebilirsiniz; böylece her test çalıştırmasında 100 dolar ödemezsiniz. - -Genel durum, nesnelerin API'lerinde bildirilmeyen bağımlılıklara gizlice erişmesine izin verir ve API'lerinizi fiilen patolojik yalancılara çevirir. - -Bunu daha önce böyle düşünmemiş olabilirsiniz, ama genel durum kullandığınız her seferde gizli kablosuz iletişim kanalları yaratırsınız. Bu uzaktan ürkütücü etki, geliştiricileri olası etkileşimleri anlamak için her satırı okumaya zorlar; üretkenliği düşürür ve yeni ekip üyelerinin kafasını karıştırır. Kodu siz yazdıysanız gerçek bağımlılıkları bilirsiniz, ama sizden sonra gelen herkes karanlıktadır. - -Genel duruma dayanan kod yazmaktan kaçının; bağımlılıkları açıkça aktarmayı yeğleyin. Bağımlılık enjeksiyonunu benimseyin. - - -Genel Durumun Kırılganlığı --------------------------- - -Genel durum ve singleton kullanan kodda, durumun ne zaman ve kim tarafından değiştirildiğinden asla emin olamazsınız. Bu risk başlatma sırasında bile kendini gösterir. Aşağıdaki kod bir veritabanı bağlantısı kurup bir ödeme geçidini başlatmayı amaçlar, ama sürekli istisna fırlatır ve nedenini bulmak son derece yorucudur: - -```php -PaymentGateway::init(); -DB::init('mysql:', 'user', 'password'); -``` - -`PaymentGateway` nesnesinin başka nesnelere kablosuz eriştiğini, bazılarının da veritabanı bağlantısı gerektirdiğini bulmak için kodu titizlikle izlemeniz gerekir. Dolayısıyla veritabanı `PaymentGateway` nesnesinden önce başlatılmalıdır. Ama genel durumun sis perdesi bunu sizden gizler. Bu sınıfların API'leri dürüst olup bağımlılıklarını bildirseydi ne kadar zaman kazanılırdı? - -```php -$db = new DB('mysql:', 'user', 'password'); -$gateway = new PaymentGateway($db, /* ... */); -``` - -Bir veritabanı bağlantısına genel erişim kullanıldığında da benzer bir sorun doğar: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public function save(): void - { - DB::insert(/* ... */); - } -} -``` - -`save()` metodu çağrıldığında, bir veritabanı bağlantısının kurulup kurulmadığı ya da kurmaktan kimin sorumlu olduğu belirsizdir. Veritabanı bağlantısını dinamik olarak değiştirmemiz gerekirse (örneğin test için), `DB::reconnect(...)` ya da `DB::reconnectForTest()` gibi metotlar eklemeye yönelebiliriz. - -Bir örnek düşünün: - -```php -$article = new Article; -// ... -DB::reconnectForTest(); -Foo::doSomething(); -$article->save(); -``` - -`$article->save()` çağrıldığında test veritabanının gerçekten kullanıldığından nasıl emin olabiliriz? Ya `Foo::doSomething()` metodu genel veritabanı bağlantısını değiştirdiyse? Bunu belirlemek için `Foo` sınıfının ve olasılıkla daha pek çok sınıfın kaynak kodunu incelememiz gerekirdi. Bu araştırma da yalnızca geçici bir yanıt verirdi; çünkü durum sonradan değişebilir. - -Ya veritabanı bağlantısını `Article` sınıfının içindeki statik bir değişkene taşırsak? - -```php -class Article -{ - private static DB $db; - - public static function setDb(DB $db): void - { - self::$db = $db; - } - - public function save(): void - { - self::$db->insert(/* ... */); - } -} -``` - -Bu hiçbir şeyi değiştirmez. Sorun, hangi sınıfın içinde gizlendiğinden bağımsız olarak genel durumun kendisidir. Bu senaryoda da, tıpkı öncekinde olduğu gibi, `$article->save()` çağrıldığında verinin hangi veritabanına yazılacağından emin olamayız. Uygulamanın herhangi bir yerinde herkes, `Article::setDb()` ile veritabanını istediği an değiştirmiş olabilir. Bizim haberimiz olmadan. - -Genel durum, uygulamamızı **son derece kırılgan** kılar. - -Ancak bu sorunla başa çıkmanın basit bir yolu var. Düzgün çalışmayı güvence altına almak için API'nin bağımlılıkları bildirmesini sağlayın. - -```php -class Article -{ - public function __construct( - private DB $db, - ) { - } - - public function save(): void - { - $this->db->insert(/* ... */); - } -} - -$article = new Article($db); -// ... -Foo::doSomething(); -$article->save(); -``` - -Bu yaklaşım, veritabanı bağlantısında gizli ya da beklenmedik değişiklik kaygısını ortadan kaldırır. Makalenin nereye kaydedildiğinden artık eminiz ve ilgisiz sınıflardaki değişiklikler bunu artık etkileyemez. Kod artık kırılgan değil, kararlıdır. - -Genel duruma dayanan kod yazmaktan kaçının; bağımlılıkları açıkça aktarmayı yeğleyin. Bağımlılık enjeksiyonunu benimseyin. - - -Singleton ---------- - -Singleton, ünlü Gang of Four kitabındaki [tanıma |https://en.wikipedia.org/wiki/Singleton_pattern] göre bir sınıfı tek bir örnekle sınırlayan ve ona genel erişim sunan bir tasarım desenidir. Bu desenin gerçekleştirimi genellikle şu koda benzer: - -```php -class Singleton -{ - private static self $instance; - - public static function getInstance(): self - { - self::$instance ??= new self; - return self::$instance; - } - - // ve sınıfın işlevlerini yerine getiren diğer metotlar -} -``` - -Ne yazık ki singleton, uygulamaya genel durum katar. Ve yukarıda gösterdiğimiz gibi genel durum istenmeyen bir şeydir. Bu yüzden singleton bir anti-desen sayılır. - -Kodunuzda singleton kullanmayın, onları başka düzeneklerle değiştirin. Singleton'lara gerçekten ihtiyacınız yok. Yine de uygulama boyunca bir sınıftan yalnızca bir örnek bulunmasını sağlamanız gerekiyorsa, bu sorumluluğu [DI container'a |container] devredin. Bu, yaygın olarak servis diye anılan, uygulama kapsamında bir singleton oluşturur. Sınıfın kendisi böylece kendi biricikliğini yönetmekten kurtulur (yani `getInstance()` metodu ya da statik örnek özelliği olmaz) ve yalnızca kendi sorumluluklarına odaklanabilir. Böylece tek sorumluluk ilkesini çiğnemeyi bırakır. - - -Genel Durum ve Testler ----------------------- - -Test yazarken ideal olarak her testin yalıtılmış bir birim olduğunu, ona dışarıdan durum girmediğini ve ondan durum çıkmadığını varsayarız. Bir test bittikten sonra onunla ilgili her durum çöp toplayıcı tarafından otomatik olarak temizlenmelidir. Testleri yalıtılmış kılan budur. Böylece testleri istediğimiz sırayla çalıştırabiliriz. - -Ancak genel durumlar ya da singleton'lar varsa, bu yararlı varsayımlar çöker. Durum testlere sızabilir ve testlerden dışarı çıkabilir. Aniden testlerin sırası önem kazanabilir. - -Singleton içeren kodu test edebilmek için bile geliştiriciler çoğu zaman bütünlükten ödün vermek zorunda kalır; örneğin singleton örneğinin değiştirilebilmesine izin vererek. Böyle çözümler en iyi durumda birer hiledir ve bakımı ile anlaşılması zor koda yol açar. Genel durumu değiştiren her test (ya da onun `tearDown()` metodu), bu değişiklikleri titizlikle geri almalıdır. - -Genel durum, birim testlerdeki en büyük baş ağrısıdır! - -Bu nasıl düzeltilir? Basit. Singleton kullanan kod yazmaktan kaçının; bağımlılıkları açıkça aktarmayı yeğleyin. Bağımlılık enjeksiyonunu benimseyin. - - -Genel Sabitler --------------- - -Genel durum, singleton ve statik değişken kullanımıyla sınırlı değildir; genel sabitler için de geçerli olabilir. - -Değerleri evrensel gerçekleri temsil eden (`M_PI`) ya da kendi içinde tam bilgi veren (`PREG_BACKTRACK_LIMIT_ERROR`) sabitler genellikle kabul edilebilir. Buna karşılık, koda *kablosuz* bilgi enjekte etmenin bir yolu olarak kullanılan sabitler aslında gizli bağımlılıklardır. Aşağıdaki örnekteki `LOG_FILE` gibi. `FILE_APPEND` sabitini kullanmak ise tümüyle doğrudur. - -```php -const LOG_FILE = '...'; - -class Foo -{ - public function doSomething() - { - // ... - file_put_contents(LOG_FILE, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Bunun yerine günlük dosyasının yolunu `Foo` sınıfının yapıcısında bir parametre olarak bildirmeli ve böylece API'sinin açık bir parçası yapmalıyız: - -```php -class Foo -{ - public function __construct( - private string $logFile, - ) { - } - - public function doSomething() - { - // ... - file_put_contents($this->logFile, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Artık günlük dosyasının yolunu açıkça aktarıyoruz. Onu gerektiğinde kolayca değiştirebiliriz; bu da testi ve kod bakımını kolaylaştırır. - - -Genel Fonksiyonlar ve Statik Metotlar -------------------------------------- - -Statik metot ve genel fonksiyon kullanmanın özünde sorunlu olmadığını vurgulamak isteriz. `DB::insert()` gibi metotlardaki sorunları açıkladık, ama asıl sorun her zaman alttaki genel durumdu; tipik olarak statik bir değişkende saklanan durum. `DB::insert()` metodu, veritabanı bağlantısını tutmak için statik bir değişkene dayanır. Bu değişken olmasa metodu gerçekleştirmek olanaksız olurdu. - -`Closure::fromCallable()`, `strlen()` gibi belirlenimci statik metotları ve fonksiyonları kullanmak, bağımlılık enjeksiyonuyla tümüyle bağdaşır. Bu fonksiyonlar öngörülebilirdir; çünkü aynı girdi parametreleri için her zaman aynı sonucu döndürürler. Hiçbir genel durum kullanmazlar. - -Ancak PHP'de belirlenimci olmayan fonksiyonlar da vardır. Örneğin `htmlspecialchars()` fonksiyonu. Üçüncü parametresi `$encoding`, atlanırsa varsayılan olarak `default_charset` yapılandırma seçeneğinin değerini (`ini_get('default_charset')`) alır. Bu yüzden olası öngörülemez davranışı önlemek için bu parametreyi her zaman belirtmek önerilir. Nette bunu tutarlı biçimde yapar. - -`strtolower()` ve `strtoupper()` gibi bazı fonksiyonlar yakın geçmişte, yerel ayara (`setlocale()`) bağlı olarak belirlenimci olmayan davranış gösteriyordu. Bu, çoğu zaman Türkçeyle çalışırken pek çok soruna yol açtı. Çünkü Türkçe, hem küçük hem büyük harfte noktalı ve noktasız "I" harfini ayırır. Sonuç olarak `strtolower('I')` `ı` (noktasız küçük i), `strtoupper('i')` ise `İ` (noktalı büyük I) döndürüyordu; bu da sayısız gizemli uygulama hatasına yol açtı. Ancak bu sorun PHP 8.2 sürümünde düzeltildi ve fonksiyonlar artık yerel ayara bağlı değil. - -Bu, genel durumun (yerel ayarın) dünya çapında binlerce geliştiriciyi nasıl uğraştırdığının iyi bir örneğidir. Nihai çözüm, fonksiyonları yerel ayardan bağımsız kılmak, yani gizli bağımlılığı kaldırmak oldu. - - -Genel Durum Ne Zaman Kullanılabilir? ------------------------------------- - -Genel durum kullanmanın kabul edilebilir olduğu belirli, sınırlı durumlar vardır. Örneğin hata ayıklama sırasında bir değişkenin değerini dökmeniz ya da belirli bir kod parçasının yürütme süresini ölçmeniz gerektiğinde. Sonradan koddan kaldırılacak geçici işlemleri kapsayan bu durumlarda, her yerden erişilebilir bir dumper ya da zamanlayıcı kullanmak meşru olabilir. Bu araçlar uygulamanın temel tasarımının parçası değildir. - -Bir başka örnek de, derlenmiş düzenli ifadeleri içeride statik bellekte önbelleğe alan PHP'nin düzenli ifade fonksiyonlarıdır (`preg_*`). Kodunuzda aynı düzenli ifadeyle bu fonksiyonları defalarca çağırdığınızda, ifade yalnızca bir kez derlenir. Bu önbellekleme başarımı artırır ve kullanıcı için tümüyle görünmezdir; bu yüzden iç statik durumun bu kullanımı genellikle kabul edilebilir. - - -Özet ----- - -Şunları yapmanın neden anlamlı olduğunu ele aldık: - -1) Kodunuzdaki tüm değiştirilebilir statik özellikleri (genel durumu) ortadan kaldırmak -2) Bağımlılıkları açıkça bildirmek -3) Ve bağımlılık enjeksiyonundan yararlanmak - -Kodunuzu tasarlarken, her değiştirilebilir `static $foo` değişkeninin olası bir sorun kaynağı olduğunu unutmayın. DI dostu bir ortam yaratmak için genel durumu tümüyle ortadan kaldırıp yerine bağımlılık enjeksiyonunu koymak can alıcı önemdedir. - -Bu süreçte, birden çok sorumluluğu olan sınıfları bölme ihtiyacını keşfedebilirsiniz. Bunu yapmaktan çekinmeyin; tek sorumluluk ilkesini hedefleyin. - -*Bu bölümün temelini oluşturan [Flaw: Brittle Global State & Singletons |https://web.archive.org/web/20230321084133/http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/] gibi yazıları için Miško Hevery'ye teşekkür ederim.* diff --git a/dependency-injection/tr/introduction.texy b/dependency-injection/tr/introduction.texy deleted file mode 100644 index 244ab19e79..0000000000 --- a/dependency-injection/tr/introduction.texy +++ /dev/null @@ -1,525 +0,0 @@ -Bağımlılık Enjeksiyonu Nedir? -***************************** - -.[perex] -Bu bölüm, herhangi bir uygulama yazarken izlemeniz gereken temel programlama uygulamalarını tanıtıyor. Bunlar; temiz, anlaşılır ve bakımı kolay kod yazmak için gereken temellerdir. - -Bu kuralları benimseyip izlerseniz, Nette her adımda size destek olur. Rutin işleri sizin yerinize üstlenir ve en yüksek rahatlığı sağlar; böylece siz mantığın kendisine odaklanabilirsiniz. - -Burada göstereceğimiz ilkeler oldukça basit. Korkulacak bir şey yok. - - -İlk Programınızı Hatırlıyor musunuz? ------------------------------------- - -Onu hangi dilde yazdığınızı bilmiyoruz, ama PHP olsaydı muhtemelen şöyle görünürdü: - -```php -function addition(float $a, float $b): float -{ - return $a + $b; -} - -echo addition(23, 1); // 24 yazdırır -``` - -Birkaç önemsiz satır kod, ama içlerinde ne çok temel kavram saklı. Değişkenlerin var olduğu. Kodun fonksiyon gibi daha küçük birimlere bölündüğü. Onlara girdi argümanları aktardığımız ve sonuç döndürdükleri. Yalnızca koşullar ve döngüler eksik. - -Bir fonksiyona girdi verisi aktarmamız ve onun sonuç döndürmesi, matematik gibi başka alanlarda da kullanılan, gayet anlaşılır bir kavramdır. - -Bir fonksiyonun imzası vardır; adı, parametre listesi ile türleri ve son olarak dönüş değerinin türünden oluşur. Kullanıcı olarak bizi imza ilgilendirir; iç gerçekleştirim hakkında genellikle bir şey bilmemize gerek yoktur. - -Şimdi fonksiyon imzasının şöyle olduğunu düşünün: - -```php -function addition(float $x): float -``` - -Tek parametreli toplama? Bu tuhaf… Peki ya bu? - -```php -function addition(): float -``` - -Bu artık gerçekten tuhaf, değil mi? Fonksiyon nasıl kullanılıyor? - -```php -echo addition(); // ne yazdıracak? -``` - -Böyle bir koda bakınca kafamız karışırdı. Yalnızca yeni başlayan biri değil, becerikli bir programcı bile böyle bir kodu anlamazdı. - -Böyle bir fonksiyonun içeriden nasıl göründüğünü merak ediyor musunuz? Toplayacağı sayıları nereden alırdı? Muhtemelen onları *bir şekilde* kendisi bulurdu, belki şöyle: - -```php -function addition(): float -{ - $a = Input::get('a'); - $b = Input::get('b'); - return $a + $b; -} -``` - -Fonksiyonun gövdesinde, başka genel fonksiyonlara ya da statik metotlara gizli bağımlılıklar keşfettik. Sayıların gerçekten nereden geldiğini bulmak için daha derine inmemiz gerekir. - - -Böyle Olmaz! ------------- - -Az önce gösterdiğimiz tasarım, pek çok olumsuz özelliğin özüdür: - -- Fonksiyonun imzası, toplayacağı sayılara ihtiyacı yokmuş gibi davrandı ve kafamızı karıştırdı. -- Fonksiyona iki farklı sayı toplatmayı nasıl yapacağımıza dair hiçbir fikrimiz yok. -- Sayıları nereden aldığını bulmak için koda bakmak zorunda kaldık. -- Gizli bağımlılıklar keşfettik. -- Tümüyle anlamak için bu bağımlılıkları da incelemek gerekiyor. - -Peki girdileri elde etmek toplama fonksiyonunun işi mi? Elbette hayır. Onun sorumluluğu yalnızca toplamanın kendisidir. - - -Böyle bir kodla karşılaşmak istemeyiz ve kesinlikle böyle kod yazmak da istemeyiz. Düzeltmesi basit: temele dönün ve yalnızca parametreleri kullanın: - - -```php -function addition(float $a, float $b): float -{ - return $a + $b; -} -``` - - -Kural #1: Bırakın Size Aktarılsın ---------------------------------- - -En önemli kural şudur: **fonksiyonların ya da sınıfların ihtiyaç duyduğu tüm veriler onlara verilmelidir**. - -Veriyi elde etmeleri için gizli yollar icat etmek yerine yalnızca parametreleri verin. Kodunuzu kesinlikle iyileştirmeyecek gizli yollar icat etmek için harcanan zamandan tasarruf edersiniz. - -Bu kurala her yerde her zaman uyarsanız, gizli bağımlılıkları olmayan bir koda giden yoldasınız demektir. Yalnızca yazarı için değil, sonradan okuyan herkes için anlaşılır bir koda. Her şeyin fonksiyon ve sınıf imzalarından anlaşıldığı, gerçekleştirimde gizli ayrıntılar aramaya gerek olmayan bir koda. - -Bu tekniğin uzmanlık dilindeki adı **Dependency Injection** (bağımlılık enjeksiyonu). Veriler ise **bağımlılık** diye anılır. Bu yalnızca düz parametre aktarımıdır, fazlası değil. - -.[note] -Lütfen bir tasarım deseni olan Dependency Injection'ı, bir araç olan ve kökten farklı bir şey olan "Dependency Injection container" ile karıştırmayın. Container'ları sonra ele alacağız. - - -Fonksiyonlardan Sınıflara -------------------------- - -Peki bu sınıflara nasıl uygulanır? Bir sınıf, basit bir fonksiyondan daha karmaşık bir varlıktır, ama Kural #1 burada da tümüyle geçerlidir. Yalnızca [argüman aktarmanın daha fazla yolu |passing-dependencies] vardır. Örneğin fonksiyon durumuna oldukça benzer biçimde: - -```php -class Math -{ - public function sum(float $a, float $b): float - { - return $a + $b; - } -} - -$math = new Math; -echo $math->sum(23, 1); // 24 -``` - -Ya da başka metotlarla veya doğrudan yapıcıyla: - -```php -class Sum -{ - public function __construct( - private float $a, - private float $b, - ) { - } - - public function calculate(): float - { - return $this->a + $this->b; - } -} - -$sum = new Sum(23, 1); -echo $sum->calculate(); // 24 -``` - -Her iki örnek de Dependency Injection ile tümüyle uyumludur. - - -Gerçek Hayattan Örnekler ------------------------- - -Gerçek dünyada sayı toplayan sınıflar yazmayacaksınız. Pratik örneklere geçelim. - -Bir blog makalesini temsil eden bir `Article` sınıfımız olsun: - -```php -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - // makaleyi veritabanına kaydet - } -} -``` - -kullanımı da şöyle olsun: - -```php -$article = new Article; -$article->title = 'Kilo vermek hakkında bilmeniz gereken 10 şey'; -$article->content = 'Her yıl milyonlarca insan ...'; -$article->save(); -``` - -`save()` metodu makaleyi bir veritabanı tablosuna kaydeder. Onu [Nette Database |database:] ile gerçekleştirmek dolaysız olurdu, tek bir püf noktası olmasa: `Article` veritabanı bağlantısını, yani `Nette\Database\Connection` sınıfından bir nesneyi nereden alacak? - -Görünüşe göre pek çok seçeneğimiz var. Onu statik bir değişkenden alabilir. Ya da veritabanı bağlantısını sunan bir sınıftan türeyerek. Ya da bir [singleton |global-state#Singleton] kullanarak. Ya da Laravel'de kullanılan facade'ları: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - DB::insert( - 'INSERT INTO articles (title, content) VALUES (?, ?)', - [$this->title, $this->content], - ); - } -} -``` - -Harika, sorunu çözdük. - -Öyle mi? - -[#Kural #1: Bırakın Size Aktarılsın] kuralını anımsayalım: sınıfın ihtiyaç duyduğu tüm bağımlılıklar ona aktarılmalıdır. Çünkü kuralı çiğnersek, gizli bağımlılıklarla dolu, anlaşılmaz bir kodun yoluna girmiş oluruz; sonuç da bakımı ve geliştirilmesi baş belası bir uygulama olur. - -`Article` sınıfının kullanıcısının, `save()` metodunun makaleyi nereye sakladığına dair hiçbir fikri yoktur. Bir veritabanı tablosuna mı? Hangisine, üretim mi test veritabanına mı? Ve bu nasıl değiştirilebilir? - -Kullanıcının, `save()` metodunun nasıl gerçekleştirildiğine bakması gerekir ve `DB::insert()` metodunun kullanıldığını görür. Yani bu metodun veritabanı bağlantısını nasıl elde ettiğini daha da araştırması gerekir. Gizli bağımlılıklar da epey uzun bir zincir oluşturabilir. - -Temiz ve iyi tasarlanmış kodda gizli bağımlılık, Laravel facade'ları ya da statik değişken asla bulunmaz. Temiz ve iyi tasarlanmış kodda argümanlar verilir: - -```php -class Article -{ - public function save(Nette\Database\Connection $db): void - { - $db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -Sonra göreceğimiz gibi, yapıcıyı kullanmak daha da pratiktir: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function save(): void - { - $this->db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -.[note] -Deneyimli bir programcıysanız, `Article` sınıfının hiç `save()` metodu olmaması gerektiğini; salt bir veri yapısını temsil etmesi ve kaydetmeyi ayrı bir repository'nin üstlenmesi gerektiğini düşünebilirsiniz. Bu mantıklı. Ama bu bizi konunun, yani bağımlılık enjeksiyonunun ve basit örnekler verme amacının epey dışına taşırdı. - -Çalışması için örneğin veritabanı gerektiren bir sınıf yazıyorsanız, onu nereden alacağınızı icat etmeyin, size aktarılmasını sağlayın. Belki yapıcının ya da başka bir metodun parametresi olarak. Bağımlılıkları kabul edin. Onları sınıfınızın API'sinde kabul edin. Anlaşılır ve öngörülebilir bir kod elde edersiniz. - -Peki hata mesajlarını günlükleyen şu sınıf: - -```php -class Logger -{ - public function log(string $message): void - { - $file = LOG_DIR . '/log.txt'; - file_put_contents($file, $message . "\n", FILE_APPEND); - } -} -``` - -Sizce [#Kural #1: Bırakın Size Aktarılsın] kuralına uyduk mu? - -Uymadık. - -Anahtar bilgi, yani günlük dosyasını içeren dizin, *sınıfın kendisi tarafından* bir sabitten elde ediliyor. - -Kullanım örneğine bakın: - -```php -$logger = new Logger; -$logger->log('Sıcaklık 23 °C'); -$logger->log('Sıcaklık 10 °C'); -``` - -Gerçekleştirimi bilmeden, mesajların nereye yazıldığını söyleyebilir miydiniz? Çalışması için `LOG_DIR` sabitinin var olması gerektiği aklınıza gelir miydi? Ve başka bir yere yazacak ikinci bir örnek oluşturabilir miydiniz? Kesinlikle hayır. - -Sınıfı düzeltelim: - -```php -class Logger -{ - public function __construct( - private string $file, - ) { - } - - public function log(string $message): void - { - file_put_contents($this->file, $message . "\n", FILE_APPEND); - } -} -``` - -Sınıf artık çok daha anlaşılır, yapılandırılabilir ve dolayısıyla daha yararlı. - -```php -$logger = new Logger('/path/to/log.txt'); -$logger->log('Sıcaklık 15 °C'); -``` - - -Ama Beni İlgilendirmiyor! -------------------------- - -*"Bir Article nesnesi oluşturup save() çağırdığımda veritabanıyla uğraşmak istemiyorum; yalnızca yapılandırdığım veritabanına kaydedilsin istiyorum."* - -*"Logger kullandığımda mesajın yazılmasını istiyorum, nereye yazıldığıyla uğraşmak istemiyorum. Genel ayarlar kullanılsın."* - -Bunlar geçerli noktalar. - -Örnek olarak, bülten dağıtan ve sonucu günlükleyen bir sınıf gösterelim: - -```php -class NewsletterDistributor -{ - public function distribute(): void - { - $logger = new Logger(/* ... */); - try { - $this->sendEmails(); - $logger->log('E-postalar gönderildi'); - - } catch (Exception $e) { - $logger->log('Gönderim sırasında hata oluştu'); - throw $e; - } - } -} -``` - -Artık `LOG_DIR` sabitini kullanmayan iyileştirilmiş `Logger`, yapıcıda dosya yolunu ister. Bu nasıl çözülür? `NewsletterDistributor` sınıfı mesajların nereye yazıldığıyla ilgilenmez; yalnızca günlüklenmesini ister. - -Çözüm yine [#Kural #1: Bırakın Size Aktarılsın]: sınıfın ihtiyaç duyduğu tüm verileri aktarırız. - -Peki bu, günlük yolunu yapıcı üzerinden aktarıp `Logger` nesnesini oluştururken kullanacağımız anlamına mı gelir? - -```php -class NewsletterDistributor -{ - public function __construct( - private string $file, // ⛔ BÖYLE DEĞİL! - ) { - } - - public function distribute(): void - { - $logger = new Logger($this->file); -``` - -Böyle değil! Çünkü yol, `NewsletterDistributor` sınıfının ihtiyaç duyduğu bir veri **değildir**; ona `Logger` ihtiyaç duyar. Farkı görüyor musunuz? `NewsletterDistributor` sınıfının ihtiyacı olan şey logger'ın kendisidir. Öyleyse logger'ın kendisini aktaracağız: - -```php -class NewsletterDistributor -{ - public function __construct( - private Logger $logger, // ✅ - ) { - } - - public function distribute(): void - { - try { - $this->sendEmails(); - $this->logger->log('E-postalar gönderildi'); - - } catch (Exception $e) { - $this->logger->log('Gönderim sırasında hata oluştu'); - throw $e; - } - } -} -``` - -Artık `NewsletterDistributor` sınıfının imzasından, günlüklemenin onun işlevinin bir parçası olduğu anlaşılıyor. Logger'ı, belki test için, bir başkasıyla değiştirme işi de tümüyle dolaysız. Üstelik `Logger` sınıfının yapıcısı değişse bile bunun sınıfımıza etkisi olmaz. - - -Kural #2: Kendine Ait Olanı Al ------------------------------- - -Kafanız karışmasın ve bağımlılıklarınızın bağımlılıklarını kabul etmeyin. Yalnızca kendi bağımlılıklarınızı kabul edin. - -Bu sayede başka nesneleri kullanan kod, onların yapıcılarındaki değişikliklerden tümüyle bağımsız olur. API'si daha isabetli olur. Ve en önemlisi, bu bağımlılıkları başkalarıyla değiştirmek dolaysız hâle gelir. - - -Aileye Yeni Katılan -------------------- - -Geliştirme ekibi ikinci bir logger, veritabanına yazan bir logger yapmaya karar verdi. Böylece bir `DatabaseLogger` sınıfı oluşturuyoruz. Artık iki sınıfımız var, `Logger` ve `DatabaseLogger`; biri dosyaya, öteki veritabanına yazıyor… adlandırma biraz tuhaf görünmüyor mu? `Logger` adını `FileLogger` yapmak daha iyi olmaz mıydı? Kesinlikle. - -Ama bunu akıllıca yapalım. Özgün adı kullanarak bir arayüz oluşturuyoruz: - -```php -interface Logger -{ - function log(string $message): void; -} -``` - -… her iki logger da bunu uygulayacak: - -```php -class FileLogger implements Logger -// ... - -class DatabaseLogger implements Logger -// ... -``` - -Ve bu sayede, logger'ın kullanıldığı kodun geri kalanında hiçbir şeyi değiştirmeye gerek kalmayacak. Örneğin `NewsletterDistributor` sınıfının yapıcısı parametre olarak `Logger` istemekle yetinmeyi sürdürecek. Ona hangi örneği vereceğimiz de bize kalacak. - -**Bu yüzden arayüz adlarına asla `Interface` son ekini ya da `I` önekini eklemeyiz.** Aksi hâlde kodu bu kadar şık genişletmek olanaklı olmazdı. - - -Houston, Bir Sorunumuz Var --------------------------- - -Uygulamanın tamamında, ister dosya ister veritabanı tabanlı olsun, tek bir logger örneğiyle idare edip onu günlükleme yapılan her yere aktarabilirken, `Article` sınıfında durum epey farklıdır. Onun örneklerini gerektikçe, hatta defalarca oluştururuz. Yapıcısındaki veritabanı bağımlılığını nasıl ele alırız? - -Örnek olarak, bir form gönderildikten sonra makaleyi veritabanına kaydetmesi gereken bir controller düşünelim: - -```php -class EditController extends Controller -{ - public function formSubmitted($data) - { - $article = new Article(/* ... */); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Olası bir çözüm açık görünüyor: veritabanı nesnesini yapıcı üzerinden `EditController` sınıfına aktaralım ve `$article = new Article($this->db)` kullanalım. - -Tıpkı `Logger` ile dosya yolunu içeren önceki durumda olduğu gibi, bu doğru yaklaşım değildir. Veritabanı, `EditController` sınıfının değil, `Article` sınıfının bağımlılığıdır. Veritabanını aktarmak böylece [Kural #2: Kendine ait olanı al |#Kural #2: Kendine Ait Olanı Al] kuralını çiğner. `Article` sınıfının yapıcısı değişirse (yeni bir parametre eklenirse), örneklerin oluşturulduğu tüm yerlerde kodu değiştirmeniz gerekir. Of. - -Houston, öneriniz nedir? - - -Kural #3: Bırakın Factory Halletsin ------------------------------------ - -Gizli bağımlılıkları ortadan kaldırıp tüm bağımlılıkları argüman olarak aktararak daha yapılandırılabilir ve esnek sınıflar elde ettik. Bu yüzden bu daha esnek sınıfları bizim yerimize oluşturup yapılandıracak fazladan bir şeye ihtiyacımız var. Bunlara factory diyeceğiz. - -Kural şudur: Bir sınıfın bağımlılıkları varsa, örneklerinin oluşturulmasını bir factory'ye devredin. - -Factory'ler, bağımlılık enjeksiyonu dünyasında `new` operatörünün daha akıllı alternatifidir. - -.[note] -Lütfen bunu, factory kullanımının belirli bir biçimini anlatan ve bu konuyla ilgisi olmayan *factory method* tasarım deseniyle karıştırmayın. - - -Factory -------- - -Factory, nesneleri oluşturup yapılandıran bir metot ya da sınıftır. `Article` üreten sınıfa `ArticleFactory` diyeceğiz ve şöyle görünebilir: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Controller'daki kullanımı şöyle olacak: - -```php -class EditController extends Controller -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function formSubmitted($data) - { - // bırakın nesneyi factory oluştursun - $article = $this->articleFactory->create(); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Bu noktada, `Article` sınıfının yapıcı imzası değişirse, tepki vermesi gereken tek kod parçası `ArticleFactory` sınıfının kendisidir. `Article` nesneleriyle etkileşen diğer tüm kod, örneğin `EditController`, etkilenmez. - -Şimdi kafanızı kaşıyıp durumu gerçekten iyileştirip iyileştirmediğimizi merak ediyor olabilirsiniz. Kod miktarı arttı ve her şey kuşku uyandıracak kadar karmaşık görünmeye başladı. - -Merak etmeyin, birazdan Nette DI container'a geleceğiz. Ve onun, bağımlılık enjeksiyonu kullanan uygulamalar kurmayı büyük ölçüde kolaylaştıracak birkaç numarası var. Örneğin `ArticleFactory` sınıfı yerine [yalnızca bir arayüz yazmak |factory] yetecek: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Ama kendimizden ileri gidiyoruz, bizi izlemeye devam edin :-) - - -Özet ----- - -Bu bölümün başında, temiz kod tasarlamanın bir yolunu göstereceğimizi söylemiştik. Yalnızca sınıfların şunları sağladığından emin olun: - -1) [ihtiyaç duydukları bağımlılıklar onlara aktarılsın |#Kural #1: Bırakın Size Aktarılsın] -2) [ve tersine, doğrudan ihtiyaç duymadıkları şeyler onlara aktarılmasın |#Kural #2: Kendine Ait Olanı Al] -3) [ve bağımlılıkları olan nesneler en iyi factory'lerde oluşturulsun |#Kural #3: Bırakın Factory Halletsin] - -İlk bakışta belli olmayabilir, ama bu üç kuralın uzun erimli sonuçları vardır. Kod tasarımına kökten farklı bir bakışa yol açarlar. Buna değer mi? Eski alışkanlıklarını bırakıp bağımlılık enjeksiyonunu tutarlı biçimde kullanmaya başlayan programcılar, bu adımı meslek hayatlarının belirleyici anı sayarlar. Onlara anlaşılır ve bakımı kolay uygulamalar dünyasının kapısını açtı. - -Peki kod bağımlılık enjeksiyonunu tutarlı biçimde kullanmıyorsa ne olur? Statik metotlar ya da singleton'lar üzerine kurulmuşsa? Bu sorunlara yol açar mı? [Evet, hem de çok ciddi sorunlara |global-state]. diff --git a/dependency-injection/tr/nette-container.texy b/dependency-injection/tr/nette-container.texy deleted file mode 100644 index 681739f236..0000000000 --- a/dependency-injection/tr/nette-container.texy +++ /dev/null @@ -1,117 +0,0 @@ -Nette DI Container -****************** - -.[perex] -Nette DI, Nette'nin en ilginç kütüphanelerinden biridir. Son derece hızlı ve şaşırtıcı derecede kolay yapılandırılan, derlenmiş DI container'ları üretebilir ve otomatik olarak güncelleyebilir. - -DI container'ın oluşturması gereken servislerin biçimi genellikle [NEON biçimindeki|neon:format] yapılandırma dosyalarıyla tanımlanır. [Önceki bölümde|container] elle oluşturduğumuz container şöyle yazılırdı: - -```neon -parameters: - db: - dsn: 'mysql:' - user: root - password: '***' - -services: - - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - - ArticleFactory - - EditController -``` - -Söz dizimi çok derli topludur. - -`ArticleFactory` ve `EditController` sınıflarının yapıcılarında bildirilen tüm bağımlılıklar, [autowiring|autowiring] sayesinde Nette DI tarafından bulunur ve otomatik olarak aktarılır; dolayısıyla yapılandırma dosyasında bir şey belirtmeye gerek yoktur. Böylece parametreler değişse bile yapılandırmada bir şey değiştirmeniz gerekmez. Geliştirme sırasında Nette container'ı otomatik olarak yeniden üretir. Yalnızca uygulama geliştirmeye odaklanabilirsiniz. - -Bağımlılıkları setter'larla aktarmak istersek, bunun için [setup |services#Setup] bölümünü kullanırız. - -Nette DI, container için doğrudan PHP kodu üretir. Sonuç, açıp inceleyebileceğiniz bir `.php` dosyasıdır. Böylece container'ın tam olarak nasıl çalıştığını görebilirsiniz. Onu IDE'nizde hata ayıklayabilir ve adım adım izleyebilirsiniz. Ve en önemlisi: üretilen PHP kodu son derece hızlıdır. - -Nette DI, verilen bir arayüze dayanarak [factory|factory] kodu da üretebilir. Bu yüzden `ArticleFactory` sınıfı yerine uygulamada yalnızca bir arayüz oluşturmamız yeterlidir: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Tam örneği [GitHub'da|https://github.com/nette-examples/di-example-doc] bulabilirsiniz. - - -Bağımsız Kullanım ------------------ - -Nette DI kütüphanesini bir uygulamaya katmak çok kolaydır. Önce onu Composer ile kuruyoruz (çünkü zip dosyası indirmek çok demode): - -```shell -composer require nette/di -``` - -Aşağıdaki kod, `config.neon` dosyasında saklanan yapılandırmaya göre bir DI container örneği oluşturmak için [Compiler |api:Nette\DI\Compiler] kullanır: - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp'); -$class = $loader->load(function ($compiler) { - $compiler->loadConfig(__DIR__ . '/config.neon'); -}); -$container = new $class; -``` - -Container yalnızca bir kez üretilir, kodu önbelleğe (`__DIR__ . '/temp'` dizinine) yazılır ve sonraki isteklerde yalnızca oradan yüklenir. - -`Compiler` kendi başına yapılandırmada yalnızca `services` ve `parameters` bölümlerini etkinleştirir. Diğerlerini (`search`, `decorator`, `di` ya da `inject` gibi) kullanmak için önce ilgili extension'ları kaydedin. Yapılandırmanın `extensions` bölümünden extension kaydedebilmek için de `ExtensionsExtension` ekleyin: - -```php -$compiler->addExtension('search', new Nette\DI\Extensions\SearchExtension($tempDir)); -$compiler->addExtension('extensions', new Nette\DI\Extensions\ExtensionsExtension); -``` - -Tam Nette uygulamalarında kullanılan [Configurator |application:bootstrapping] bunların hepsini otomatik olarak kaydeder. - -Aynı önbellek dizininde birkaç farklı container tutuyorsanız, `load()` metoduna ikinci argüman olarak verilen bir anahtarla onları ayırt edin; bu anahtar üretilen sınıf adının bir parçası olur: - -```php -$class = $loader->load( - fn($compiler) => $compiler->loadConfig(__DIR__ . '/config.neon'), - 'my-key', -); -``` - -Servisleri oluşturmak ve almak için `getService()` ya da `getByType()` metotları kullanılır. `EditController` nesnesini şöyle oluştururuz: - -```php -$controller = $container->getByType(EditController::class); -$controller->someMethod(); -``` - -Geliştirme sırasında, herhangi bir sınıf ya da yapılandırma dosyası değiştiğinde container'ın kendiliğinden yeniden üretildiği otomatik yenileme kipini açmak yararlıdır. [ContainerLoader |api:Nette\DI\ContainerLoader] yapıcısında ikinci argüman olarak `true` vermeniz yeterlidir. - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp', true); -``` - - -Container ile Çalışma ---------------------- - -`getService()` ve `getByType()` dışında container nesnesi başka kullanışlı metotlar da sunar: - -- `getByType(string $type, bool $throw = true): ?object` verilen türdeki servisi döndürür. İkinci argüman olarak `false` verirseniz, böyle bir servis yoksa istisna fırlatmak yerine `null` döndürür. -- `hasService(string $name): bool` ve `isCreated(string $name): bool` bir servisin tanımlı olup olmadığını ve örneklenip örneklenmediğini söyler. -- `getParameters(): array` container'ın tüm parametrelerini, `getParameter($key)` ise tek bir parametreyi döndürür. -- `createInstance(string $class, array $args = []): object` verilen sınıftan yeni bir örnek oluşturur ve yapıcı bağımlılıklarını autowiring ile aktarır. -- `callMethod(callable $function, array $args = []): mixed` verilen callable'ı çağırır ve argümanlarını autowiring ile aktarır. -- `callInjects(object $service): void` verilen nesnedeki tüm `inject*()` metotlarını çağırır ve onlara bağımlılıkları aktarır. - -Container yapıcısı ayrıca, yapılandırmada tanımlananları tamamlayan bir parametre dizisi de kabul eder: - -```php -$container = new $class(['host' => 'localhost']); -``` - - -Nette Framework ile Kullanım ----------------------------- - -Gösterdiğimiz gibi, Nette DI kullanımı Nette Framework ile kurulan uygulamalarla sınırlı değildir; onu yalnızca üç satır kodla her yere katabilirsiniz. Ancak Nette Framework kullanarak uygulama geliştiriyorsanız, container'ın yapılandırılmasını ve oluşturulmasını [Bootstrap |application:bootstrapping#DI konteynerinin yapılandırması] üstlenir. diff --git a/dependency-injection/tr/passing-dependencies.texy b/dependency-injection/tr/passing-dependencies.texy deleted file mode 100644 index 9d903fd4f4..0000000000 --- a/dependency-injection/tr/passing-dependencies.texy +++ /dev/null @@ -1,215 +0,0 @@ -Bağımlılıkların Aktarılması -*************************** - -<div class=perex> - -Argümanlar, ya da DI terminolojisiyle "bağımlılıklar", sınıflara başlıca şu yollarla aktarılabilir: - -* Yapıcı enjeksiyonu (constructor injection) -* Metot enjeksiyonu (setter injection denir) -* Özellik enjeksiyonu (property injection) -* `inject*()` metodu ya da `#[Inject]` attribute'u ile - -</div> - -Her seçeneği somut örneklerle gösterelim. - - -Yapıcı Enjeksiyonu -================== - -Bağımlılıklar, nesne örneklenirken yapıcı argümanları olarak verilir: - -```php -class MyClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -$obj = new MyClass($cache); -``` - -Bu yaklaşım, sınıfın çalışması için kesinlikle gereken zorunlu bağımlılıklar için uygundur; çünkü onlar olmadan örnek oluşturulamaz. - -PHP 8.0'dan beri, işlevsel olarak eşdeğer olan daha kısa bir yazım ([constructor property promotion |https://blog.nette.org/tr/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]) kullanabiliriz: - -```php -// PHP 8.0 -class MyClass -{ - public function __construct( - private Cache $cache, - ) { - } -} -``` - -PHP 8.1'den beri bir özellik `readonly` bayrağıyla işaretlenebilir; bu, özelliğin değerinin başlatmadan sonra değişmeyeceğini bildirir: - -```php -// PHP 8.1 -class MyClass -{ - public function __construct( - private readonly Cache $cache, - ) { - } -} -``` - -DI container, bağımlılıkları yapıcıya [autowiring |autowiring] ile otomatik olarak aktarır. Bu yolla verilemeyen argümanlar (örneğin dizeler, sayılar, boolean değerler) [yapılandırmada belirtilir |services#Argümanlar]. - - -Constructor Hell ----------------- - -*Constructor hell* terimi, bir alt sınıfın, yapıcısı bağımlılık isteyen bir üst sınıftan türediği ve alt sınıfın da kendi bağımlılıklarını istediği durumu anlatır. O zaman üst sınıfın bağımlılıklarını da alıp aktarması gerekir: - -```php -abstract class BaseClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass extends BaseClass -{ - private Database $db; - - // ⛔ CONSTRUCTOR HELL - public function __construct(Cache $cache, Database $db) - { - parent::__construct($cache); - $this->db = $db; - } -} -``` - -Sorun, `BaseClass` sınıfının yapıcısını değiştirmek istediğimizde, örneğin yeni bir bağımlılık eklendiğinde ortaya çıkar. O zaman alt sınıfların tüm yapıcılarını da değiştirmek gerekir. Bu da böyle bir değişikliği cehenneme çevirir. - -Bu nasıl önlenir? Çözüm, **[kompozisyonu kalıtıma yeğlemektir |faq#Kompozisyon neden kalıtıma yeğlenir?]**. - -Yani kodu farklı tasarlarız. [Soyut |nette:introduction-to-object-oriented-programming#Soyut Sınıflar] `Base*` sınıflarından kaçınacağız. `MyClass`, belirli bir işlevi `BaseClass` sınıfından türeyerek edinmek yerine, bu işlevi bağımlılık olarak alacak: - -```php -final class SomeFunctionality -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass -{ - private SomeFunctionality $sf; - private Database $db; - - public function __construct(SomeFunctionality $sf, Database $db) // ✅ - { - $this->sf = $sf; - $this->db = $db; - } -} -``` - - -Setter Enjeksiyonu -================== - -Bağımlılıklar, onları private bir özellikte saklayan bir metot çağrılarak verilir. Bu metotların yaygın adlandırma uzlaşımı `set*()` biçimidir, bu yüzden onlara setter denir; ama elbette başka türlü de adlandırılabilirler. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - $this->cache = $cache; - } -} - -$obj = new MyClass; -$obj->setCache($cache); -``` - -Bu yaklaşım, sınıfın çalışması için zorunlu olmayan isteğe bağlı bağımlılıklar için uygundur; çünkü nesnenin bağımlılığı gerçekten alacağı (yani çağıranın metodu çağıracağı) garanti değildir. - -Aynı zamanda bu yöntem, bağımlılığı değiştirmek için setter'ın defalarca çağrılmasına olanak tanır. Bu istenmiyorsa metoda bir denetim ekleyin ya da PHP 8.1'den beri `$cache` özelliğini `readonly` bayrağıyla işaretleyin. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - if (isset($this->cache)) { - throw new RuntimeException('The dependency has already been set'); - } - $this->cache = $cache; - } -} -``` - -Setter çağrısı, DI container yapılandırmasında [setup anahtarında |services#Setup] tanımlanır. Burada da bağımlılıkların autowiring ile otomatik aktarımı kullanılır: - -```neon -services: - - create: MyClass - setup: - - setCache -``` - - -Özellik Enjeksiyonu -=================== - -Bağımlılıklar, doğrudan bir üye özelliğe yazılarak verilir: - -```php -class MyClass -{ - public Cache $cache; -} - -$obj = new MyClass; -$obj->cache = $cache; -``` - -Bu yöntem uygunsuz sayılır; çünkü üye özelliğin `public` olarak bildirilmesi gerekir. Sonuç olarak, aktarılan bağımlılığın gerçekten gereken türde olduğunu güvence altına alma denetimini yitiririz (bu özellikle PHP 7.4'ün özellik tür bildirimlerinden önce geçerliydi) ve yeni atanan bir bağımlılığa özel mantıkla tepki verme, örneğin sonradan değiştirilmesini önleme olanağını kaybederiz. Aynı zamanda özellik, sınıfın public API'sinin bir parçası hâline gelir; bu da istenmeyebilir. - -Özellik ataması, DI container yapılandırmasında [setup bölümünde |services#Setup] tanımlanır: - -```neon -services: - - create: MyClass - setup: - - $cache = @\Cache -``` - - -Inject -====== - -Önceki üç yaklaşım tüm nesne yönelimli dillerde genel olarak geçerliyken, `inject*()` metotlarıyla ya da `#[Inject]` attribute'uyla enjeksiyon tipik olarak Nette presenter'larında kullanılır ve orada varsayılan olarak etkindir; başka herhangi bir servis [`inject: true` |services#Inject Kipi] ile bunu açabilir. Bunlar [ayrı bir bölümde |best-practices:inject-method-attribute] ele alınıyor. - - -Hangi Yöntemi Seçmeli? -====================== - -- Yapıcı, sınıfın çalışması için kesinlikle gereken zorunlu bağımlılıklar için uygundur. -- Setter ise tersine, isteğe bağlı ya da sonradan değiştirilmesi gerekebilecek bağımlılıklar için uygundur. -- Public özellikler genellikle önerilmez. diff --git a/dependency-injection/tr/services.texy b/dependency-injection/tr/services.texy deleted file mode 100644 index fc97a464f7..0000000000 --- a/dependency-injection/tr/services.texy +++ /dev/null @@ -1,480 +0,0 @@ -Servis Tanımları -**************** - -.[perex] -Yapılandırma, DI container'a tek tek servisleri nasıl oluşturacağını ve onları bağımlılıklarıyla nasıl bağlayacağını anlattığımız yerdir. Nette bunu yapmanın çok anlaşılır ve şık bir yolunu sunar. - -NEON yapılandırma dosyasındaki `services` bölümü, kendi servislerimizi ve yapılandırmalarını tanımladığımız yerdir. `PDO` sınıfının bir örneğini temsil eden `database` adlı bir servisi tanımlayan basit bir örneğe bakalım: - -```neon -services: - database: PDO('sqlite::memory:') -``` - -Yukarıdaki yapılandırma, [DI container|container] içinde şu factory metodunu doğurur: - -```php -public function createServiceDatabase(): PDO -{ - return new PDO('sqlite::memory:'); -} -``` - -Servis adları, yapılandırma dosyasının başka yerlerinde `@servisAdı` biçimiyle onlara başvurmayı sağlar. Servise ad vermeye gerek yoksa yalnızca bir madde imi (`-`) kullanabiliriz: - -```neon -services: - - PDO('sqlite::memory:') -``` - -DI container'dan bir servis almak için, parametre olarak servis adını alan `getService()` metodunu ya da servis türünü alan `getByType()` metodunu kullanabiliriz: - -```php -$database = $container->getService('database'); -$database = $container->getByType(PDO::class); -``` - - -Servis Oluşturma -================ - -Genellikle bir servisi yalnızca belirli bir sınıfı örnekleyerek oluştururuz. Örneğin: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Yapılandırmayı ek anahtarlarla genişletmemiz gerekirse, tanım birden çok satıra bölünebilir: - -```neon -services: - database: - create: PDO('sqlite::memory:') - setup: ... -``` - -`create` anahtarının `factory` adında bir takma adı vardır; iki biçim de yaygın olarak kullanılır. Yine de `create` kullanmanızı öneririz. - -Yapıcının ya da factory metodunun argümanları, alternatif olarak `arguments` anahtarıyla da belirtilebilir: - -```neon -services: - database: - create: PDO - arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] -``` - -Servisler yalnızca sınıf örneklemesiyle oluşturulmak zorunda değildir; statik metotların ya da başka servislerin metotlarının çağrılmasının sonucu da olabilirler: - -```neon -services: - database: DatabaseFactory::create() - router: @routerFactory::create() -``` - -Basitlik için `->` yerine `::` kullanıldığına dikkat edin, bkz. [#İfade Dili]. Şu factory metotları üretilecek: - -```php -public function createServiceDatabase(): PDO -{ - return DatabaseFactory::create(); -} - -public function createServiceRouter(): RouteList -{ - return $this->getService('routerFactory')->create(); -} -``` - -DI container'ın, oluşturulan servisin türünü bilmesi gerekir. Servisi, dönüş türü belirtilmemiş bir metotla oluşturuyorsak, bu türü yapılandırmada açıkça bildirmeliyiz: - -```neon -services: - database: - create: DatabaseFactory::create() - type: PDO -``` - - -Argümanlar -========== - -Argümanları yapıcılara ve metotlara, PHP'nin kendisinde yapıldığına çok benzer biçimde aktarırız: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Daha iyi okunurluk için argümanları ayrı satırlarda sıralayabiliriz. Bu durumda virgül kullanmak isteğe bağlı olur: - -```neon -services: - database: PDO( - 'mysql:host=127.0.0.1;dbname=test' - root - secret - ) -``` - -Argümanları adlandırabilir ve böylece sıralarıyla uğraşmaktan kurtulabilirsiniz: - -```neon -services: - database: PDO( - username: root - password: secret - dsn: 'mysql:host=127.0.0.1;dbname=test' - ) -``` - -Belirli argümanları atlayıp varsayılan değerlerini kullanmak ya da [autowiring|autowiring] ile bir servisin enjekte edilmesini istiyorsanız, alt çizgi (`_`) kullanın: - -```neon -services: - foo: Foo(_, %appDir%) -``` - -Argümanlar servisleri, parametreleri ve daha fazlasını içerebilir, bkz. [#İfade Dili]. - - -Setup -===== - -`setup` bölümünde, servis oluşturulurken çağrılması gereken metotları tanımlarız. - -```neon -services: - database: - create: PDO(%dsn%, %user%, %password%) - setup: - - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) -``` - -Bu PHP'de şöyle görünürdü: - -```php -public function createServiceDatabase(): PDO -{ - $service = new PDO('...', '...', '...'); - $service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - return $service; -} -``` - -Metot çağrılarının yanı sıra özelliklere değer de atanabilir. Dizilere öğe eklemek de desteklenir; bu, NEON söz dizimiyle çakışmayı önlemek için dizi erişiminin tırnak içine alınmasını gerektirir: - -```neon -services: - foo: - create: Foo - setup: - - $value = 123 - - '$onClick[]' = [@bar, clickHandler] -``` - -Bu, PHP kodunda şöyle görünürdü: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - $service->value = 123; - $service->onClick[] = [$this->getService('bar'), 'clickHandler']; - return $service; -} -``` - -Ancak setup içinde statik metotları ya da başka servislerin metotlarını da çağırabilirsiniz. Geçerli servisin kendisini argüman olarak aktarmanız gerekiyorsa, ona `@self` ile başvurun: - -```neon -services: - foo: - create: Foo - setup: - - My\Helpers::initializeFoo(@self) - - @anotherService::setFoo(@self) -``` - -Basitlik için `->` yerine `::` kullanıldığına dikkat edin, bkz. [#İfade Dili]. Şöyle bir factory metodu üretilecek: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - My\Helpers::initializeFoo($service); - $this->getService('anotherService')->setFoo($service); - return $service; -} -``` - - -İfade Dili -========== - -Nette DI son derece zengin bir ifade dili sunar; bu dille neredeyse her şeyi tanımlayabiliriz. Yapılandırma dosyalarında [parametreleri |configuration#Parametreler] kullanabiliriz: - -```neon -# parametre -%wwwDir% - -# bir anahtarın altındaki parametrenin değeri -%mailer.user% - -# dize içinde parametre -'%wwwDir%/images' -``` - -Ayrıca nesne oluşturabilir, metot ve fonksiyon çağırabiliriz: - -```neon -# nesne oluştur -DateTime() - -# statik metot çağır -Collator::create(%locale%) - -# PHP fonksiyonu çağır -::getenv(DB_USER) -``` - -Servislere ya adlarıyla ya da türleriyle başvurabiliriz: - -```neon -# ada göre servis -@database - -# türe göre servis -@Nette\Database\Connection -``` - -First-class callable söz dizimini kullanın: .{data-version:3.2.0} - -```neon -# callback oluşturur, [@user, logout] ile eşdeğerdir -@user::logout(...) -``` - -Sabitleri kullanın: - -```neon -# sınıf sabiti -FilesystemIterator::SKIP_DOTS - -# genel sabiti PHP'nin constant() fonksiyonuyla al -::constant(\PHP_VERSION) -``` - -Bir servisin public özelliklerine ve sabitlerine `@service::member` ile erişin. Adın bir özelliğe mi yoksa bir sabite mi çözüleceğine ilk harfi karar verir: küçük harfle başlaması public bir özellik, büyük harfle başlaması bir sabit demektir: - -```neon -# bir servisin public özelliği (küçük harfle başlar) -@settings::apiUrl - -# bir servisin sınıf sabiti (büyük harfle başlar) -@settings::Version -``` - -Metot çağrıları, tıpkı PHP'deki gibi zincirlenebilir. Basitlik için `->` yerine `::` kullanılır: - -```neon -DateTime()::format('Y-m-d') -# PHP: (new DateTime())->format('Y-m-d') - -@http.request::getUrl()::getHost() -# PHP: $this->getService('http.request')->getUrl()->getHost() -``` - -Bu ifadeleri her yerde kullanabilirsiniz: [servis oluştururken |#Servis Oluşturma], [argümanlarda |#Argümanlar], [setup |#Setup] bölümünde ya da [parametrelerde |configuration#Parametreler]: - -```neon -parameters: - ipAddress: @http.request::getRemoteAddress() - -services: - database: - create: DatabaseFactory::create( @anotherService::getDsn() ) - setup: - - initialize( ::getenv('DB_USER') ) -``` - - -Özel Fonksiyonlar ------------------ - -Yapılandırma dosyalarında şu özel fonksiyonları kullanabilirsiniz: - -- `not()` bir değeri tersine çevirir -- `bool()`, `int()`, `float()`, `string()` belirtilen türe kayıpsız dönüştürme .{data-version:3.0.5} -- `typed()` belirtilen türdeki tüm servislerden oluşan bir dizi oluşturur -- `tagged()` verilen etikete sahip tüm servislerden oluşan bir dizi oluşturur - -```neon -services: - - Foo( - id: int(::getenv('ProjectId')) - productionMode: not(%debugMode%) - ) -``` - -`(int)` gibi standart PHP dönüşümlerinin aksine, kayıpsız dönüştürme sayısal olmayan değerlerde istisna fırlatır. - -`typed()` fonksiyonu, belirtilen türdeki (sınıf ya da arayüz) tüm servislerden oluşan bir dizi oluşturur. Autowiring'i kapatılmış servisleri dışarıda bırakır. Virgülle ayırarak birden çok tür de belirtilebilir. - -```neon -services: - - BarsDependent( typed(Bar) ) -``` - -Belirli bir türdeki servislerden oluşan bir dizi, [autowiring |autowiring#Servis Koleksiyonu] ile argüman olarak otomatik de aktarılabilir. - -`tagged()` fonksiyonu ise belirli bir etikete sahip tüm servislerden oluşan bir dizi oluşturur. Burada da virgülle ayırarak birden çok etiket belirtebilirsiniz. - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - - -Autowiring -========== - -`autowired` anahtarı, belirli bir servisin autowiring davranışını etkilemenizi sağlar. Ayrıntılar için [autowiring bölümüne|autowiring] bakın. - -```neon -services: - foo: - create: Foo - autowired: false # foo servisi autowiring'den çıkarılır -``` - - -Tembel Servisler .{data-version:3.2.4} -====================================== - -Tembel yükleme, bir servisin oluşturulmasını gerçekten gerekene dek erteleyen bir tekniktir. Genel yapılandırmada tüm servisler için [tembel oluşturmayı |configuration#Tembel Servisler] tek seferde açabilirsiniz. Tek tek servislerde bu davranışı geçersiz kılabilirsiniz: - -```neon -services: - foo: - create: Foo - lazy: false -``` - -Bir servis tembel olarak tanımlandığında, onu DI container'dan istediğimizde özel bir proxy nesnesi alırız. Bu proxy, gerçek servisle aynı görünür ve aynı davranır, ama asıl başlatma (yapıcının ve setup çağrılarının çalıştırılması) yalnızca metotlarından ya da özelliklerinden birine ilk erişimde gerçekleşir. - -Servis daha sonra oluşturulduğundan, yapılandırmasındaki hataların da daha sonra ortaya çıkacağını unutmayın. Örneğin yanlış veritabanı kimlik bilgileri, uygulama başlarken değil, ilk sorguda kendini gösterir. - -Tembel oluşturma, döngüsel bağımlılıkları da, yani A servisinin B'yi, B'nin de aynı anda A'yı gerektirdiği durumu hafifletir. Bu olmadan container `Circular reference detected` hatasını bildirir. Tembel proxy'yle A servisi yalnızca B servisinin bir proxy'sini alır; o da gerçekten kullanıldığında, yani A zaten varken kendini başlatır. Yine de döngüsel bağımlılık kusurlu bir tasarımın işaretidir ve ondan kurtulmak daha iyidir. - -.[note] -Tembel yükleme PHP 8.4 ya da daha yenisini gerektirir ve yalnızca doğrudan sınıf örneklenerek oluşturulan servislerde çalışır (örneğin `create: Foo`), factory metoduyla oluşturulanlarda çalışmaz. Sonunda PHP'nin iç sınıflarından birini genişleten sınıflarda da kullanılamaz. Tembel yükleme uygulanamadığında `lazy: true` bayrağı sessizce yok sayılır. - - -Etiketler -========= - -Etiketler, servislere ek bilgi eklemeye yarar. Bir servise bir ya da daha çok etiket atayabilirsiniz: - -```neon -services: - foo: - create: Foo - tags: - - cached -``` - -Etiketler değer de taşıyabilir: - -```neon -services: - foo: - create: Foo - tags: - logger: monolog.logger.event -``` - -Belirli etiketlerle ilişkili tüm servisleri almak için `tagged()` fonksiyonunu kullanabilirsiniz: - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - -DI container içinde, belirli bir etikete sahip tüm servislerin adlarını `findByTag()` metoduyla alabilirsiniz: - -```php -$names = $container->findByTag('logger'); -// $names, anahtarları servis adları, değerleri etiket değerleri olan bir dizidir -// örneğin ['foo' => 'monolog.logger.event', ...] -``` - - -Inject Kipi -=========== - -`inject: true` bayrağı, [Inject |best-practices:inject-method-attribute#Inject Attribute'ları] attribute'una sahip public özellikler ve [inject*() |best-practices:inject-method-attribute#inject*() Metotları] metotları üzerinden bağımlılık enjeksiyonunu etkinleştirir. - -```neon -services: - articles: - create: App\Model\Articles - inject: true -``` - -Varsayılan olarak `inject` kipi yalnızca presenter'larda açıktır. - - -Servis Değişiklikleri -===================== - -DI container, yerleşik ya da [kullanıcı extension'larıyla|extensions] eklenmiş pek çok servis tutar. Bu var olan servislerin tanımlarını doğrudan yapılandırmada değiştirebilirsiniz. Örneğin varsayılanı `Nette\Application\Application` olan `application.application` servisinin sınıfını başka bir sınıfla değiştirebilirsiniz: - -```neon -services: - application.application: - create: MyApplication - alteration: true -``` - -`alteration` bayrağı, var olan bir servisi yalnızca değiştirdiğimizi gösterir. Aynı zamanda bir güvence görevi görür: değiştirilen servis yoksa derleme istisnayla başarısız olur. - -Setup'ı da tamamlayabiliriz: - -```neon -services: - application.application: - create: MyApplication - alteration: true - setup: - - '$onStartup[]' = [@resource, init] -``` - -Bir servisi iç adıyla belirtmek zorunda değilsiniz; onun yerine türüyle başvurabilirsiniz. Önceki örnek şöyle de yazılabilir: - -```neon -services: - @Nette\Application\Application: - create: MyApplication -``` - -Bir servisi değiştirirken özgün argümanları, setup öğelerini ya da etiketleri `reset` anahtarıyla kaldırmak isteyebiliriz: - -```neon -services: - application.application: - create: MyApplication - alteration: true - reset: - arguments: true - setup: true - tags: true -``` - -Bir extension tarafından eklenen bir servisi kaldırmak isterseniz bunu şöyle yapabilirsiniz: - -```neon -services: - cache.journal: false -``` diff --git a/dependency-injection/tr/upgrading.texy b/dependency-injection/tr/upgrading.texy deleted file mode 100644 index a1586f6c77..0000000000 --- a/dependency-injection/tr/upgrading.texy +++ /dev/null @@ -1,49 +0,0 @@ -Yükseltme -********* - - -Sürüm 3.1'e Yükseltme -===================== - -- autowiring, varsayılan değeri olmayan nullable bir parametreye artık `null` aktarmıyor; argümanı açıkça verin ya da parametreye bir varsayılan değer tanımlayın -- `@return` açıklaması desteği kaldırıldı; bir dönüş türü kullanın ya da türü servis tanımında `type:` ile belirtin -- `dynamic` anahtarının adı `imported`, `class` anahtarının adı `type` olarak değişti -- atlanan argümanın simgesi `...` yerine `_` oldu, örneğin `MyService(_, 123)` -- NEON dosyalarında, dizenin başındaki `@` karakterinin artık kaçışlanması gerekmiyor -- üretilen factory tanımlarının içindeki `parameters` anahtarı kullanımdan kaldırıldı -- `Nette\DI\Config\Loader::save()` metodu kullanımdan kaldırıldı; yapılandırmayı `Nette\DI\Config\Adapters\NeonAdapter::dump()` ile dışa aktarın - -3.1 sürümü bir geçiş sürümüdür: yeni özellik getirmez, ama sonradan farklı çalışacak her şey için notice'larla uyarır. [Nette DI 3.1: geçiş sürümü |https://blog.nette.org/en/nette-di-3-1-transition-release] yazısına bakın. - - -Sürüm 3.0'a Yükseltme -===================== - -- INI dosyaları desteği kaldırıldı -- yapılandırmaya soru işaretleriyle doğrudan PHP kodu yazma (örneğin `"$service->onError[] = ?"(...)`) kaldırıldı; bunun yerine `'$onError[]' = [...]` dizi söz dizimini kullanın -- yapılandırma dosyalarında `class: PDO(...)` yerine `factory: PDO(...)` kullanın -- presenter'lar için `nette.presenter` etiketi artık kullanılmıyor - - -Compiler Extension Yazarları İçin ---------------------------------- - -Nette 2.4 içeride her servisi `Nette\DI\ServiceDefinition` olarak tanımlarken, artık birkaç tanım türü var: içe aktarılan (dinamik) servisler için `Nette\DI\Definitions\ImportedDefinition`, arayüz tabanlı üretilen factory'ler için `Nette\DI\Definitions\FactoryDefinition`, üretilen accessor'lar için `Nette\DI\Definitions\AccessorDefinition` ve sıradan servisler için `Nette\DI\Definitions\ServiceDefinition`. - -Bu yüzden `ContainerBuilder::addDefinition()` metodunun yanında yeni tanım oluşturmaya yarayan birkaç metot daha var: `addFactoryDefinition()`, `addAccessorDefinition()` ve `addImportedDefinition()`. - - -Sürüm 2.4'e Yükseltme -===================== - -- tek bir yapılandırma dosyasındaki yapılandırma bölümleri (örneğin production, development) kullanımdan kaldırıldı; `config.neon` ve `config.local.neon` dosya çiftini kullanın -- servis tanımlarının kalıtımı kullanımdan kaldırıldı -- `Statement::setEntity()` kullanımdan kaldırıldı - - -Sürüm 2.3'e Yükseltme -===================== - -- servisleri yapılandırma dosyasının extension bölümüne koyma desteği kaldırıldı -- dinamik olarak eklenen extension'ların desteği kaldırıldı -- bir servisi dinamik olarak değiştirirken (`removeService()`, `addService()` ile), yeni servis özgün servisle aynı arayüzün/sınıfın bir örneği olmalıdır diff --git a/dev/cs/@home.texy b/dev/cs/@home.texy new file mode 100644 index 0000000000..7c7a12450b --- /dev/null +++ b/dev/cs/@home.texy @@ -0,0 +1,81 @@ +Píšeme první aplikaci +********************* + + +<div class=perex> + +Tento návod vás v rychlosti seznámí s Nette Frameworkem při tvorbě jednoduché aplikace. Ukážeme si, jak vytvořit bezpečnou aplikaci s využitím hlavních předností frameworku. Pojďme na to! + + +.[navig] +1. [Začínáme|book/start] +2. [Databáze a model|book/database] +3. [Presentery a šablony|book/presenter] +4. [Formuláře|book/forms] +5. [Komponenty|book/components] +6. [Přihlašování uživatelů|book/authentication] +7. [AJAX|book/ajax] +8. URL a routování (v přípravě) +9. Nasazení aplikace a její bezpečnost (v přípravě) + +</div> + + +Návod je psán pro **Nette Framework 2.0.5** a PHP 5.3 nebo novější. Ověřte si, zda máte správnou verzi. + + +/--comment +- dřívější quick start +- všechny kapitoly by měly být maximálně stručné a tedy i povrchní +- na konci kapitoly body „co bychom si měli zapamatovat“ +- dávat vždy příklad ke stažení (na konci nebo na začátku, co bude vhodnější) + + +Osnova (zatím nekompletní) +-------------------------- + +1. Začínáme s Nette Framework + - výzva ke stažení, instalaci a zkopírování Skeletonu; + - vysvětlení propojení router + presenter + templates (ale žádné další adresáře) + - velmi ale velmi stručné vysvětlení, k čemu je config.ini, Debugger::enable, RobotLoader +2. Síla šablon + - v presenteru naplníme data do šablony (jednoduché pole, zatím bez DB) + - v šabloně vykreslíme – zapojíme makra {foreach}, {if}, využijeme $iterator, n:attributy a modifikátory + - vysvětlíme kontextově senzitivní escapování + - vytvoříme druhou stránku a prolinkujeme je + - vytvoříme layout + - bloky lze ukázat na propojení <title> a <h1> (nepoužívat termín „dědění“) +3. Model a databáze + - tahle kapitola bude chtít ještě promyslet + - využil bych Dibi, protože je v distribuci a dá se ukázat na Debugger Bar + - nakonfigurujeme spojení s databází v config.ini a připojíme se (k SQLite buď přímo nebo přes k PDO) k připravenému souboru + - vytvoříme jednu třídu pro načtení a eventuelně zápis do databáze +4. Formuláře snadno a rychle + - vytvoříme si jednoduchý formulář přes createComponent a vložíme do šablony + - ukážeme základní validační pravidla + - získaná data vložíme do databáze a přesměrujeme +5. AJAX + - s využitím jQuery + - necháme část stránky měnit pomocí snippetu (žádná zmínka o zavináčích) + - necháme formulář odesílat AJAXově +6. URL a routování + - ukážeme, že u hotové aplikace lze měnit tvar URL + - ukážeme Routing Debugger, jakožto součást Debugger Bar +7. Gratulujeme! + - nasměrujeme na další zdroje informací + + +Zdroje +------ + +- [doc-0.9:quickstart/Co budeme potřebovat?] +- [doc-0.9:quickstart/Adresářová struktura] +- [doc-0.9:quickstart/Vytvoření databáze] +- [doc-0.9:quickstart/Vytvoření presenteru] +- [doc-0.9:quickstart/Vytvoření šablony] +- [doc-0.9:quickstart/Vytvoření modelu] +- [doc-0.9:quickstart/Hezčí šablony] +- [doc-0.9:quickstart/Dokončení základní aplikace] +- [doc-0.9:quickstart/Stránkování a routování] +- Inzův quick start: https://doc.nette.org/cs/quickstart +\-- diff --git a/dev/cs/book/ajax.texy b/dev/cs/book/ajax.texy new file mode 100644 index 0000000000..44e738a5c3 --- /dev/null +++ b/dev/cs/book/ajax.texy @@ -0,0 +1,194 @@ +AJAX +**** + +.[perex] +Většina moderních aplikací již dnes používá AJAX. Díky Nette je "zajaxování" celé aplikace velmi snadné a téměř nebudete muset měnit původní kód. Nyní si ukážeme, jak na to. + + +Největší síla AJAXu v Nette spočívá v použití tzv. snippetů. Tyto speciálně označené bloky kódu se pak při AJAXových požadavcích přenášejí ke klientovi, který si podle nich aktualizuje stránku. Takto je možné posílat jen upravené části stránky. Největší výhodou je, že vše je prakticky bez práce. + + +Příprava +======== + +Ještě než začneme, musíme si připravit některé pomocné skripty. Nette bohužel zatím nemá žádný oficiální JavaScript na podporu AJAXu na straně klienta, proto musíme buď sáhnout po již existujících skriptech do [doplňků |https://componette.com/search/ajax/], nebo si napsat vlastní. S využitím jQuery je to však snadné: + +.[note] +Uvedený skript v sobě nemá ošetření chyb a neumožňuje rozlišit, jakým tlačítkem byl formulář odeslán. Pro naše účely však plně postačuje. Ve skutečné aplikaci raději sáhněte po řešení z doplňků. + +```js +jQuery.ajaxSetup({ + cache: false, + dataType: 'json', + success: function (payload) { + if (payload.snippets) { + for (var i in payload.snippets) { + $('#' + i).html(payload.snippets[i]); + } + } + } +}); + +// odesílání odkazů +$('a.ajax').live('click', function (event) { + event.preventDefault(); + $.get(this.href); +}); + +// odesílání formulářů +$('form.ajax').live('submit', function (event) { + event.preventDefault(); + $.post(this.action, $(this).serialize()); +}); +``` + +Tento skript jednoduše nechá všechny odkazy a formuláře s třídou `ajax` odeslat pomocí AJAXu. Díky metodě `live` se událost vykoná i v případě odkazů a formulářů, které budou vytvořeny později AJAXem. + +Skript uložíme například do `www/js/ajax.js`. Poté jej nalinkujeme do hlavičky stránky v `@layout.latte`. Musíme jej však vložit až za načtení jQuery, takže v hlavičce budou následující skripty: + +```html +<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7/jquery.min.js"></script> +<script type="text/javascript" src="{$basePath}/js/netteForms.js"></script> +<script type="text/javascript" src="{$basePath}/js/ajax.js"></script> +``` + +Nyní už můžeme AJAX začít používat. + + +Komponenta `TaskList` +===================== + +Začneme s komponentou s výpisem úkolů. Celý obsah šablony obalíme do makra `{snippet}` a odkazu na splnění úkolu přidáme třídu `ajax`: + +```html +{snippet} +<table class="tasks"> + <thead> + <tr> + <th class="created"> </th> + <th class="list" n:if="$displayList">Seznam</th> + <th class="text">Úkol</th> + <th class="user" n:if="$displayUser">Přiřazeno</th> + <th class="action"> </th> + </tr> + </thead> + <tbody> + {foreach $tasks as $task} + <tr n:class="$iterator->isOdd() ? odd : even, $task->done ? done"> + <td class="created">{$task->created|date:'j. n. Y'}</td> + <td class="list" n:if="$displayList">{$task->list->title}</td> + <td class="text">{$task->text}</td> + <td class="user" n:if="$displayUser">{$task->user->name}</td> + <td class="action"><a n:if="!$task->done" n:href="markDone! $task->id" class="icon tick ajax">hotovo</a></td> + </tr> + {/foreach} + </tbody> +</table> +{/snippet} +``` + +.[note] +Snippet je vykreslován jako `<div>`, proto je jím třeba obalit celou tabulku. V opačném případě by se vygeneroval nevalidní kód. + +Poté jen upravíme signál `markDone` tak, aby při AJAXových požadavcích neprováděl přesměrování, ale nechal zaslat opravenou tabulku: + +```php +public function handleMarkDone(int $taskId): void +{ + $this->taskRepository->markDone($taskId); + if (!$this->presenter->isAjax()) { + $this->presenter->redirect('this'); + } else { + $this->invalidateControl(); + } +} +``` + +Metoda `isAjax()` třídy `Presenter` zjišťuje, zda se jedná o AJAXový požadavek. Metodou `invalidateControl()` pak necháme celou komponentu, resp. všechny snippety v ní zneplatnit. Pokud metodě uvedeme jako parametr název snippetu, bude zneplatněn pouze uvedený snippet. Zneplatněné snippety jsou nakonec poslány klientovi. + +A to je vše. Pokud máme správně zavedený dříve uvedený script a správně upravenou šablonu, bude nyní označování hotových úkolů probíhat s využitím AJAXu. + + +Přidávání úkolů +=============== + +Nyní by bylo vhodné upravit `TaskPresenter` tak, aby i přidávání úkolů probíhalo s využitím AJAXu. Stačí jen formuláři přidat třídu `ajax`, a pokud chceme formulář nechat jednoduše po odeslání vymazat, obalíme jej také do snippetu: + +```html +{block content} + +<h1 n:block="title">{$list->title}</h1> + +{snippet form} +<fieldset> + <legend>Přidat úkol</legend> + + {form taskForm class: ajax} + <div class="task-form"> + {control $form errors} + + {label text /} {input text size: 30, autofocus: true} {label userId /} {input userId} {input create} + </div> + {/form} +</fieldset> +{/snippet} + +{control taskList} + +{/block} +``` + +V metodě, která zpracovává odeslaný formulář, provedeme podobnou úpravu, jako u signálu v komponentě `TaskList`: + +```php +public function taskFormSubmitted(Nette\Application\UI\Form $form, stdClass $values): void +{ + $this->taskRepository->createTask($this->list->id, $form->values->text, $form->values->userId); + $this->flashMessage('Úkol přidán.', 'success'); + if (!$this->isAjax()) { + $this->redirect('this'); + } else { + $form->setValues([], true); + $this->invalidateControl('form'); + $this['taskList']->invalidateControl(); + } +} +``` + +Tentokrát však invalidujeme konkrétní snippet (`form`) a celou komponentu `taskList`. Také vymazáváme odeslané hodnoty ve formuláři pomocí volání `setValues`. Prvním parametrem této metody je seznam hodnot, zde prázdné pole, druhý říká, že chceme u neuvedených prvků hodnoty vymazat. Pokud bychom tento parametr neuvedli, Nette by jen přepisovalo hodnoty prvků, které jsme uvedli v poli. + +To je opět vše. Nyní by i přidávání úkolů mělo fungovat s využitím AJAXu. Pokud to zkusíme, zjistíme, že by možná bylo vhodnější nemazat hodnoty v celém formuláři, ale ponechat v selectu hodnotu vybraného uživatele. To zajistíme jednoduchým upravením volání `$form->setValues()`: + +```php +$form->setValues(['userId' => $form->values->userId], true); +``` + + +Flash zprávy +============ + +Všechny komponenty se sice aktualizují, ale nezobrazují se nám flash zprávy. Řešení je opět velmi jednoduché: stačí flash zprávy obalit do snippetu a ten ve vhodný okamžik invalidovat. + +`@layout.latte`: + +```html +{snippet flashMessages} +<div n:foreach="$flashes as $flash" class="flash {$flash->type}">{$flash->message}</div> +{/snippet} +``` + +Invalidaci můžeme provést v metodě `beforeRender()` v `BasePresenter`u: + +```php +protected function beforeRender(): void +{ + $this->template->lists = $this->listRepository->findAll()->order('title ASC'); + if ($this->isAjax()) { + $this->invalidateControl('flashMessages'); + } +} +``` + +Jak vidíte, je práce s AJAXem v Nette opravdu velmi snadná. Jedním chytrým skriptem a několika málo změnami v kódu jsme převedli klíčové prvky naší aplikace do AJAXu. + +Zdrojové kódy aplikace v této fázi naleznete na [GitHubu |https://github.com/nette/book/tree/7]. diff --git a/dev/cs/book/authentication.texy b/dev/cs/book/authentication.texy new file mode 100644 index 0000000000..2939eb63ef --- /dev/null +++ b/dev/cs/book/authentication.texy @@ -0,0 +1,436 @@ +Přihlašování uživatelů +********************** + +.[perex] +Nyní aplikaci rozšíříme o přihlašování uživatelů. Přístup k úkolům tak bude podmíněn přihlášením, navíc na úvodní stránce každého uživatele můžeme zobrazit úkoly, které mu jsou přiřazeny. + + +Authenticator +============= + +Nejprve se vrátíme ke třídě `Authenticator`, kterou jsme v části věnované databázi a modelu tak sprostě odbyli. V první řadě budeme muset upravit hashovací funkci. Nyní používá MD5 a složité solení hesla. My však chceme použít [php:crypt()], hesla budeme solit a díky chytrému algoritmu, bude sůl součástí hashe, takže se o ni nemusíme starat. + +Ve třídě `Authenticator` tedy upravíme metodu `calculateHash()` takto: + +```php +public static function calculateHash(string $password, string $salt = null): string +{ + if ($salt === null) { + $salt = '$2a$07$' . Nette\Utils\Strings::random(22); + } + return crypt($password, $salt); +} +``` + +Tato metoda jen dostane heslo jako parametr a vrátí jej v zahashované podobě. Druhý argument je pro původní heslo kvůli zachování algoritmu a přenášení soli. + +Hlavní metodou této třídy je však metoda `authenticate()`. Ta provádí ověření přihlašovacích údajů a pokud jsou zadané údaje správné, vrátí objekt s identitou uživatele. Pokud údaje nesouhlasí, vyhodí výjimku typu `AuthenticationException`. Připravenou metodu musíme také mírně upravit, konkrétně při vytváření objektu `Identity` nebudeme zadávat žádné role uživatele, ale `null`: + +```php +public function authenticate(array $credentials): NS\Identity +{ + list($username, $password) = $credentials; + $row = $this->userRepository->findByName($username); + + if (!$row) { + throw new NS\AuthenticationException("User '$username' not found.", self::IDENTITY_NOT_FOUND); + } + + if ($row->password !== self::calculateHash($password, $row->password)) { + throw new NS\AuthenticationException("Invalid password.", self::INVALID_CREDENTIAL); + } + + unset($row->password); + return new NS\Identity($row->id, null, $row->toArray()); +} +``` + +Metoda dostane jako argumenty pole s přihlašovacími údaji. Na indexu `0` je uživatelské jméno, na indexu `1` heslo. Nejprve získáme z tabulky uživatelů záznam o uživateli, který se snaží přihlásit. Pokud záznam není nalezen, je vyhozena výjimka. Poté je ověřeno, zda zadané heslo odpovídá heslu uživatele v databázi. Nakonec se vytvoří objekt typu `Identity`. V konstruktoru mu jsou předány jako parametry ID uživatele, seznam rolí (zde `null`) a nakonec pole s dalšími informacemi o uživateli. Zde se předá celý záznam bez pole `password`. K těmto datům pak můžeme později přistupovat. + +Ještě musíme implementovat metodu `findByName`, ve třídě `UserRepository`. + +```php +// class UserRepository + +public function findByName(string $username) +{ + return $this->findAll()->where('username', $username)->fetch(); +} +``` + +To je vše, nyní se můžeme přesunout do dalších částí aplikace. + + +Přihlašovací presenter +====================== + +V základní kostře je již připravený základ přihlašování v podobě `SignPresenter`u. My si ho však raději od základu přepišeme, už jen proto, že je celý v angličtině. + +Nejprve si připravíme přihlašovací formulář: + +.[note] +Nezapomeňte na `use Nette\Application\UI\Form;` na začátku souboru. + +```php +protected function createComponentSignInForm(): Form +{ + $form = new Form(); + $form->addText('username', 'Uživatelské jméno:', 30, 20); + $form->addPassword('password', 'Heslo:', 30); + $form->addCheckbox('persistent', 'Pamatovat si mě na tomto počítači'); + $form->addSubmit('login', 'Přihlásit se'); + $form->onSuccess[] = [$this, 'signInFormSubmitted']; + return $form; +} + +public function signInFormSubmitted(Nette\Application\UI\Form $form, stdClass $values): void +{ + try { + $user = $this->getUser(); + $values = $form->getValues(); + if ($values->persistent) { + $user->setExpiration('30 days', false); + } + $user->login($values->username, $values->password); + $this->flashMessage('Přihlášení bylo úspěšné.', 'success'); + $this->redirect('Homepage:'); + } catch (NS\AuthenticationException $e) { + $form->addError('Neplatné uživatelské jméno nebo heslo.'); + } +} +``` + +Při odeslání formuláře se nejprve připraví instance objektu uživatele a vytáhnou se z něj hodnoty. Poté se ověří, zda bylo zaškrtnuto trvalé přihlášení ("pamatovat si mě na tomto počítači"), pokud ano, nastaví se expirace sezení uživatele na 30 dnů pomocí metody `setExpiration`. Druhý parametr nastavuje, že uživatel nemá být odhlášen, pokud zavře okno prohlížeče. + +Poté se konečně provede přihlášení uživatele metodou `login()`. Pokud vše proběhlo, jak má, tak se vypíše flash zpráva a přesměrujeme uživatele na domovskou stránku. Pokud ne, metoda `login()` vyhodí výjimku, kterou zachytíme a ve formuláři necháme zobrazit chybovou hlášku voláním `$form->addError()`. + +Formulář ještě musíme v šabloně vykreslit. Šablonu `Sign/in.latte` upravíme následovně: + +```html +{block content} + +<h1 n:block="title">Přihlášení</h1> + +{form signInForm} +<div class="sign-in-form"> + {control $form errors} + + <div class="pair"> + {label username /} + <div class="input">{input username}</div> + </div> + <div class="pair"> + {label password /} + <div class="input">{input password}</div> + </div> + <div class="pair"> + <div class="input">{input persistent} {label persistent /}</div> + </div> + + <div class="pair"> + <div class="input">{input login}</div> + </div> +</div> +{/form} +``` + + +Bylo by také vhodné dát uživateli možnost odhlášení. Vytvoříme tedy v `BasePresenter`u signál `signOut`: + +```php +public function handleSignOut(): void +{ + $this->getUser()->logout(); + $this->redirect('Sign:in'); +} +``` + +Uživatele jen odhlásíme metodou `logout()` a přesměrujeme jej na přihlášení. + + +Zprovoznění trvalého přihlášení +------------------------------- + +Aby fungovalo trvalé přihlášení, je nutné nastavit delší dobu vypršení session. Toho nejsnáze docílíme pomocí konfigurace v `config.neon`. Najdeme sekci `nette` a do podsekce `session` přidáme: + +```neon +session: + autoStart: smart + expiration: 30 days +``` + + +Přesměrování nepřihlášeného uživatele +===================================== + +Pokud se nepřihlášený uživatel pokusí přistoupit na úvodní stránku nebo stránku s výpisem uživatelů, musíme jej přesměrovat na přihlášení. Uděláme to tím nejjednodušším způsobem, prostě nepřihlášeného uživatele ve `startup()` přesměrujeme. + +```php +protected function startup(): void +{ + parent::startup(); + + if (!$this->getUser()->isLoggedIn()) { + $this->redirect('Sign:in'); + } +} +``` + +Metoda `User::isLoggedIn()` ověří, zda je uživatel přihlášen. Pokud ne, proběhne přesměrování. + +Teď velice jednoduše zkopírujeme tyto řádky do metody `startup()` v presenteru `HomepagePresenter` a `TaskPresenter`. + +Nyní již vše funguje. Při pokusu o přístup na stránku s výpisem úkolů jsme přesměrováni na přihlášení. Ale pořád vidíme seznamy úkolů a můžeme zakládat nové. Zobrazování seznamů ošetříme jednoduše v šabloně `@layout.latte`: + +```html +<div id="sidebar"> + {if $user->isLoggedIn()} + <div class="title">Seznamy úkolů</div> + <div class="task-lists"> + <ul> + <li n:foreach="$lists as $list"><a n:href="Task: $list->id">{$list->title}</a></li> + </ul> + </div> + + <fieldset> + <legend>Nový seznam</legend> + {form newListForm} + <div class="new-list-form"> + {input title} + {input create} + </div> + {/form} + </fieldset> + {/if} +</div> +``` + +Všechny šablony presenteru mají k dispozici objekt uživatele. Můžeme nad ním bezproblémů zavolat metodu `isLoggedIn()` a zajistit tak podmíněné vykreslování jednotlivých částí stránky. + +Nyní však máme v aplikaci bezpečnostní slabinu: formulář pro založení seznamu úkolů sice zmizel, ale pokud pošleme správná data, formulář se nám vytvoří a provede vložení do databáze. Ověření, zda je uživatel přihlášen, je nutné provést také při vytváření formuláře. Oprava je snadná: + +```php +protected function createComponentNewListForm(): Form +{ + if (!$this->getUser()->isLoggedIn()) { + $this->redirect('Sign:in'); + } + + $form = new Form(); + // ... +} +``` + +Pokud uživatel nebude přihlášen, vyhodíme výjimku, která signalizuje neoprávněný přístup. Nette díky tomu zobrazí adekvátní chybovou stránku. + +Rovnou také můžeme zajistit, aby v políčku pro výběr uživatele byl předvyplněn aktuální uživatel: + +```php +$form->addSelect('userId', 'Pro:', $userPairs) + ->setPrompt('- Vyberte -') + ->addRule(Form::FILLED, 'Je nutné vybrat, komu je úkol přiřazen.') + ->setDefaultValue($this->getUser()->getId()); +``` + +Metoda `setDefaultValue()` slouží k nastavení výchozí hodnoty jednoho formulářového prvku. Formulář samotný má také metodu `setDefaults()`, která přejímá jako parametr asociativní pole a nastavuje výchozí hodnoty celému formuláři. + +Seznamy úkolů v levém sloupci zmizely a ani nepůjdou zakládat nové. Pokud se nyní přihlásíme, opět se nám zobrazí. Připravená data mají založené 3 uživatele: `admin`, `pepa`, `franta`. Jejich hesla jsou shodná s jejich uživatelským jménem. + +Po přihlášení nám ještě chybí možnost odhlášení. Opět si vypomůžeme podmíněným vykreslováním v `@layout.latte`: + +```html +<div id="header"> + <div id="header-inner"> + <div class="title"><a n:href="Homepage:">Úkolníček</a></div> + + {if $user->isLoggedIn()} + <div class="user"> + <span class="icon user">{$user->getIdentity()->name}</span> | + <a n:href="signOut!">Odhlásit se</a> + </div> + {/if} + </div> +</div> +``` + +`$user->getIdentity()` vrací identitu uživatele, kterou jsme vytvořili při přihlašování. Identita má v sobě uložený celý záznam uživatele z databáze. Díky magické metodě `__get()` k tomuto záznamu můžeme přistupovat podobně, jako kdybychom pracovali přímo s záznamem získaným z databáze. + +Hotovo! Nyní máme kompletně funkční přihlašování a odhlašování uživatelů. + +[* 07-logged-in.webp *] + + +Výpis úkolů uživatele +===================== + +S připravenými komponentami je nyní již výpis všech úkolů, které jsou přiřazené momentálně přihlášenému uživateli, triviální. Stačí jen vytvořit novou komponentu a vykreslit ji v šabloně. `HomepagePresenter`: + +```php +protected function createComponentUserTasks(): Todo\TaskListControl +{ + $incomplete = $this->taskRepository->findIncompleteByUser($this->getUser()->getId()); + $control = new Todo\TaskListControl($incomplete, $this->taskRepository); + $control->displayList = true; + $control->displayUser = false; + return $control; +} +``` + +U tohoto výpisu nebudeme chtít zobrazit uživatele, kterému je úkol přiřazen, ale místo toho naopak seznam úkolů, ve kterém je úkol veden. + +Potřebujeme ještě upravit metodu `findIncomplete()` na `findIncompleteByUser($userId)` + +```php +// class TaskRepository + +public function findIncompleteByUser(int $userId) +{ + return $this->findIncomplete()->where(['user_id' => $userId]); +} +``` + +Šablona `Homepage/default.latte`: + +```html +{block content} + +<h1 n:block="title">Přehled úkolů</h1> + +<h2>Mé úkoly</h2> +{control userTasks} + +<h2>Všechny nesplněné</h2> +{control incompleteTasks} + +{/block} +``` + + +Formulář pro změnu hesla +======================== + +Změna hesla uživatele je také velmi jednoduchá. Pro začátek si do `UserRepository` přidáme pomocnou metodu pro nastavení hesla uživatele: + +```php +public function setPassword(int $id, string $password): void +{ + $this->getTable()->where(['id' => $id])->update([ + 'password' => Authenticator::calculateHash($password) + ]); +} +``` + +Nyní už stačí jen vytvořit formulář, který změnu hesla provede. Umístíme jej do vlastního presenteru, který pojmenujeme například `UserPresenter`: + +```php +use Nette\Application\UI\Form; +use Nette\Security as NS; + +/** + */ +class UserPresenter extends BasePresenter +{ + /** @var Todo\UserRepository */ + private $userRepository; + + /** @var Todo\Authenticator */ + private $authenticator; + + protected function startup(): void + { + parent::startup(); + if (!$this->getUser()->isLoggedIn()) { + $this->redirect('Sign:in'); + } + $this->userRepository = $this->context->userRepository; + $this->authenticator = $this->context->authenticator; + } + + protected function createComponentPasswordForm(): Form + { + $form = new Form(); + $form->addPassword('oldPassword', 'Staré heslo:', 30) + ->addRule(Form::FILLED, 'Je nutné zadat staré heslo.'); + $form->addPassword('newPassword', 'Nové heslo:', 30) + ->addRule(Form::MIN_LENGTH, 'Nové heslo musí mít alespoň %d znaků.', 6); + $form->addPassword('confirmPassword', 'Potvrzení hesla:', 30) + ->addRule(Form::FILLED, 'Nové heslo je nutné zadat ještě jednou pro potvrzení.') + ->addRule(Form::EQUAL, 'Zadná hesla se musejí shodovat.', $form['newPassword']); + $form->addSubmit('set', 'Změnit heslo'); + $form->onSuccess[] = [$this, 'passwordFormSubmitted']; + return $form; + } + + + public function passwordFormSubmitted(Nette\Application\UI\Form $form, stdClass $values): void + { + $values = $form->getValues(); + $user = $this->getUser(); + try { + $this->authenticator->authenticate([$user->getIdentity()->username, $values->oldPassword]); + $this->userRepository->setPassword($user->getId(), $values->newPassword); + $this->flashMessage('Heslo bylo změněno.', 'success'); + $this->redirect('Homepage:'); + } catch (NS\AuthenticationException $e) { + $form->addError('Zadané heslo není správné.'); + } + } +} +``` + +Povšimněte si něktrých nových validačních pravidel. `Form::MIN_LENGTH` ověřuje délku hesla. V popisce je použito zástupné `%d`. To je nahrazeno parametrem validačního pravidla, tedy číslem 6, protože popisky jsou před použitím zpracovány funkcí [sprintf |http://php.net/manual/en/function.sprintf.php]. Pravidlo `EQUAL` kontroluje, zda se hodnota políčka rovná zadané hodnotě. Jako hodnotu můžeme zadat buď statickou hodnotu, nebo odkaz na jiné políčko formuláře, v našem případě `$form['newPassword']`. + +Ve zpracování už asi není nic, co by nás mohlo zaskočit. Při zpracování jen ověříme, zda je zadané heslo platné a poté jej změníme. + +Pro tento presenter pak také samozřejmě musíme vytvořit šablonu. Půjde o `User/password.latte`: + +```html +{block content} + +<h1 n:block="title">Změna hesla</h1> + +{form passwordForm} +<div class="password-form"> + {control $form errors} + + <div class="pair"> + {label oldPassword /} + <div class="input">{input oldPassword}</div> + </div> + <div class="pair"> + {label newPassword /} + <div class="input">{input newPassword}</div> + </div> + <div class="pair"> + {label confirmPassword /} + <div class="input">{input confirmPassword}</div> + </div> + <div class="pair"> + <div class="input">{input set}</div> + </div> +</div> +{/form} +``` + +A ještě do hlavičky v `@layout.latte` přidáme odkaz, aby bylo možné heslo změnit: + +```html +<div id="header"> + <div id="header-inner"> + <div class="title"><a n:href="Homepage:">Úkolníček</a></div> + + {if $user->isLoggedIn()} + <div class="user"> + <span class="icon user">{$user->getIdentity()->name}</span> | + <a n:href="User:password">Změna hesla</a> | + <a n:href="signOut!">Odhlásit se</a> + </div> + {/if} + </div> +</div> +``` + + +Ukázali jsme si základy přihlašování uživatelů a aplikace již začíná být použitelná. Pomalu se chýlíme ke konci, ještě si ukážeme, jak uživateli zpříjemnit práci pomocí AJAXu. + +Zdrojové kódy aplikace v této fázi naleznete na [GitHubu |https://github.com/nette/book/tree/6]. diff --git a/dev/cs/book/components.texy b/dev/cs/book/components.texy new file mode 100644 index 0000000000..0e35d1e800 --- /dev/null +++ b/dev/cs/book/components.texy @@ -0,0 +1,280 @@ +Komponenty +********** + +.[perex] +Přesuneme seznam úkolů do komponenty. Budeme jej tak moci snadno použít kdekoliv v aplikaci a také jej budeme moci snadno upravovat. + +Často se dostaneme do situace, kdy bychom chtěli stejný nebo podobný prvek aplikace využít na více místech. Nette toto řeší pomocí [komponent |cs/components]. Jedná se o třídy, které reprezentují vykreslitelný objekt a je možné je do stránky opakovaně vkládat. + +Pojďme tedy naší tabulku s výpisem oddělit do samostatné komponenty. + + +Základ komponenty +================= + +Začneme tím, že si vytvoříme složku `app/components`. Do ní budeme ukládat námi vytvořené komponenty, a to včetně šablon. V ní si vytvoříme `TaskList.php`. Třída bude dědit od `Nette\Application\UI\Control` a bude mít pouze konstruktor a metodu `render()`: + +```php +namespace Todo; +use Nette; + +class TaskListControl extends Nette\Application\UI\Control +{ + /** @var Nette\Database\Table\Selection */ + private $selected; + + public function __construct(Nette\Database\Table\Selection $selected) + { + parent::__construct(); // vždy je potřeba volat rodičovský konstruktor + $this->selected = $selected; + } + + public function render(): void + { + $this->template->setFile(__DIR__ . '/TaskList.latte'); + $this->template->tasks = $this->selected; + $this->template->render(); + } +} +``` + +Všimněte si, že komponenta se vůbec nezajímá o `$parent` a `$name` jako to umí formulář. Nejsou totiž nezbytné, protože budeme komponenty v továrnách připojovat pomocí return. Konstruktor má tedy jediný povinný parametr - výraz, podle kterého budeme úkoly vybírat. + +Komponenta může být připojena k jakémukoliv objektu, který rozhraní `Nette\ComponentModel\IContainer` implementuje. Tím je i objekt `Control` samotný - do komponenty tak můžeme vnořovat libovolné jiné komponenty. V potomcích `Nette\ComponentModel\Container`, což jsou i `UI\Control` a formulář, můžeme použít stejné tovární metody `createComponent`, jaké jsme používali v presenteru. + +Metoda `render()` zajišťuje vykreslení šablony. Podobně jako u presenteru je i v komponentách k dispozici šablona. Před použitím se jí jen musí nastavit soubor, který se bude vykreslovat. To se udělá voláním `setFile()`. Pak už jen stačí do šablony přiřadit data, v našem případě tedy výraz, který jsme předali v konstruktoru, a šablonu vykreslit metodou `render()`. + +Šablona `TaskList.latte` bude téměř shodná s tím, co jsme používali v šabloně pro `TaskPresenter` a `HomepagePresenter`: + +```html +<table> + <thead> + <tr> + <th>Čas vytvoření</th> + <th>Úkol</th> + <th>Přiřazeno</th> + </tr> + </thead> + <tbody> + {foreach $tasks as $task} + <tr n:class="$iterator->isOdd() ? odd : even"> + <td>{$task->created|date:'j. n. Y'}</td> + <td>{$task->text}</td> + <td>{$task->user->name}</td> + </tr> + {/foreach} + </tbody> +</table> +``` + +Všimněte si jedné malé změny: u řádku tabulky se objevil atribut `n:class`. Jedná se o speciální latte makro, které umožňuje pohodlně nastavovat HTML elementům třídy. Jako parametry se mu předávají výrazy podobné ternárním operátorům oddělené čárkami. `$iterator->isOdd() ? odd : even` ověří, zda je splněno `$iterator->isOdd()`. Pokud ano, přidá třídu `odd`, jinak třídu `even`. Část `: even` je možno vynechat, pak se v případě nesplněné podmínky nebude přidávat nic. + +Proměnná `$iterator` obsahuje speciální objekt, který Latte vkládá mezi všechny cykly `foreach`. Pomocí něj můžeme zjišťovat, zda je momentální prvek v pořadí sudý nebo lichý, kolikátý v pořadí je a některé další věci. Více se dozvíte v dokumentaci k [makru `{foreach}` |cs/default-macros#cykly]. + +Výsledkem je v našem případě "zebrovaná" tabulka. + + +Použití +------- + +Nyní již zbývá jen komponentu použít. Nejprve v `TaskPresenter`u upravíme metodu `renderDefault()` a přidáme `createComponentTaskList()`: + +```php +public function renderDefault(int $id): void +{ + $this->template->list = $this->list; +} + + +protected function createComponentTaskList(): Todo\TaskListControl +{ + if ($this->list === null) { + $this->error('Wrong action'); + } + return new Todo\TaskListControl($this->listRepository->tasksOf($this->list)); +} +``` + +Výběr z databáze se přesunul do metody `createComponentTaskList()`. Úplně na začátku metody je podmínka a volání metody [error() |api:Nette\Application\UI\Presenter::error()] nad presenterem. + +```php +if ($this->list === null) { + $this->error('Wrong action'); +} +``` + +Metoda `error()` vyhazuje [api:Nette\Application\BadRequestException] s danou zprávou. Je to proto, že komponenty je možné používat napříč různými akcemi. Pokud bychom se snažili ke komponentě přistoupit pomocí úpravy URL, přes jinou akci, ve které by nebylo nastavováno `$this->list`, skončila by aplikace chybou. Tohle ji před chybou ochrání, protože se na produkci zobrazí prosté "stránka nenalezena" a ve vývojovém režimu laděnka. + +V šabloně `Task/default.latte` nyní místo tabulky stačí použít makro `{control}`: + +```html +{block content} + +<h1>{$list->title}</h1> + +<fieldset> + <legend>Přidat úkol</legend> + + {form taskForm} + <div class="task-form"> + {label text /} {input text size: 30, autofocus: true} {label userId /} {input userId} {input create} + </div> + {/form} +</fieldset> + +{control taskList} + +{/block} +``` + +A to je vše. Nyní podobou úpravu provedeme v `HomepagePresenter`u: + +```php +protected function createComponentIncompleteTasks(): Todo\TaskListControl +{ + return new Todo\TaskListControl($this->taskRepository->findIncomplete()); +} +``` + +Metoda `renderDefault` je nyní prázdná, o proto jí můžeme bez obav odstranit. Použití v šabloně `Homepage/default.latte` je opět velmi jednoduché: + +```html +{block content} + +<h1>Nesplněné úkoly</h1> + +{control incompleteTasks} + +{/block} +``` + +A to je vše. Úspěšně jsme oba výpisy úkolů nahradili komponentou. Zatím však máme pouze výpis. Bylo by dobré mít možnost i přidané úkoly označit jako splněné... + + +Signály +======= + +Signály umožňují komponentám reagovat na akce od uživatele. Příkladem signálu může být změna řazení tabulky, požadavek na zobrazení podrobnějších informací, odeslání formuláře, nebo právě označení úkolu jako splněného. Signál samotný je předán v adrese a jeho parametry jsou připojeny k aktuálním parametrům stránky. Je tedy realizován novým požadavkem na server. + +Před tím, než začneme signál psát, musíme naší komponentě navíc předat objekt modelu, aby vůbec mohla aktualizovat stav úkolu. To vyřešíme přidáním privátního atributu `$taskRepository` a upravením konstruktoru. + +```php +class TaskListControl extends Nette\Application\UI\Control +{ + /** @var Nette\Database\Table\Selection */ + private $selected; + + /** @var TaskRepository */ + private $taskRepository; + + public function __construct(Nette\Database\Table\Selection $selected, TaskRepository $taskRepository) + { + parent::__construct(); // vždy je potřeba volat rodičovský konstruktor + $this->selected = $selected; + $this->taskRepository = $taskRepository; + } +``` + +Předání modelu také musíme doplnit do metod, které vytvářejí komponentu. V `TaskPresenter`: + +```php +protected function createComponentTaskList(): Todo\TaskListControl +{ + return new Todo\TaskListControl($this->listRepository->tasksOf($this->list), $this->taskRepository); +} +``` + +V `HomepagePresenter`: + +```php +protected function createComponentIncompleteTasks(): Todo\TaskListControl +{ + return new Todo\TaskListControl($this->taskRepository->findIncomplete(), $this->taskRepository); +} +``` + +Komponenta nyní má k dispozici model a může jej využívat. Přidáme do ní tedy signál `markDone`. To provedeme vytvořením metody `handleMarkDone()`, která jako jediný parametr bude mít ID úkolu, který chceme označit jako splněný: + +```php +// class TaskListControl + +public function handleMarkDone(int $taskId): void +{ + $this->taskRepository->markDone($taskId); + $this->presenter->redirect('this'); +} +``` + +Opět si implementujeme metodu v třídě `Todo\TaskRepository`. + +```php +// class TaskRepository + +public function markDone(int $id): void +{ + $this->findBy(['id' => $id])->update(['done' => 1]); +} +``` + +Metoda `update` funguje obdobně, jako metoda insert. Před jejím voláním je však nutno specifikovat pomocí `where()`, jaké záznamy se mají upravit. Je nutné, aby `where()` bylo **před** voláním `update`, jinak se nejprve provede `UPDATE` bez `WHERE` (a tím pádem na celé tabulce) a až pak se přidají podmínky, které samozřejmě již nic neovlivní. + +Po provedení aktualizace opět musíme provést přesměrování, jinak by se stránka uložila do historie prohlížeče. Metodu `redirect()` je nutno volat nad presenterem. + +Signál z šablony zavoláme následovně: + +```html +<table class="tasks"> + <thead> + <tr> + <th class="created"> </th> + <th class="list" n:if="$displayList">Seznam</th> + <th class="text">Úkol</th> + <th class="user" n:if="$displayUser">Přiřazeno</th> + <th class="action"> </th> + </tr> + </thead> + <tbody> + {foreach $tasks as $task} + <tr n:class="$iterator->isOdd() ? odd : even, $task->done ? done"> + <td class="created">{$task->created|date:'j. n. Y'}</td> + <td class="list" n:if="$displayList">{$task->list->title}</td> + <td class="text">{$task->text}</td> + <td class="user" n:if="$displayUser">{$task->user->name}</td> + <td class="action"><a n:if="!$task->done" n:href="markDone! $task->id" class="icon tick">hotovo</a></td> + </tr> + {/foreach} + </tbody> +</table> +``` + +Upravili jsme sloupečky tabulky. V posledním sloupečku je nyní odkaz na označení úkolu jako splněného. `n:if` zajistí, že se zobrazí pouze u nesplněných úkolů. Odkaz `n:href` je velmi podobný způsobu odkazování, které jsme již dělali. Jako cíl odkazu je však uveden `markDone!`, tedy název signálu s vykřičníkem na konci. Signál je možné posílat vždy jen na aktuální akci presenteru, nelze tedy současně s ním zaslat změnu akce. Jinak platí stejná pravidla, jako v případě normálního odkazování - parametry můžeme, ale nemusíme pojmenovávat, pokud je uvedeme ve správném pořadí. + +Presenter je také svým způsobem komponentou. To mimo jiné znamená, že i v presenteru můžeme používat signály. Právě proto je na konci odkazu `!`. Slouží k rozlišení akce od signálu. V případě, že vytváříme odkaz v šabloně komponenty, můžeme vykřičník i vynechat, protože komponenta nemá akce. + +Další změnou je přidání třídy `done` řádkám s již splněnými úkoly. Vidíte tak makro `n:class` s více třídami v praxi. Také jsme přidali podmínku pro vykreslení uživatele. Oboje později využijeme pro zobrazení na úvodní stránce, kde budeme chtít některé sloupečky skrýt, nebo naopak nechat zobrazit. Pro toto podmíněné vykreslování budeme muset do komponenty ještě dopsat dva atributy: + +```php +/** @var boolean */ +public $displayUser = true; + +/** @var boolean */ +public $displayList = false; + +public function render(): void +{ + $this->template->setFile(__DIR__ . '/TaskList.latte'); + $this->template->tasks = $this->selected; + $this->template->displayUser = $this->displayUser; + $this->template->displayList = $this->displayList; + $this->template->render(); +} +``` + +Nyní bychom měli mít následujicí výsledek: + +[* 06-tasklist.webp *] + + +Vytvořili jsme komponentu, která nám umožní pohodlně zobrazit seznam úkolů kdekoliv v aplikaci. Příště se podíváme na přihlašování a ověřování uživatelů. + +Zdrojové kódy aplikace v této fázi naleznete na [GitHubu |https://github.com/nette/book/tree/5]. diff --git a/dev/cs/book/database.texy b/dev/cs/book/database.texy new file mode 100644 index 0000000000..4015989b87 --- /dev/null +++ b/dev/cs/book/database.texy @@ -0,0 +1,229 @@ +Databáze a model +**************** + +.[perex] +Připravíme si strukturu databáze a navrhneme modelovou vrstvu. Díky Nette Database to bude opravdu rychlé a pohodlné. + + +Databázová struktura +==================== + +Vytvoříme si databázi nazvanou `quickstart` a v ní tabulku uživatelů `user`, jejich úkolů `task` a seznamů úkolů `list`. + +[* 03-schema.webp *] + +Tabulka uživatelů `user` bude mít sloupce: + +- `id`: unikátní ID +- `username`: přihlašovací jméno +- `password`: hash hesla včetně soli (jako hashovací funkci použijeme Blowfish s mnoha opakováními, aby ani při použití hrubé výpočetní síly nebylo možné heslo zpětně uhádnout) +- `name`: skutečné jméno uživatele, které budeme zobrazovat v aplikaci + +Tabulka `list` bude jednoduchá: + +- `id`: unikátní ID +- `title`: název seznamu + +A nakonec tabulka úkolů `task`: + +- `id`: unikátní ID +- `text`: text úkolu +- `created`: čas, kdy byl úkol vytvořen +- `done`: příznak, zda byl úkol splněn +- `user_id`: ID uživatele, kterému je úkol přiřazen +- `list_id`: ID seznamu úkolů, do kterého je úkol zařazen + +Pro vytvoření databáze můžete využít [Adminer |http://adminer.org/], který už máte předinstalovaný na adrese `http://localhost/sandbox/www/adminer/`, a připravené [SQL s definicemi tabulek |https://github.com/nette/book/blob/master/mysql.structure.sql] a také [ukázková data |https://github.com/nette/book/blob/master/mysql.data.sql]. SQL je určeno pro databázi MySQL, jelikož je nepoužívanější, nicméně s drobnými úpravami bude fungovat i s jinou databází. + + +Připojení k databázi +==================== + +Parametry připojení k databázi uvedeme v konfiguračním souboru. Popis řetězce `dsn` najdete v [dokumentaci PHP |http://php.net/manual/en/ref.pdo-mysql.connection.php], zadejte i správné jméno a heslo. + +```neon +database: + dsn: 'mysql:host=localhost;dbname=quickstart' + user: + password: +``` + +.[note] +Při editaci konfiguračního souboru si dejte pozor na odsazování. Formát [NEON |http://ne-on.org/] akceptuje odsazení tabulátorem i mezerami, ale v celém souboru musí být použito stejné odsazení. V připraveném konfiguračním souboru jsou použity tabulátory. Nette se v případě problémů ozve. + + +Model +===== + +Doménový model je funkční základ celé aplikace a reprezentuje problém, který aplikace řeší. Patří sem entity (což jsou objekty reprezentující úkol, seznam úkolů a uživatele), popisuje vazby mezi nimi a chování (např. jak označit úkol za splněný) atd. Je nezávislý na prezentační logice, tedy té části aplikace, která model prezentuje uživateli a zpětně překládá jeho požadavky. + +Existuje řada možností, jak pojmout objektový návrh modelu a jak přistupovat k databázi. Vhodné je vytvořit třídy reprezentující jednotlivé entity a vazby mezi nimi. Přičemž perzistenci (tedy práci s databází) přenecháme samostatným třídám, tzv. data-mapperům, k čemuž lze využít třeba knihovnu Doctrine 2. Obvyklé činnosti prováděné nad entitami pak svěříme opět samostatným třídám, tzv. fasádám. + +Jinou možností, kterou použijeme při tvorbě naší velmi jednoduché aplikace, je rezignovat na zapouzdření databáze a použít nástroj `Nette\Database\Table`, což je jakýsi chytrý "průzkumník pro databázi". Dobereme se tak cíle mnohem rychleji a naše aplikace bude pokládat nejefektivnější SQL dotazy. + +Napišme si repozitáře `UserRepository`, `TaskRepository` a `ListRepository` ve jmenném prostoru `Todo` umožňující přístup k jednotlivým entitám ze stejnojmenných tabulek. Všechny budou dědit od společného abstraktního repozitáře `Repository`. + +Vytvoříme soubor `app/model/Repository.php`: + +```php +namespace Todo; +use Nette; + +/** + * Provádí operace nad databázovou tabulkou. + */ +abstract class Repository +{ + /** @var Nette\Database\Connection */ + protected $connection; + + public function __construct(Nette\Database\Connection $db) + { + $this->connection = $db; + } + + /** + * Vrací objekt reprezentující databázovou tabulku. + */ + protected function getTable(): Nette\Database\Table\Selection + { + // název tabulky odvodíme z názvu třídy + preg_match('#(\w+)Repository$#', get_class($this), $m); + return $this->connection->table(lcfirst($m[1])); + } + + /** + * Vrací všechny řádky z tabulky. + */ + public function findAll(): Nette\Database\Table\Selection + { + return $this->getTable(); + } + + /** + * Vrací řádky podle filtru, např. ['name' => 'John']. + */ + public function findBy(array $by): Nette\Database\Table\Selection + { + return $this->getTable()->where($by); + } + +} +``` + +Doporučujeme neuvádět koncovou značku PHP `?>`, jazyk ji nevyžaduje a nemůže se nám stát, že by se nám za ní dostal nějaký bílý znak, díky kterému by se nám znefunkčnily stránky. + +A následují soubory `app/model/UserRepository.php`: + +```php +namespace Todo; +use Nette; + +/** + * Tabulka user + */ +class UserRepository extends Repository +{ +} +``` + +`app/model/ListRepository.php`: + +```php +namespace Todo; +use Nette; + +/** + * Tabulka list + */ +class ListRepository extends Repository +{ +} +``` + +a `app/model/TaskRepository.php`: + +```php +namespace Todo; +use Nette; + +/** + * Tabulka task + */ +class TaskRepository extends Repository +{ +} +``` + + +A to je vše. Připravili jsme si základní kostry datového modelu. + + +Služby +====== + +Sekce `services` v konfiguračním souboru definuje takzvané služby, viz [Dependency Injection |/dependency-injection]. Ve výchozím konfiguračním souboru vidíme registraci 3 služeb. První z nich je `authenticator`, který se stará o ověřování platnosti uživatelského jména a hesla. Tuto službu využije později při implementaci přihlašování uživatelů. Zbývající 2 služby nastavují routování. + +```neon +services: + authenticator: Authenticator + + routerFactory: RouterFactory + router: @routerFactory::createRouter +``` + +Naše třídy datového modelu, které jsme před chvílí vytvořili, zaregistrujeme právě jako služby: + +```neon +services: + authenticator: Authenticator + + routerFactory: RouterFactory + router: @routerFactory::createRouter + + taskRepository: Todo\TaskRepository + userRepository: Todo\UserRepository + listRepository: Todo\ListRepository +``` + +Tím jsme zaregistrovali tři služby: `taskRepository`, která bude obsahovat instanci třídy `Todo\TaskRepository`, `userRepository`, která bude obsahovat instanci `Todo\UserRepository` a `listRepository`, která bude obsahovat instanci `Todo\ListRepository`. Všechny tři třídy vyžadují jeden parametr v konstruktoru - objekt `Nette\Database\Connection` zajišťující spojení s databází. Protože instance této třídy je v rámci aplikace jen jedna nemusí zde být nijak uvedena. + +Pokud bychom parametry chtěli explicitně uvést, vypadala by definice takto: + +```neon +services: + authenticator: Authenticator(@nette.database.default) + + routerFactory: RouterFactory + router: @routerFactory::createRouter + + taskRepository: Todo\TaskRepository(@nette.database.default) + userRepository: Todo\UserRepository(@nette.database.default) + listRepository: Todo\ListRepository(@nette.database.default) +``` + +Lepší je však zůstat u volání bez parametrů, abychom se o ně nemuseli starat. O vložení správné instance se v Nette stará takzvaný [autowiring |/di-configuration#autowiring]. + +Nyní je vhodná chvíle upravit si definici Authenticatoru. Definice třídy `Authenticator` říká, že má dostat jako jediný parametr konstruktoru `Nette\Database\Connection` (a Nette to pozná a automaticky ho tam předá). To nám ovšem nevyhovuje, raději autentikátoru předáme instanci naší třídy starající se o uživatele: `UserRepository`. + +```php +// Authenticator.php + +// odstraníme následující property +/** @var Nette\Database\Connection */ +private $database; + +// přidáme novou property pro repozitář uživatelů +/** @var Todo\UserRepository */ +private $repository; + +// upravíme konstruktor +public function __construct(Todo\UserRepository $repository) +{ + $this->repository = $repository; +} +``` + +Úspěšně jsme vytvořili strukturu databáze a několik jednoduchých repozitářů, které hned využjeme v presenterech.. + +Zdrojové kódy aplikace v této fázi naleznete na [GitHubu |https://github.com/nette/book/tree/2]. diff --git a/dev/cs/book/forms.texy b/dev/cs/book/forms.texy new file mode 100644 index 0000000000..0535a7af0e --- /dev/null +++ b/dev/cs/book/forms.texy @@ -0,0 +1,285 @@ +Formuláře +********* + +.[perex] +Vytvoříme si jednoduchý formulář pro zakládání nových úkolů a další pro vytváření nových seznamů. + + +Formuláře z Nette lze používat samostatně, nezávisle na zbytku frameworku. Pokud se je ale rozhodneme používat v rámci Nette aplikaci ve spojení s presentery a plným komponentovým modelem, bude jejich používání ještě o něco jednodušší a hlavně radostnější. + +Základní třídy sídlí ve jmenném prostoru `Nette\Forms`. Je zde jak základní třída formuláře, tak veškeré formulářové komponenty a třídy pro vykreslování. Pokud budeme formulář používat v Nette aplikaci, budeme ještě potřebovat třídu `Nette\Application\UI\Form`, která obsahuje napojení na presenter a využívá [signálů |/components#signal-neboli-subrequest] a [událostí |/SmartObject#udalosti]. + +Nejlepší ale bude si vše ukázat v praxi. + + +Formulář pro zadání úkolu +========================= + +Tento formulář bude zobrazen nad seznamem úkolů v dané kategorii. Jeho definici tedy umístíme do `TaskPresenter`u. Bude obsahovat políčko s textem a jeden selectbox na výběr uživatele, kterému má být úkol přiřazen. + +Abychom mohli vybrat, komu je úkol přiřazen, budeme potřebovat uživatele. Metodu `startup()` máme v TaskPresenteru už definovanou, takže si ji rozšíříme o získání modelu pro uživatele. + +```php +// class TaskPresenter + +/** @var Todo\UserRepository */ +private $userRepository; + +protected function startup(): void +{ + parent::startup(); + $this->listRepository = $this->context->listRepository; + $this->userRepository = $this->context->userRepository; // získáme model pro práci s uživateli +} +``` + +Definice formuláře bude vypadat následovně: + +```php +protected function createComponentTaskForm(): Form +{ + $userPairs = $this->userRepository->findAll()->fetchPairs('id', 'name'); + + $form = new Form(); + $form->addText('text', 'Úkol:', 40, 100) + ->addRule(Form::FILLED, 'Je nutné zadat text úkolu.'); + $form->addSelect('userId', 'Pro:', $userPairs) + ->setPrompt('- Vyberte -') + ->addRule(Form::FILLED, 'Je nutné vybrat, komu je úkol přiřazen.'); + $form->addSubmit('create', 'Vytvořit'); + return $form; +} +``` + +Abychom mohli použít volání `new Form()`, musíme na začátku souboru uvést deklaraci `use Nette\Application\UI\Form;`. + +Funkce `createComponentTaskForm()` je speciální tovární funkcí na komponenty. Kdykoliv presenter požádáme o instanci komponenty s názvem `taskForm`, tak se nejprve podívá, zda již takovou komponentu nemá vytvořenou. Pokud ne, tak si zavolá právě tuto funkci. Funkce buď jen komponentu vytvoří a vrátí ji jako svou návratovou hodnotu, nebo jí rovnou připojí k presenteru. + +[Tovární metody na komponenty |cs/presenters#tovarnicky-na-komponenty] jsou volány samotným presenterem. Neměly by být volány přímo, ale je nutné, aby se k nim dostal presenter, proto musí být `protected`. + +Přímé připojení vypadá takto a je možné jej zavolat kdekoliv v presenteru, nejenom v továrničce + +```php +protected function createComponentTaskForm(): Form +{ + $form = new Form($this, 'taskForm'); + //... + // return $form; by bylo zde zbytečné +} +``` + +Všechny třídy, které dědí od `Nette\ComponentModel\Component` a nebyl jim změněn konstruktor, mohou být takto připojeny k rodiči (v tomhle případě formulář k presenteru). První argument je rodič (presenteru) a druhý název komponenty `taskForm`. Továrna dostává název komponenty automaticky jako první argument, takže do kódu nemusíme název psát přímo a snížíme tak riziko překlepu: + +```php +protected function createComponentTaskForm(string $name): Form +{ + $form = new Form($this, $name); + // ... +} +``` + +Pokud formulář připojíte ihned v konstruktoru, budou si jednotlivé prvky při sestavování průběžně načítat data, která byla odeslána. Což se může hodit, pokud potřebujete podmínit vytvoření některých prvů, nebo validačních pravidel. Jinak jsou oba zápisy funkčně prakticky stejné. + +Pojďme si nyní projít jednotlivé prvky formuláře. + +```php +$form->addText('text', 'Úkol:', 40, 100) + ->addRule(Form::FILLED, 'Je nutné zadat text úkolu.'); +``` + +Přidá nové textové políčko s názvem `text` a popiskou `Úkol:`. Jeho velikost bude 40 znaků a maximální délka 100. Metoda `addRule` přidává validační pravidlo. Prvním parametrem je konstanta, která udává typ pravidla. Pravidlo `Form::FILLED` ověřuje, zda bylo políčko vyplněno. Druhý parametr je nepovinný a definuje hlášku, která se uživateli zobrazí v případě, že pravidlo nebylo splněno. Metoda má ještě třetí nepovinný parametr a tím jsou parametry validace, například v případě pravidla `Form::MIN_LENGTH` udává tento parametr minimální délku řetězce, který uživatel musí zadat. + +.[note] +Další validační pravidla jsou uvedena v dokumentaci k formulářům. [Obecná validační pravidla |/forms#validace] lze aplikovat na všechny prvky, dále pak má každý prvek vlastní sadu pravidel, která na něj lze aplikovat. Podívejte se například na pravidla k [textovému políčku |/forms#addtext]. + +```php +$prompt = Html::el('option')->setText("- Vyberte -")->class('prompt'); +$form->addSelect('userId', 'Pro:', $userPairs) + ->setPrompt($prompt) // je možné předat text i prvek HTML + ->addRule(Form::FILLED, 'Je nutné vybrat, komu je úkol přiřazen.'); +``` + +Tento kód přidává do formuláře selectbox. První dva parametry jsou stejné, jak v předchozím případě. Třetí argument je asociativní pole ve tvaru `hodnota` -> `popis volby`. Takové pole můžeme získat metodou `fetchPairs()` zavolanou nad objektem tabulky. `fetchPairs('id', 'name')` použije jako klíč v poli ID uživatele a jako popisku volby jeho jméno. Metoda `setPrompt()` přidá na začátek volbu s danou popiskou a prázdnou hodnotou. Taková volba pak nám umožňuje oddělit situaci, kdy uživatel prvek skutečně nevyplnil, a kdy jen ponechal v prvku výchozí hodnotu. V kombinaci s validačním pravidlem `Form::FILLED` pak způsobí, že uživatel musí vždy nějaký prvek vybrat. + +Posledním prvkem je odesílací tlačítko: + +```php +$form->addSubmit('create', 'Vytvořit'); +``` + +Parametry jsou opět stejné. Popiska tlačítka tak bude `Vytvořit`. + +Nyní máme tento jednoduchý formulář téměř kompletní. Zbývá nám jej jen vykreslit. Přesuneme se tedy do šablony `Task/default.latte` a nad tabulku s výpisem prvků přidáme: + +```html +<fieldset> + <legend>Přidat úkol</legend> + + {control taskForm} +</fieldset> +``` + +Povšimněte si nového makra `{control}`. To zajistí vykreslení komponenty se zadaným názvem. V našem případě presenter nejprve zjistí, zda již existuje komponenta s názvem `taskForm`. Pokud neexistuje, zavolá si metodu `createComponentTaskForm` a komponentu si vytvoří. Pak zajistí vykreslení pomocí metody `render()`, kterou má formulář definovanou. Blíže si ji představíme v další části. + +Na stránce bychom teď měli vidět následující výsledek: + +[* 05-task-form-control.webp *] + +Zkusíme si cvičně přidat úkol. Vyplníme text úkolu, vybereme komu má být přiřazen a potvrdíme... Ouha, ale nic se nestalo! To proto, že prozatím nemáme napsanou obsluhu odeslaného formuláře. K tomu slouží událost `onSuccess`, která se vykoná po úspěšném odeslání formuláře, což také znamená, že pokud formulář obsahuje validační pravidla, musí být správně vyplněn. Za přidání tlačítka pro odeslání formuláře tedy přidáme ještě jeden řádek: + +```php +$form->addSubmit('create', 'Vytvořit'); +$form->onSuccess[] = [$this, 'taskFormSubmitted']; // naše událost pro zpracování formuláře +``` + +Ten nastavuje formuláři, že po jeho úspěšném odeslání se má vykonat metoda `taskFormSubmitted` z objektu `$this`, tedy z aktuálního presenteru. To ovšem znamená, že jí musíme vytvořit. + +```php +public function taskFormSubmitted(Nette\Application\UI\Form $form, stdClass $values): void +{ + $this->taskRepository->createTask($this->list->id, $form->values->text, $form->values->userId); + $this->flashMessage('Úkol přidán.', 'success'); + $this->redirect('this'); +} +``` + +Budeme v ní potřebovat zbývající službu `taskRepository`, takže si ji předáme do presenteru. + +```php +/** @var Todo\TaskRepository */ +private $taskRepository; + +protected function startup(): void +{ + parent::startup(); + $this->listRepository = $this->context->listRepository; + $this->userRepository = $this->context->userRepository; + $this->taskRepository = $this->context->taskRepository; // předání potřebné služby +} +``` + +A vytvoříme si v ní metodu, která nám zpracuje vložení záznamu + +```php +// class TaskRepository + +public function createTask($listId, $task, $assignedUser) +{ + return $this->getTable()->insert([ + 'text' => $task, + 'user_id' => $assignedUser, + 'created' => new \DateTime(), + 'list_id' => $listId + ]); +} +``` + +Nyní ji můžeme ve zpracování formuláře zavolat. + +.[note] +Narozdíl od tovární metody je zpracování události voláno samotnou komponentou, nikoliv presenterem. Proto **musí** být `public`. + +Tato metoda dostane jako jediný parametr instanci formuláře, který byl odeslán. Do databáze vloží pomocí metody `insert()` data nového úkolu. Data jsou předána jako asociativní pole, kde jako klíč je uveden název sloupečku. Povšimněte si, že pro naplnění sloupce `created` jsme použili instanci `DateTime()`. Nette samo před vložením čas převede na formát používaný databází. Sloupeček `done` uvedený není, neboť má výchozí hodnotu 0. + +Metoda `flashMessage()` vypíše uživateli tzv. [flash zprávu |cs/presenters#flash-zpravy]. Jedná se o jednorázové oznámení o výsledku stavu akce. O jejich vykreslení se pak staráme v šabloně `@layout.latte`. Druhý parametr udává třídu zprávy, v tomto případě použijeme `success`. Hlášení pak díky definici stylů z minulé části bude mít krásnou uklidňující zelenou barvu. + +Metoda `redirect()` pak konečně provede přesměrování. Má stejné parametry, jako jsme uváděli v šabloně makru `{link}`. Klíčové slovo `this` místo dvojice `Presenter:action` nás přesměruje na aktuální stránku se stejnými parametry. Toto přesměrování je velmi důležité. Pokud bychom ho neprovedli, uživateli by se uložil odeslaný `POST` formulář do historie prohlížeče. Pokud by se pak na tuto stránku vrátil, formulář by se odeslal znovu a tím pádem by se záznam v databázi vytvořil ještě jednou. Podobně by se pak chovalo obnovení stránky. + +Nyní již vytváření úkolů bude plně funkční. Vzhled formuláře však není ideální. Nette vykresluje formuláře do tabulek, takže se nám do formuláře trochu vmíchal styl seznamu úkolů. Pojďme si to tedy napravit. + + +Vlastní vykreslení formuláře +---------------------------- + +Pokud chceme jednotlivé prvky formuláře vykreslit ručně, můžeme tak učinit pomocí latte maker `{form}`, `{input}` a párového `{label /}`. Jejich názvy mluví za vše, proto si je ukážeme v praxi: + +```html +{form taskForm} +<div class="task-form"> + {control $form errors} + + {label text /} {input text size: 30, autofocus: true} {label userId /} {input userId} {input create} +</div> +{/form} +``` + +Počáteční `{form taskForm}` říká, že budeme vykreslovat formulář s názvem `taskForm`. Uvnitř je jeden vnořený `<div>` a v něm na jedné řádce všechny prvky formuláře. + +Ještě před nimi je však nutné zajistit případné vykreslení chyb ve vyplněném formuláři. To zajistíme makrem `{control $form errors}`. Před odesláním se sice provádí kontrola JavaScriptem u klienta, pokud jej ale bude mít uživatel vypnutý, formulář se odešle a je nutné mu chyby zobrazit takto. Některé chyby navíc není možné u klienta ověřit, proto na výpis chyb nesmíme zapomenout. + +Pak hned následují jednotlivé prvky. První `{label text /}` vykreslí popisku pro prvek `text`. Jak již bylo řečeno, makro `{label /}` je párové. Buď můžeme do jeho obsahu napsat popisku přímo v šabloně (`{label text}Text:{/label}`), nebo makro ukončit podobně jako HTML tag. V takovém případě se použije popiska zadaná při definici formuláře. + +Makro `{input text}` vykreslí samotný formulářový prvek. Volitelně mu lze přidat seznam atributů, které chceme HTML elementu přiřadit. Zde nastavujeme `size` na 30 a povolujeme `autofocus`, který je podporován jen v novějších prohlížečích. + +Výsledný formulář bude nyní vypadat takto: + +[* 05-task-form-manual.webp *] + +Nyní vytvoříme další formulář, tentokrát pro vytvoření seznamu úkolů. + + +Formulář na vytvoření seznamu úkolů +----------------------------------- + +Tento formulář by bylo vhodné umístit do levého sloupce, ihned pod seznam úkolů. Bude zobrazen na každé stránce, proto jej budeme definovat v `BasePresenteru`. Protože tento formulář je velmi podobný, rovnou uveďme kód. `BasePresenter`: + +```php +protected function createComponentNewListForm(): Form +{ + $form = new Form(); + $form->addText('title', 'Název:', 15, 50) + ->addRule(Form::FILLED, 'Musíte zadat název seznamu úkolů.'); + $form->addSubmit('create', 'Vytvořit'); + $form->onSuccess[] = [$this, 'newListFormSubmitted']; + return $form; +} + +public function newListFormSubmitted(Nette\Application\UI\Form $form, stdClass $values): void +{ + $list = $this->listRepository->createList($form->values->title); + $this->flashMessage('Seznam úkolů založen.', 'success'); + $this->redirect('Task:default', $list->id); +} +``` + +A ještě si implementujeme metodu `createList` ve třídě `ListRepository` + +```php +public function createList(string $title) +{ + return $this->getTable()->insert([ + 'title' => $title + ]); +} +``` + +Povšimněte si přesměrování. Metoda `insert()` vrací záznam, který byl vložen do databáze. Můžeme tak tedy snadno provést přesměrování na nově vytvořený seznam úkolů. + +V šabloně `@layout.latte` upravíme postranní panel: + +```html +<div id="sidebar"> + <div class="task-lists"> + <ul> + <li n:foreach="$lists as $list"><a n:href="Task: $list->id">{$list->title}</a></li> + </ul> + </div> + + <fieldset> + <legend>Nový seznam</legend> + {form newListForm} + <div class="new-list-form"> + {control $form errors} + + {input title} + {input create} + </div> + {/form} + </fieldset> +</div> +``` + + +Vytvořili jsme formuláře jak pro vkládání úkolů, tak pro zakládání nových seznamů. Nyní si napíšeme vlastní komponentu. + +Zdrojové kódy aplikace v této fázi naleznete na [GitHubu |https://github.com/nette/book/tree/4]. diff --git a/dev/cs/book/presenter.texy b/dev/cs/book/presenter.texy new file mode 100644 index 0000000000..6f8c38e261 --- /dev/null +++ b/dev/cs/book/presenter.texy @@ -0,0 +1,452 @@ +Presentery a šablony +******************** + +.[perex] +Nyní si představíme presentery a vše, co s nimi souvisí. Ukážeme si, jak napsat vlastní presenter, jak psát v šablonovacím jazyce Latte a jak využívat model, který jsme již dříve napsali. + +Na začátek se bohužel nevyhneme troše teorie. Nette využívá architekturu MVP, [Model-View-Presenter |cs/presenters]. Základními kameny této architektury jsou: + +- **Model** - datová a funkční vrstva aplikace, která se stará o ukládání dat a aplikační logiku. Jakoukoliv událost uživatele (přihlášení, zobrazení či změna dat, vložení zboží do košíku) představuje akci modelu. Ten má pevně dané rozhraní, pomocí kterého s ním ostatní části aplikace komunikují, a sám o svém okolí nic neví. +- **View**, nebo také "pohled" - stará se o samotné vykreslení výsledku požadavku uživatele. V Nette tuto část představují šablony. +- **Presenter** - obě předchozí vrstvy spojuje dohromady. Nejprve na základě požadavku od uživatele vyvolá příslušnou aplikační logiku (např. zmíněné přidání zboží do košíku či zobrazení dat) a pak požádá view o vykreslení výsledku. + +Architektura MVP je podobná architektuře MVC((Model-View-Controller)). Obě architektury se liší hlavně v úloze jejich centrálního kamene, tedy Presenter × Controller. Presenter hraje čistě roli prostředníka, který jen volá model a výsledky předává view, kdežto Controller má navíc na starosti i některé události uživatelského rozhraní. + +Model již máme díky Nette Database připravený. Zbývá nám tedy presenter a view. V Nette má každý presenter vlastní sadu views, takže budeme obojí psát souběžně. + + +HomepagePresenter +================= + +Jak již bylo řečeno, presenter je třída, která spolupracuje s modelem a výsledná data předává view. Nejprve upravíme `HomepagePresenter`. Ten bude zajišťovat zobrazení úvodní stránky aplikace. V kostře aplikace vypadá kód této třídy následovně: + +```php +class HomepagePresenter extends BasePresenter +{ + + public function renderDefault(): void + { + $this->template->anyVariable = 'any value'; + } + +} +``` + +Má tedy jednu jedinou metodu: `renderDefault()`. Ta se stará o předání dat view, který se bude jmenovat `default`. Šablonu tohoto view naleznete ve složce `templates/Homepage/`, konkrétně se jedná o soubor `default.latte`. Při běhu aplikace se nejprve vykonají požadované akce v presenteru, až pak se podle toho vykresluje šablona. Posloupnost těchto kroků zachycuje následující diagram: + +.<> +[* lifecycle2.webp *] + +V diagramu si povšimněte metody `action<action>()`. V souvislosti s ní si musíme zavést nový pojem, a to "akce presenteru". Nejlepší bude tento pojem vysvětlit na příkladu: akcí presenteru může být například "zobrazení produktu". Interně jí nazveme `show`. Pokud chceme ale produkt zobrazit, může dojít k několika stavům: produkt je na skladě a můžeme jej objednat, produkt mohl být stažen z prodeje, vybraný produkt již nemusí existovat... Právě pro tyto jednotlivé stavy pak vytvoříme vlastní view, můžeme je tedy chápat jako konkrétní realizace zobrazení dané akce. + +Ve většině případů bude platit, že co akce, to view, proto pokud se o nic nestaráme, vykonává se akce a view se stejným jménem. V našem `HomepagePresenter`u by se tedy před metodou `renderDefault()` volala metoda `actionDefault()`, pokud by existovala. Během ní se však můžeme rozhodnout view změnit voláním metody `setView($view)`, například `$this->setView('error')`. Pak by po vykonání metody `actionDefault()` nedošlo k volání metody `renderDefault()`, ale `renderError()`. Také by se vykreslovala šablona `error.latte`. + +Nyní ale zpět k psaní kódu. Pokud chceme v šabloně nějaká data zobrazit, musíme presenter nějakým způsobem spojit s modelem. Model máme zaregistrovaný jako službu, takže se k němu i k jeho tabulkám můžeme pohodlně dostat. + + +Spojení s modelem +----------------- + +Protože jsme naše třídy modelu šikovně definovali jako služby, můžeme je okamžitě použít. Předáme si co potřebujeme do property v metodě `startup()`. V configu máme třídu `Todo\TaskRepository` zaregistrovanou jako službu se jménem `taskRepository`, takže se bude volat pomocí `$this->context->taskRepository`. + +```php +// class HomepagePresenter + +/** @var Todo\TaskRepository */ +private $taskRepository; + +protected function startup(): void +{ + parent::startup(); + $this->taskRepository = $this->context->taskRepository; +} +``` + +Budeme chtít zobrazit všechny nesplněné úkoly a seřadit vzestupně je podle času vytvoření. Vytvoříme si tedy metodu na výběr nesplněných úkolů, ve třídě `Todo\TaskRepository` + +```php +// class TaskRepository + +public function findIncomplete() +{ + return $this->findBy(['done' => false])->order('created ASC'); +} +``` + +A pomocí ní vybereme z tabulky `task` data a vložíme je do šablony. + +```php +// class HomepagePresenter + +public function renderDefault(): void +{ + $this->template->tasks = $this->taskRepository->findIncomplete(); +} +``` + +Nette Database poskytuje pro dotazování do databáze takzvané "fluent interface". Dotaz postupně skládáme zřetězením volání funkcí, které budou reprezentovat jednotlivé části dotazu. Podporovány jsou téměř všechny aspekty jazyka SQL. Tato abstrakce je poměrně užitečná - pokud později vyměníme SQL databázi za jiné úložiště, stačí pouze implementovat objekty se stejným rozhraním a funkčnost zůstane stejná. Toto rozhraní ukážeme později na složitějších dotazech. + +Voláním `where()` nad objektem tabulky definujeme podmínku, podle které chceme záznamy vybírat. Můžeme jí předat asociativní pole ve formátu `sloupec` -> `hodnota`, případně rovnou kus SQL dotazu s parametry pokud nám prosté vybírání podle rovnosti nestačí, nebo chceme použít složitější podmínky. Podobně voláním `order()` zajistíme setřídění dat vzestupně podle sloupce `created`. + +Důležitou vlastností těchto objektů je, že jednu instanci je možné použít pouze na jeden dotaz. Pokud bychom nad jedním objektem zkoušeli sestavit dva dotazy, zamíchaly by se nám do druhého dotazu i části z toho prvního. Proto máme taky třídy, které dědí od `Todo\Repository`, která vytváří vždy novou instanci [Selection |api:Nette\Database\Table\Selection], pomocí metody [Connection::table() |api:Nette\Database\Connection::table()] + +.[note] +Zvídavé čtenáře opět odkazuji do dokumentace na téma [Databáze & ORM |/database]. + +Získaný objekt poté přiřadíme do šablony pomocí `$this->template->tasks`. Tím definujeme v šabloně proměnnou `$tasks`, kterou můžeme používat. Takto můžeme šabloně předat prakticky libovolná data v libovolné proměnné. + + +Šablona +------- + +V šabloně `default.latte` máme nyní k dispozici proměnnou `$tasks`. Pojďme ji tedy využít. V šabloně je nyní uvítací stránka, tu můžeme bez obav smazat a nahradit jí vlastním kódem: + +```html +{block content} + +<h1>Nesplněné úkoly</h1> + +<table> + <thead> + <tr> + <th>Čas vytvoření</th> + <th>Úkol</th> + <th>Přiřazeno</th> + </tr> + </thead> + <tbody> + {foreach $tasks as $task} + <tr> + <td>{$task->created|date:'j. n. Y'}</td> + <td>{$task->text}</td> + <td>{$task->user->name}</td> + </tr> + {/foreach} + </tbody> +</table> + +{/block} +``` + +Ve vypisování šablony nám pomáhají takzvaná makra. Ta jsou uvedena ve složených závorkách a zastávají funkci různých jazykových konstrukcí. Šablony můžeme psát i bez nich, ale proč bychom to dělali, že? + +.[note] +Seznam všech maker je samozřejmě v dokumentaci: [Výchozí Latte makra |/default-macros]. + +`{block content}` a `{/block}` definuje obsahový blok. Nette používá takzvanou dědičnost šablon. Hlavní šablona definuje bloky, které pak zděděné šablony mohou (a některé musí) přepsat. Pokud není uvedeno jinak, tak šablona view dědí od šablony `@layout.latte`, kterou naleznete přímo ve složce `app/templates`. V ní se nachází řádek: + +```html +{include content} +``` + +Ten říká, že místo něj se má vložit blok s názvem `content`. Protože blok jinde definován není, je nutné tento blok definovat ve zděděné šabloně. Hned pod hlavičkou je uveden `{block head}{/block}`. Takový blok je možno přepsat, ale není to nutné. Pokud přepsán nebude, použije se místo něj výchozí obsah (zde prázdný řetězec). + +Dále máme v bloku klasickou tabulku. Ta obsahuje `<thead>` a `<tbody>` tak, jak jsme zvyklí. Uvnitř `<tbody>` je makro `{foreach}`. To se chová jako klasický `foreach` v PHP. Prochází proměnnou `$tasks` a položky ukládá do proměnné `$task`. V ní máme od databázové vrstvy jednotlivé sloupce. Všimněte si, že při výpisu se nemusíme starat o escapování pomocí `htmlspecialchars()`. Nette se o escapování postará samo. Dokonce escapování provádí v závislosti na kontextu - v bloku JavaScriptu bude používat `json_encode()`, v HTML `htmlspecialchars()`... + +Podívejme se hned na první sloupeček s časem: + +```html +<td>{$task->created|date:'j. n. Y'}</td> +``` + +Konstrukcí za svislicí `|` zajistíme, že před výpisem proměnné se na ní aplikuje tzv. helper. Helpery jsou malé funkce, které nám pomáhají jednoduše formátovat výpisy v šabloně. Helper `date` vypíše zadané datum v nějakém přívětivějším formátu. Tento formát je mu zadán jako parametr za dvojtečkou. Specifikátor může být stejný, jako v případě funkce `date()`, případně `strftime()`. + +.[note] +Přehled všech helperů, které jsou k dispozici, je k nahlédnutí v dokumentaci v kapitole [Helpery |/default-helpers]. + +Další zajímavostí je konstrukce `$task->user->name`. Jak již bylo řešeno v předchozí části věnované databázi, sloupec `user_id` považuje databázová vrstva za odkaz na tabulku `user`. Díky tomu se pak můžeme takto odkázat na tabulku `user`. + +Nyní si již můžeme výsledek zobrazit: + +[* 04-list-table.webp *] + +Povšimněte si panelu vpravo dole. Tento panel Nette zapíná jen ve vývojovém prostředí a zobrazují se na něm ladící informace. Po připojení k databázi nám v panelu přibyla nový položka - položené dotazy. Najetím na ní se nám zobrazí seznam všech dotazů, které byly pro vygenerování stránky použity. Dotaz si můžeme ihned nechat "vysvětlit" (explain). + +[* 04-debug-panel-sql.webp *] + +Nyní se ale vrhneme na další presenter. + + +TaskPresenter +============= + +Tato třída již v kostře není. Musíme si jí tedy vytvořit. Ve složce `app/presenters` vytvořte nový soubor `TaskPresenter.php`. Umístíme do ní prozatím prázdnou třídu, která bude dědit z `BasePresenter`u: + +```php +/** + * Presenter, který zajišťuje výpis seznamů úkolů. + */ +class TaskPresenter extends BasePresenter +{ + +} +``` + +`BasePresenter` je abstraktní společný předek všech presenterů v aplikaci. Můžeme v něm například provádět inicializaci, načítat data, která mají být společná pro všechny presentery a další zajímavé věci. + +```php +// class TaskPresenter + +/** @var @var Todo\ListRepository */ +private $listRepository; + +protected function startup(): void +{ + parent::startup(); + $this->listRepository = $this->context->listRepository; +} +``` + +V `TaskPresenter`u budeme chtít zobrazit jeden konkrétní seznam úkolů. Seznam, který chceme zobrazit, dostane presenter jako ID v adrese. Vytvoříme si tedy další privátní property `$list`, do kterého uložíme v metodě `actionDefault` záznam o seznamu úkolů: + +```php +// class TaskPresenter + +/** @var Nette\Database\Table\ActiveRow */ +private $list; + +public function actionDefault(int $id): void +{ + $this->list = $this->listRepository->find($id); +} +``` + +Parametr `$id` bude obsahovat identifikátor, který bude hledán v databázi. Nette si samo zkontroluje `action<action>()` a `render<view>()` metody a pokud mají nějaké argumenty, které přišly adresou, tak je předá podle jména. Více o tomto mechanismu se dozvíme v [sekci o presenterech |cs/presenters#zpracovani-akce-presenteru], prozatím se spokojíme s, pro programátora nejlepším závěrem, "funguje to a nemusíme se o to starat". + +Ve třídě `Todo\ListRepository` si vytvoříme metodu, která nám z objektu [ ActiveRow |api:Nette\Database\Table\ActiveRow ], obsahujícího řádek z tabulky list, vrátí všechny související úkoly. + +```php +// class ListRepository + +public function tasksOf(Nette\Database\Table\ActiveRow $list) +{ + return $list->related('task')->order('created'); +} +``` + +Získaný záznam spolu se seznamem úkolů předáme šabloně v metodě `renderDefault()`: + +```php +// class TaskPresenter + +public function renderDefault(): void +{ + $this->template->list = $this->list; + $this->template->tasks = $this->listRepository->tasksOf($this->list); +} +``` + + +Šablona +------- + +K view nám zbývá napsat jen šablonu. Ve složce `templates` vytvoříme podsložku `Task` a do ní šablonu `default.latte`. Bude velmi podobná té předchozí: + +.[note] +Při vytváření šablon záleží na velikost písmen. Složka s názvem presenteru by měla začínat velkým písmenem, soubor se šablonou malým. Při vývoji na Windows to nepoznáme, ale pokud aplikaci přeneseme na linuxový server, způsobí to řadu chyb. + +```html +{block content} + +<h1>{$list->title}</h1> + +<table> + <thead> + <tr> + <th>Čas vytvoření</th> + <th>Úkol</th> + <th>Přiřazeno</th> + </tr> + </thead> + <tbody> + {foreach $tasks as $task} + <tr> + <td>{$task->created|date:'j. n. Y'}</td> + <td>{$task->text}</td> + <td>{$task->user->name}</td> + </tr> + {/foreach} + </tbody> +</table> + +{/block} +``` + +Nyní už bude vše fungovat! Počkat... ale jak se k nově vytvořené stránce dostat? Možná by bylo dobré si ještě vytvořit menu s výpisem všech seznamů úkolů.... + + +Navigace +======== + +Menu bude společné pro všechny části aplikace. Žhavý kandidát na umístění do `BasePresenter`u! Budeme jej muset vykreslit na každé stránce, takže použijeme metodu `beforeRender()`, která se provede vždy, nezávisle na view, které má být vykreslováno. Nejprve si ale `Todo\ListRepository` umístíme do privátní property, abychom s modelem mohli pracovat. + +```php +// class BasePresenter + +/** @var Todo\ListRepository */ +private $listRepository; + +protected function startup(): void +{ + parent::startup(); + $this->listRepository = $this->context->listRepository; +} + +protected function beforeRender(): void +{ + $this->template->lists = $this->listRepository->findAll()->order('title ASC'); +} +``` + +Ještě musíme zajistit, aby se menu zobrazilo na každé stránce. Protože každá šablona view dědí od šablony `@layout.latte`, umístíme vykreslení právě do ní. Připravíme si rovnou základ pro nějaké rozumné rozvržení. Obsah tagu `<body>` upravíme následovně: + +```html +<div id="header"> + <div id="header-inner"> + <div class="title"><a href="{link Homepage:}">Úkolníček</a></div> + </div> +</div> + +<div id="container"> + <div id="sidebar"> + <div class="task-lists"> + <ul> + <li n:foreach="$lists as $list"><a href="{link Task: $list->id}">{$list->title}</a></li> + </ul> + </div> + </div> + + <div id="content"> + <div n:foreach="$flashes as $flash" class="flash {$flash->type}">{$flash->message}</div> + + {include content} + </div> + + <div id="footer"> + + </div> +</div> +``` + +Atribut `n:foreach` je druhý způsob, jak iterovat polem. Tento zápis je ekvivalentní, jako kdyby byl `foreach` umístěn kolem HTML tagu, ve kterém je uveden. Podobně funguje většina maker, která řídí tok programu: `if`, `for`, `while`... Pokud před makro napíšeme prefix `inner-`, bude se chovat, jako kdyby bylo napsáno uvnitř HTML tagu. Uvedený zápis bychom tedy mohli poupravit takto: + +```html +<ul n:inner-foreach="$lists as $list"> + <li><a href="{link Task: $list->id}">{$list->title}</a></li> +</ul> +``` + +Výsledek by byl nezměněn. + +Zajímavější pro nás však bude nové makro `{link Task: $list->id}`. To vytváří URL adresu. Jako první parametr je cílová dvojice `Presenter:action`. Pokud `action` neuvedeme a za dvojtečku nic nenapíšeme, použije se výchozí, tedy default. Pokud se chceme odkazovat na jinou akci ve stejném presenteru, nemusíme presenter uvádět. Pak nesmí být uvedena ani dvojtečka, která má speciální význam. + +Jako další jsou uvedeny parametry, které se mají v adrese předat. Pokud je nepojmenujeme, tak je musíme uvést ve stejném pořadí, v jakém jsou uvedeny v signatuře akce (tedy metody `action<action>()`, případně `view<action>()`, pokud předchozí neexistuje). Parametry oddělujeme čárkou a pokud je chceme pojmenovat, uděláme to stejně jako v případě klíčů pole. Výše uvedený zápis je tedy ekvivalentní s ukecanějším + +```html +<li><a href="{link Task: id: $list->id}">{$list->title}</a></li> +``` + +Pokud si chceme zápis ještě více usnadnit, existuje ještě makro `n:href`: + +```html +<li><a n:href="Task: $list->id">{$list->title}</a></li> +``` + +Nyní bychom již na stránce měli vidět jednoduché menu. Kliknutím se dostaneme na výpis konkrétního seznamu úkolů. + +[* 04-presenter-nav.webp *] + +Podívejme se na adresu, kterou Nette pro odkazy vytvořilo: `/task/default/2`. Jak je vidět, v adrese je uveden presenter, akce i zadané ID. Tvar těchto adres lze plně ovlivnit pomocí routování. Opět bez jakýchkoliv zásahů do jiných částí aplikace. + +Pojďme si trošku zaexperimentovat. Co se stane, když změníme ID na neexistující? Chyba! + +[* 04-debugger.webp *] + +Ukázala se nám laděnka v celé své kráse. Jedná se o jeden z nejužitečnějších nástrojů v Nette. Ukazuje, kde přesně k chybě došlo a zachycuje stav aplikace v době chyby. Pokud by chyba byla v SQL dotazu, ukáže nám i problematický dotaz. + +Chyba `Argument 1 passed to Todo\ListRepository::tasksOf() must be` `an instance of Nette\Database\Table\ActiveRow, boolean given` nám říká, že jsme se pokusili zavolat metodu `Todo\ListRepository::tasksOf()` s argumentem, co není objekt. Pokud funkce `fetch()` nenalezne žádný výsledek, vrátí `false`. To můžeme v `TaskPresenter`u snadno ověřit a zareagovat na to změnou view: + +```php +public function actionDefault(int $id): void +{ + $this->list = $this->listRepository->find($id); + if ($this->list === false) { + $this->setView('notFound'); + } +} +``` + +Pro tento view poté musíme vytvořit šablonu `notFound.latte` ve složce `app/templates/Task`: + +```html +{status 404} + +{block content} + +<h1>Seznam úkolů nenalezen</h1> + +<p>Litujeme, ale Vámi požadovaný seznam úkolů nebyl nalezen.</p> +``` + +Makrem `{status 404}` nastavujeme HTTP status na 404: Nenalezeno. Povšimněte si, že v šabloně chybí ukončovací makro pro `{block content}` - u posledního bloku v šabloně můžeme uzavírací část vynechat. + +Pokud zkusíme opět přistoupit na neexistující ID, bude situace o poznání lepší: + +[* 04-not-found.webp *] + +Povšimněte si, že jsme nikde nevytvářeli metodu `renderNotFound()`. View může existovat i bez ní. Stačí jen vytvořit šablonu se správným názvem a je hotovo. + + +Titulek stránek +=============== + +Nyní se sice můžeme pohodlně přesouvat mezi jednotlivými výpisy, ale pokud se podíváme do historie nebo do názvu okna prohlížeče, zjistíme, že titulek stránky se vůbec nemění. + +Díky dědičnosti šablon však bude doplnění titulku záležitostí chvilky. Šablona view se vykonává vždy před šablonou layoutu, teprve po ní se vykresluje layout a vkládají se do něj bloky z šablony. Před blokem `{content}` v šabloně view tedy můžeme nastavit libovolnou proměnnou a tu použít v layoutu. + +Nejprve tedy v `@layout.latte` upravíme obsah elementu `<title>`: + +```html +<title>{block title|stripTags|strip}Úkolníček{/block} +``` + +Využijeme faktu, že bloky je možné překrýt a nadefinujeme tedy pouze výchozí hodnotu. Dále je vhodné pomocí helperu očesat obsah bloku o tagy pomocí `stripTags` a zaříznout na jeden řádek pomocí `strip`. Nyní už stačí pouze v požadovaném pohledu překrýt blok a titulek bude automaticky v ``. + +`Homepage/default.latte`: + +```html +

    Nesplněné úkoly

    +``` + +`Task/default.latte`: + +```html +

    {$list->title}

    +``` + +`Task/notFound.latte`: +```html +

    Seznam úkolů nenalezen

    +``` + +A to je vše. Nyní už se titulek stránky bude při procházení webem měnit. + + +Ostylování +========== + +Aplikace byla prozatím poměrně smutná. Černobílým HTML dnes již nikoho neohromíte, proto je načase s tím něco udělat. Stáhneme si tedy soubor [screen.css |https://github.com/nette/book/blob/master/www/css/screen.css], uložíme ho do složky `www/css` a odkážeme se na něj z hlavičky v `@layout.latte`. Pozor! Předchozí dvě definice stylů odstraníme, aby se nám styly nemíchaly. + +.[note] +Snahou bylo vmáčknout celý design do jednoho jediného souboru, proto využívá některé modernější CSS3 vlastnosti. Pokud máte starší prohlížeč, budete jen ochuzeni o některé efekty, aplikace však bude vypadat a fungovat velmi podobně. + +```html + +``` + +Použitá proměnná `{$basePath}` obsahuje absolutní URL k našemu webu na serveru. Kvůli generování hezkých adres pomocí `mod_rewrite` je adresa aktuální stránky čistě virtuální. Složka `task/default` nikde neexistuje, proto nemůžeme použít relativní adresy a nesmíme zapomínat na `{$basePath}` při vkládání externích zdrojů. + + +Úspěšně jsme vytvořili seznam všech nedokončených úkolů a zobrazení jednotlivých seznamů úkolů. Můžeme se vrhnout do použití formulářů. + +Zdrojové kódy aplikace v této fázi naleznete na [GitHubu |https://github.com/nette/book/tree/3]. diff --git a/dev/cs/book/start.texy b/dev/cs/book/start.texy new file mode 100644 index 0000000000..366cc9cbaa --- /dev/null +++ b/dev/cs/book/start.texy @@ -0,0 +1,105 @@ +Začínáme +******** + +.[perex] +Seznamte se v rychlosti s Nette Frameworkem při tvorbě jednoduchého úkolovníku. Ukážeme si, jak vytvořit bezpečnou aplikaci včetně přihlašování s využitím databáze, šablon, formulářů, routování i AJAXu. + +Co všechno bude náš úkolovník umět? + +- sdružovat úkoly do samostatných seznamů +- přidávat nové úkoly i nové seznamy +- označit úkol jako dokončený (pomocí AJAXu) +- přihlásit uživatele a odeslat zapomenuté heslo + + +Stažení +======= + +Nejprve si prosím ověřte, že máte spuštěný webový server s PHP ve verzi 5.3.0 nebo jakoukoliv vyšší. + +Stáhneme si Nette Framework a tzv. sandbox, což je předpřipravená kostra aplikace. Pokud používáte nástroj [Composer |/composer], stačí spustit v kořenovém adresáři webového serveru tento příkaz a sandbox i s frameworkem se vám stahnou do adresáře `sandbox`: + +```shell +php composer.phar create-project nette/sandbox [cílová.složka] dev-release-2.0.x +``` +nebo pokud máte Composer nainstalovaný, bude stačit i kratší zápis +```shell +composer create-project nette/sandbox [cílová.složka] +``` + +Nebo si [stáhněte Nette Framework |https://nette.org/cs/download] ručně a archív rozbalte. Obsahuje řadu složek, které jsou [popsány jinde |/installation], nás teď bude zajímat pouze zmíněný `sandbox`. Ten zkopírujte do kořenového adresáře webového serveru. + +Otevřete v prohlížeči adresu: + +``` +http://localhost/sandbox/www/ +``` + +a Nette Framework vás s gratulací přivítá: + +[* welcome-620.webp *] + + +Pohled do sandboxu +================== + +Pojďme sandbox nejprve trošku prozkoumat: + +/--pre +www/ ← kořenový adresář webového serveru +└── sandbox/ + ├── app/ ← místo pro naši aplikaci + │ ├── config/ ← konfigurační soubory + │ ├── model/ ← třídy modelové vrstvy + │ ├── presenters/ ← třídy presenterů + │ ├── router/ ← továrna na routy + │ ├── templates/ ← šablony + │ └── bootstrap.php ← zaváděcí soubor + │ + ├── libs/ ← knihovny, které bude naše aplikace využívat + │ └── Nette/ ← jako třeba váš oblíbený framework :-) + │ + ├── log/ ← zde se v produkčním módu logují chybová hlášení + ├── temp/ ← místo pro dočasné soubory cache nebo session + ├── test/ ← unit testy naší aplikace + └── www/ ← složka veřejně přístupná z webu +\-- + + +Složka `www` je určena pro obrázky, JavaScripty, CSS a další veřejně dostupné zdroje. Je jako jediná přístupná z prohlížeče, proto si sem můžete namířit kořenový adresář webového serveru. + +Pokud používáte Linux nebo Mac OS X, bude potřeba nastavit oprávnění pro zápis do složek `temp` a `log`. Nejsnázeji příkazem `chmod -R a+rwX temp log`. + +Nás bude především zajímat složka `app`, ve které budeme programovat. V ní se nachází soubor `bootstrap.php`, vstupní bod aplikace. Stará se o načtení frameworku a konfigurace, aktivuje [automatické načítání tříd |cs/robotloader] a spustí aplikaci. + +Z hlediska konfigurace aplikace je ještě důležitý soubor `RouterFactory.php` ve složce `app/router/`, který se stará o nastavení [routování |cs/routing]. Routování říká, jakou přesně mají mít URL adresy podobu. Výhoda Nette Frameworku je, že tvar URL můžete změnit kdykoliv a velice snadno, proto se routování budeme věnovat až ve chvíli, kdy budeme mít celou aplikaci naprogramovanou. + + +Laděnka +======= + +Nesmírně důležitým nástrojem při vývoji je [Debugger |cs/debugging], někdy přezdívaný Laděnka. Schválně si zkuste otevřít soubor `app/presenters/HomepagePresenter.php` a udělat v něm nějakou chybu, třeba smazat písmenko ze slova `class`. Pokud spouštíte aplikaci lokálně, objeví se vám srozumitelná červená stránka a hned jste v obraze. (Pokud spouštíte aplikaci na vzdáleném serveru, čtěte dál.) + +[* debugger-620.webp *] + +Laděnka vám neuvěřitelným způsobem usnadní práci při hledání chyb. Také si všimněte plovoucího Debugger baru v pravé spodní části, který vás informuje o důležitých běhových údajích. + +Nette může běžet v buď v produkčním, nebo v debug módu. V produkčním prostředí Laděnka pochopitelně žádné citlivé údaje neprozrazuje a namísto toho je loguje. Debug mode se zapne, pokud k aplikaci přistupujete lokálně; naopak na serveru je mód produkční. + +Debug mód lze natvrdo zapnout či vypnout (např. pro zapnutí debug módu lokálně, či naopak) v souboru `bootstrap.php`: + +```php +$configurator->setDebugMode(false); // <- tento řádek je klíčový, vypne debug i lokálně +//$configurator->setDebugMode(['93.184.216.119', '2606:2800:220:6d:26bf:1447:1097:aa7']); // <- tento řádek zapne debug pouze pro dané IP adresy +$configurator->enableTracy(__DIR__ . '/../log'); +``` + +V produkčním módu je potom červená obrazovka nahrazená uživatelsky srozumitelnou zprávou (můžete to sami zkusit úpravou `bootstrap.php` a znovunačtením): + +[* server-error-620.webp *] + +Podívejte se do adresáře `log`, kde jednak najdete v souboru `error.log` stručnou zprávu o chybě, ale také celou červenou stránku uloženou v souboru, jehož název začíná slovem `exception`. + +Řádek `$configurator->setDebugMode(false);` opět zakomentujte a Laděnka bude debug-mód zapínat lokálně automaticky. Všude jinde bude vypnutý. + +Nyní, vyzbrojeni základními znalostmi, se můžeme pustit do návrhu databáze a modelu. diff --git a/dev/cs/db/@left-menu.texy b/dev/cs/db/@left-menu.texy new file mode 100644 index 0000000000..8353639528 --- /dev/null +++ b/dev/cs/db/@left-menu.texy @@ -0,0 +1,8 @@ +Databáze +******** +- [Core] +- [Explorer] +- [Reflexe |reflection] +- [Konfigurace |configuration] +- [Bezpečnostní rizika |security] +- [Upgrade |upgrading] diff --git a/database/cs/configuration.texy b/dev/cs/db/configuration.texy similarity index 87% rename from database/cs/configuration.texy rename to dev/cs/db/configuration.texy index 34d8bf384c..021ee3cd11 100644 --- a/database/cs/configuration.texy +++ b/dev/cs/db/configuration.texy @@ -4,7 +4,7 @@ Konfigurace databáze .[perex] Přehled konfiguračních voleb pro Nette Database. -Pokud nepoužíváte celý framework, ale jen tuto knihovnu, přečtěte si, [jak konfiguraci načíst|bootstrap:]. +Pokud nepoužívate celý framework, ale jen tuto knihovnu, přečtěte si, [jak konfiguraci načíst|bootstrap:]. Jedno spojení @@ -27,7 +27,7 @@ Další nastavení: ```neon database: # zobrazit database panel v Tracy Bar? - debugger: ... # (bool) výchozí zapnuto při aktivní Tracy + debugger: ... # (bool) výchozí je true # zobrazit EXPLAIN dotazů v Tracy Bar? explain: ... # (bool) výchozí je true @@ -108,6 +108,3 @@ Neautowirované služby předáváme explicitně odkazem na jejich název: services: - UserFacade(@database.another.connection) ``` - -{{maintitle: Konfigurace Nette Database: připojení v NEON}} -{{description: Nastavte jedno nebo více připojení k databázi v konfiguraci NEON: DSN, přihlašovací údaje, volby pro ladění a služby DI, které Nette Database registruje.}} diff --git a/database/cs/sql-way.texy b/dev/cs/db/core.texy similarity index 52% rename from database/cs/sql-way.texy rename to dev/cs/db/core.texy index 5105765444..c38f72f39a 100644 --- a/database/cs/sql-way.texy +++ b/dev/cs/db/core.texy @@ -1,20 +1,27 @@ -SQL přístup -*********** +Database Core +************* .[perex] -Nette Database nabízí dvě cesty: můžete psát SQL dotazy sami (SQL přístup), nebo je nechat generovat automaticky (viz [Explorer |explorer]). SQL přístup vám dává plnou kontrolu nad dotazy a přitom zajišťuje jejich bezpečné sestavení. +Nette Database Core je základní, nízkoúrovňová vrstva pro přístup k databázi (tzv. database abstraction layer). Poskytuje pohodlný a bezpečný způsob, jak komunikovat s databází, provádět dotazy a manipulovat s daty. -.[note] -Detaily k připojení a konfiguraci databáze najdete v kapitole [Připojení a konfigurace |guide#Připojení a konfigurace]. +Vedle této základní vrstvy nabízí Nette také pokročilejší [Nette Database Explorer |explorer], který vám umožní pracovat s databází bez nutnosti psát SQL dotazy. +Pro připojení k databázi vytvořte instanci třídy [api:Nette\Database\Connection], nebo ji získejte [jako službu z DI kontejneru|dependency-injection:passing-dependencies]: + + +Pokládání SQL dotazů +==================== + +Pro dotazování do databáze slouží metoda `query()`. Ta vrací objekt [ResultSet |api:Nette\Database\ResultSet], který reprezentuje výsledek dotazu. V případě selhání metoda [vyhodí výjimku|#Výjimky]. -Základní dotazování -=================== -Pro dotazování do databáze slouží metoda `query()`. Ta vrací objekt [ResultSet |api:Nette\Database\ResultSet], který reprezentuje výsledek dotazu. V případě selhání metoda [vyhodí výjimku|exceptions]. Výsledek dotazu můžeme procházet pomocí cyklu `foreach`, nebo použít některou z [pomocných funkcí |#Získání dat]. +Získávání dat (SELECT) +---------------------- + +Nejjednodušší použití je zavolat `query()` a následně výsledek dotazu, který se vrací jako objekt `ResultSet`, procházet pomocí cyklu `foreach`: ```php -$result = $database->query('SELECT * FROM users'); +$result = $db->query('SELECT * FROM users'); foreach ($result as $row) { echo $row->id; @@ -25,24 +32,26 @@ foreach ($result as $row) { Pro bezpečné vkládání hodnot do SQL dotazů používáme parametrizované dotazy. Nette Database je dělá maximálně jednoduché - stačí za SQL dotaz přidat čárku a hodnotu: ```php -$database->query('SELECT * FROM users WHERE name = ?', $name); +$db->query('SELECT * FROM users WHERE name = ?', $name); ``` Při více parametrech máte dvě možnosti zápisu. Buď můžete SQL dotaz "prokládat" parametry: ```php -$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age); +$db->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age); ``` Nebo napsat nejdříve celý SQL dotaz a pak připojit všechny parametry: ```php -$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); +$db->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); ``` +Podívejte se, jaké techniky nabízí Nette Database pro [snadný zápis pokročilejších SQL dotazů |#Techniky dotazování]. + Ochrana před SQL injection -========================== +-------------------------- Proč je důležité používat parametrizované dotazy? Protože vás chrání před útokem zvaným SQL injection, při kterém by útočník mohl podstrčit vlastní SQL příkazy a tím získat nebo poškodit data v databázi. @@ -51,83 +60,15 @@ Proč je důležité používat parametrizované dotazy? Protože vás chrání ```php // ❌ NEBEZPEČNÝ KÓD - zranitelný vůči SQL injection -$database->query("SELECT * FROM users WHERE name = '$name'"); +$db->query("SELECT * FROM users WHERE name = '$name'"); // ✅ Bezpečný parametrizovaný dotaz -$database->query('SELECT * FROM users WHERE name = ?', $name); +$db->query('SELECT * FROM users WHERE name = ?', $name); ``` Seznamte se s [možnými bezpečnostními riziky |security]. -Techniky dotazování -=================== - - -Podmínky WHERE --------------- - -Podmínky WHERE můžete zapsat jako asociativní pole, kde klíče jsou názvy sloupců a hodnoty jsou data pro porovnání. Nette Database automaticky vybere nejvhodnější SQL operátor podle typu hodnoty. - -```php -$database->query('SELECT * FROM users WHERE', [ - 'name' => 'John', - 'active' => true, -]); -// WHERE `name` = 'John' AND `active` = 1 -``` - -V klíči můžete také explicitně uvést operátor pro porovnání: - -```php -$database->query('SELECT * FROM users WHERE', [ - 'age >' => 25, // použije operátor > - 'name LIKE' => '%John%', // použije operátor LIKE - 'email NOT LIKE' => '%example.com%', // použije operátor NOT LIKE -]); -// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' -``` - -Nette automaticky ošetřuje speciální případy jako hodnoty `null` nebo pole. - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name' => 'Laptop', // použije operátor = - 'category_id' => [1, 2, 3], // použije IN - 'description' => null, // použije IS NULL -]); -// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL -``` - -Pro negativní podmínky použijte operátor `NOT`: - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // použije operátor != - 'category_id NOT' => [1, 2, 3], // použije NOT IN - 'description NOT' => null, // použije IS NOT NULL - 'id NOT' => [], // vynechá se -]); -// WHERE `name` != 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL -``` - -Pro spojování podmínek se používá operátor `AND`. To lze změnit pomocí [zástupného symbolu ?or |#Hinty pro sestavování SQL]. - - -Pravidla ORDER BY ------------------ - -Řazení `ORDER BY` se dá zapsat pomocí pole. V klíčích uvedeme sloupce a hodnotou bude boolean určující, zda řadit vzestupně: - -```php -$database->query('SELECT id FROM author ORDER BY', [ - 'id' => true, // vzestupně - 'name' => false, // sestupně -]); -// SELECT id FROM author ORDER BY `id`, `name` DESC -``` - - Vkládání dat (INSERT) --------------------- @@ -138,18 +79,18 @@ $values = [ 'name' => 'John Doe', 'email' => 'john@example.com', ]; -$database->query('INSERT INTO users ?', $values); -$userId = $database->getInsertId(); +$db->query('INSERT INTO users ?', $values); +$userId = $db->getInsertId(); ``` -Metoda `getInsertId()` vrátí ID naposledy vloženého řádku. U některých databází (např. PostgreSQL) je nutné jako parametr specifikovat název sekvence, ze které se má ID generovat pomocí `$database->getInsertId($sequenceId)`. +Metoda `getInsertId()` vrátí ID naposledy vloženého řádku. U některých databází (např. PostgreSQL) je nutné jako parametr specifikovat název sekvence, ze které se má ID generovat pomocí `$db->getInsertId($sequenceId)`. Jako parametry můžeme předávat i [#speciální hodnoty] jako soubory, objekty DateTime nebo výčtové typy. Vložení více záznamů najednou: ```php -$database->query('INSERT INTO users ?', [ +$db->query('INSERT INTO users ?', [ ['name' => 'User 1', 'email' => 'user1@mail.com'], ['name' => 'User 2', 'email' => 'user2@mail.com'], ]); @@ -157,20 +98,20 @@ $database->query('INSERT INTO users ?', [ Vícenásobný INSERT je mnohem rychlejší, protože se provede jediný databázový dotaz, namísto mnoha jednotlivých. -**Bezpečnostní upozornění:** Nikdy nepoužívejte jako `$values` nevalidovaná data. Seznamte se s [možnými riziky |security#Bezpečná práce se sloupci]. +**Bezpečnostní upozornění:** Nikdy nepoužívejte jako `$values` nevalidovaná data. Seznamte se s [možnými riziky |security#Klíče polí nejsou bezpečné API]. Aktualizace dat (UPDATE) ------------------------ -Pro aktualizaci záznamů se používá SQL příkaz `UPDATE`. +Pro aktualizacizáznamů se používá SQL příkaz `UPDATE`. ```php // Aktualizace jednoho záznamu $values = [ 'name' => 'John Smith', ]; -$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); +$result = $db->query('UPDATE users SET ? WHERE id = ?', $values, 1); ``` Počet ovlivněných řádků vrátí `$result->getRowCount()`. @@ -178,7 +119,7 @@ Počet ovlivněných řádků vrátí `$result->getRowCount()`. Pro UPDATE můžeme využít operátorů `+=` a `-=`: ```php -$database->query('UPDATE users SET ? WHERE id = ?', [ +$db->query('UPDATE users SET ? WHERE id = ?', [ 'login_count+=' => 1, // inkrementace login_count ], 1); ``` @@ -190,7 +131,7 @@ $values = [ 'name' => $name, 'year' => $year, ]; -$database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', +$db->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', $values + ['id' => $id], $values, ); @@ -198,7 +139,7 @@ $database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', // ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 ``` -Všimněte si, že Nette Database pozná, v jakém kontextu SQL příkazu parametr s polem vkládáme a podle toho z něj sestaví SQL kód. Takže z prvního pole sestavil `(id, name, year) VALUES (123, 'Jim', 1978)`, zatímco druhé převedl do podoby `name = 'Jim', year = 1978`. Podrobněji se tomu věnujeme v části [#Hinty pro sestavování SQL]. +Všimněte si, že Nette Database pozná, v jakém kontextu SQL příkazu parametr s polem vkládáme a podle toho z něj sestaví SQL kód. Takže z prvního pole sestavil `(id, name, year) VALUES (123, 'Jim', 1978)`, zatímco druhé převedl do podoby `name = 'Jim', year = 1978`. Podroběji se tomu věnujeme v části [Hinty pro sestavování SQL|#Hinty pro sestavování SQL]. Mazání dat (DELETE) @@ -207,11 +148,288 @@ Mazání dat (DELETE) Pro mazání záznamů se používá SQL příkaz `DELETE`. Příklad se získáním počtu smazaných řádků: ```php -$count = $database->query('DELETE FROM users WHERE id = ?', 1) +$count = $db->query('DELETE FROM users WHERE id = ?', 1) ->getRowCount(); ``` +Získání dat +=========== + + +Zkratky pro SELECT dotazy +------------------------- + +Pro zjednodušení načítání dat nabízí `Connection` několik zkratek, které kombinují volání `query()` s následujícím `fetch*()`. Tyto metody přijímají stejné parametry jako `query()`, tedy SQL dotaz a volitelné parametry. +Plnohodnotný popis metod `fetch*()` najdete [níže|#fetch()]. + +| `fetch($sql, ...$params): ?Row` | Provede dotaz a vrátí první řádek jako objekt `Row` +| `fetchAll($sql, ...$params): array` | Provede dotaz a vrátí všechny řádky jako pole objektů `Row` +| `fetchPairs($sql, ...$params): array` | Provede dotaz a vrátí asocitivní pole, kde první sloupec představuje klíč a druhý hodnotu +| `fetchField($sql, ...$params): mixed` | Provede dotaz a vrátí hodnotu prvního políčka z prvního řádku +| `fetchList($sql, ...$params): ?array` | Provede dotaz a vrací první řádek jako indexované pole + +Příklad: + +```php +// fetchField() - vrátí hodnotu první buňky +$count = $db->query('SELECT COUNT(*) FROM articles') + ->fetchField(); +``` + + +`foreach` - iterace přes řádky +------------------------------ + +Po vykonání dotazu se vrací objekt [ResultSet|api:Nette\Database\ResultSet], který umožňuje procházet výsledky několika způsoby. +Nejsnazší způsob, jak vykonat dotaz a získat řádky, je iterováním v cyklu `foreach`. Tento způsob je paměťově nejúspornější, neboť vrací data postupně a neukládá si je do paměti najednou. + +```php +$result = $db->query('SELECT * FROM users'); + +foreach ($result as $row) { + echo $row->id; + echo $row->name; + // ... +} +``` + +.[note] +`ResultSet` lze iterovat pouze jednou. Pokud potřebujete iterovat opakovaně, musíte nejprve načíst data do pole, například pomocí metody `fetchAll()`. + + +fetch(): ?Row .[method] +----------------------- + +Vrací řádek jako objekt `Row`. Pokud už neexistují další řádky, vrací `null`. Posune interní ukazatel na další řádek. + +```php +$result = $db->query('SELECT * FROM users'); +$row = $result->fetch(); // načte první řádek +if ($row) { + echo $row->name; +} +``` + + +fetchAll(): array .[method] +--------------------------- + +Vrací všechny zbývající řádky z `ResultSetu` jako pole objektů `Row`. + +```php +$result = $db->query('SELECT * FROM users'); +$rows = $result->fetchAll(); // načte všechny řádky +foreach ($rows as $row) { + echo $row->name; +} +``` + + +fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] +--------------------------------------------------------------------------------------- + +Vrátí výsledky jako asociativní pole. První argument určuje název sloupce, který se použije jako klíč v poli, druhý argument určuje název sloupce, který se použije jako hodnota: + +```php +$result = $db->query('SELECT id, name FROM users'); +$names = $result->fetchPairs('id', 'name'); +// [1 => 'John Doe', 2 => 'Jane Doe', ...] +``` + +Pokud uvedeme pouze první parametr, bude hodnotou celý řádek, tedy objekt `Row`: + +```php +$rows = $result->fetchPairs('id'); +// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...] +``` + +Pokud jako klíč uvedeme `null`, bude pole indexováno numericky od nuly: + +```php +$names = $result->fetchPairs(null, 'name'); +// [0 => 'John Doe', 1 => 'Jane Doe', ...] +``` + + +fetchPairs(Closure $callback): array .[method] +---------------------------------------------- + +Alternativně můžete jako parametr uvést callback, který bude pro každý řádek vracet buď samotnou hodnotu, nebo dvojici klíč-hodnota. + +```php +$result = $db->query('SELECT * FROM users'); +$items = $result->fetchPairs(fn($row) => "$row->id - $row->name"); +// ['1 - John', '2 - Jane', ...] + +// Callback může také vracet pole s dvojicí klíč & hodnota: +$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); +// ['John' => 46, 'Jane' => 21, ...] +``` + + +fetchField(): mixed .[method] +----------------------------- + +Vrací hodnotu prvního políčka z aktuálního řádku. Pokud už neexistují další řádky, vrací `null`. Posune interní ukazatel na další řádek. + +```php +$result = $db->query('SELECT name FROM users'); +$name = $result->fetchField(); // načte jméno z prvního řádku +``` + + +fetchList(): ?array .[method] +----------------------------- + +Vrací řádek jako indexované pole. Pokud už neexistují další řádky, vrací `null`. Posune interní ukazatel na další řádek. + +```php +$result = $db->query('SELECT name, email FROM users'); +$row = $result->fetchList(); // ['John', 'john@example.com'] +``` + + +getRowCount(): ?int .[method] +----------------------------- + +Vrací počet ovlivněných řádků posledním dotazem `UPDATE` nebo `DELETE`. Pro `SELECT` je to počet vrácených řádků, ale ten nemusí být znám - v takovém případě metoda vrátí `null`. + + +getColumnCount(): ?int .[method] +-------------------------------- + +Vrací počet sloupců v `ResultSetu`. + + +Konverze typů +============= + +Nette Database automaticky konvertuje hodnoty vrácené z databáze na odpovídající PHP typy. + + +Datum a čas +----------- + +Časové údaje jsou převáděny na objekty `Nette\Utils\DateTime`. Pokud chcete, aby byly časové údaje převáděny na immutable objekty `Nette\Database\DateTime`, nastavte v [konfiguraci|configuration] volbu `newDateTime` na true. + +```php +$row = $db->fetch('SELECT created_at FROM articles'); +echo $row->created_at instanceof DateTime; // true +echo $row->created_at->format('j. n. Y'); +``` + + +Booleovské hodnoty +------------------ + +Booleovské hodnoty jsou automaticky převedeny na `true` nebo `false`. U MySQL se převádí `TINYINT(1)` pokud nastavíme v [konfiguraci|configuration] `convertBoolean`. + +```php +$row = $db->fetch('SELECT is_published FROM articles'); +echo gettype($row->is_published); // 'boolean' +``` + + +Číselné hodnoty +--------------- + +Číselné hodnoty jsou převedeny na `int` nebo `float` podle typu sloupce v databázi: + +```php +$row = $db->fetch('SELECT id, price FROM products'); +echo gettype($row->id); // integer +echo gettype($row->price); // float +``` + + +Vlastní normalizace +------------------- + +Pomocí metody `setRowNormalizer(?callable $normalizer)` můžete nastavit vlastní funkci pro transformaci řádků z databáze. To se hodí například pro automatický převod datových typů. + +```php +$db->setRowNormalizer(function(array $row, ResultSet $resultSet): array { + // konverze typů + return $row; +}); +``` + + +Techniky dotazování +=================== + +Nette Database nabízí elegantní a expresivní způsoby, jak sestavovat SQL dotazy. Podívejte se na ně. + + +Podmínky WHERE +-------------- + +Podmínky WHERE můžete zapsat jako asociativní pole, kde klíče jsou názvy sloupců a hodnoty jsou data pro porovnání. Nette Database automaticky vybere nejvhodnější SQL operátor podle typu hodnoty. + +```php +$db->query('SELECT * FROM users WHERE', [ + 'name' => 'John', + 'active' => true, +]); +// WHERE `name` = 'John' AND `active` = 1 +``` + +V klíči můžete také explicitně specifikovat operátor pro porovnání: + +```php +$db->query('SELECT * FROM users WHERE', [ + 'age >' => 25, // použije operátor > + 'name LIKE' => '%John%', // použije operátor LIKE + 'email NOT LIKE' => '%example.com%', // použije operátor NOT LIKE +]); +// WHERE `age` > 25 AND `created_at` <= '2024-01-26 12:00:00' AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' +``` + +Nette automaticky ošetřuje speciální případy jako `null` hodnoty nebo pole. + +```php +$db->query('SELECT * FROM products WHERE', [ + 'name' => 'Laptop', // použije operátor = + 'category_id' => [1, 2, 3], // použije IN + 'description' => null, // použije IS NULL +]); +// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL + +$db->query('SELECT * FROM products WHERE', [ + 'name NOT' => 'Laptop', // použije operátor <> + 'category_id NOT' => [1, 2, 3], // použije NOT IN + 'description NOT' => null, // použije IS NOT NULL + 'id' => [], // vynechá se +]); +// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL +``` + +Pro spojování podmínek se používá operátor `AND`. To lze změnit pomocí zástupného symbolu `?or` (viz níže). + +```php +$db->query('SELECT * FROM users WHERE ?or', [ + 'name' => 'John', + 'active' => true, +]); +// WHERE `name` = 'John' OR `active` = 1 +``` + + +Pravidla ORDER BY +----------------- + +Řazení `ORDER BY` se dá zapsat pomocí pole. V klíčích uvedeme sloupce a hodnotou bude boolean určující, zda řadit vzestupně: + +```php +$db->query('SELECT id FROM author ORDER BY', [ + 'id' => true, // vzestupně + 'name' => false, // sestupně +]); +// SELECT id FROM author ORDER BY `id`, `name` DESC +``` + + Hinty pro sestavování SQL ------------------------- @@ -219,7 +437,7 @@ Hint je speciální zástupný symbol v SQL dotazu, který říká, jak se má h | Hint | Popis | Automaticky se použije |-----------|-------------------------------------------------|----------------------------- -| `?name` | použije se pro vložení názvu tabulky či sloupce | - +| `?name` | použije pro vložení názvu tabulky nebo sloupce | - | `?values` | vygeneruje `(key, ...) VALUES (value, ...)` | `INSERT ... ?`, `REPLACE ... ?` | `?set` | vygeneruje přiřazení `key = value, ...` | `SET ?`, `KEY UPDATE ?` | `?and` | spojí podmínky v poli operátorem `AND` | `WHERE ?`, `HAVING ?` @@ -231,7 +449,7 @@ Pro dynamické vkládání názvů tabulek a sloupců do dotazu slouží zástup ```php $table = 'users'; $column = 'name'; -$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); +$db->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); // SELECT `name` FROM `users` WHERE id = 1 (v MySQL) ``` @@ -240,7 +458,7 @@ $database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); Ostatní hinty obvykle není potřeba uvádět, neboť Nette používá při skládání SQL dotazu chytrou autodetekci (viz třetí sloupec tabulky). Ale můžete jej využít například v situaci, kdy chcete spojit podmínky pomocí `OR` namísto `AND`: ```php -$database->query('SELECT * FROM users WHERE ?or', [ +$db->query('SELECT * FROM users WHERE ?or', [ 'name' => 'John', 'email' => 'john@example.com', ]); @@ -254,12 +472,12 @@ Speciální hodnoty Kromě běžných skalárních typů (string, int, bool) můžete jako parametry předávat i speciální hodnoty: - soubory: `fopen('image.gif', 'r')` vloží binární obsah souboru -- datum a čas: objekty `DateTimeInterface` se převedou na databázový formát +- datum a čas: objekty `DateTime` se převedou na databázový formát - výčtové typy: instance `enum` se převedou na jejich hodnotu - SQL literály: vytvořené pomocí `Connection::literal('NOW()')` se vloží přímo do dotazu ```php -$database->query('INSERT INTO articles ?', [ +$db->query('INSERT INTO articles ?', [ 'title' => 'My Article', 'published_at' => new DateTime, 'content' => fopen('image.png', 'r'), @@ -273,22 +491,22 @@ U databází, které nemají nativní podporu pro datový typ `datetime` (jako S SQL literály ------------ -V některých případech potřebujete jako hodnotu uvést přímo SQL kód, který se ale nemá chápat jako řetězec a escapovat. K tomu slouží objekty třídy `Nette\Database\SqlLiteral`. Vytváří je metoda `Connection::literal()`. +V některých případech potřebujete jako hodnotu uvést přímo SQL kód, který se ale nemá chápat jako řetězec a escapovat. K tomuto slouží objekty třídy `Nette\Database\SqlLiteral`. Vytváří je metoda `Connection::literal()`. ```php -$result = $database->query('SELECT * FROM users WHERE', [ +$result = $db->query('SELECT * FROM users WHERE', [ 'name' => $name, - 'year >' => $database::literal('YEAR()'), + 'year >' => $db::literal('YEAR()'), ]); // SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) ``` -Nebo alternativně: +Nebo alternativě: ```php -$result = $database->query('SELECT * FROM users WHERE', [ +$result = $db->query('SELECT * FROM users WHERE', [ 'name' => $name, - $database::literal('year > YEAR()'), + $db::literal('year > YEAR()'), ]); // SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) ``` @@ -296,19 +514,19 @@ $result = $database->query('SELECT * FROM users WHERE', [ SQL literály mohou obsahovat parametry: ```php -$result = $database->query('SELECT * FROM users WHERE', [ +$result = $db->query('SELECT * FROM users WHERE', [ 'name' => $name, - $database::literal('year > ? AND year < ?', $min, $max), + $db::literal('year > ? AND year < ?', $min, $max), ]); // SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) ``` -Díky tomu můžeme vytvářet zajímavé kombinace: +Díky čemuž můžeme vytvářet zajímavé kombinace: ```php -$result = $database->query('SELECT * FROM users WHERE', [ +$result = $db->query('SELECT * FROM users WHERE', [ 'name' => $name, - $database::literal('?or', [ + $db::literal('?or', [ 'active' => true, 'role' => $role, ]), @@ -317,162 +535,154 @@ $result = $database->query('SELECT * FROM users WHERE', [ ``` -Získání dat -=========== - - -Zkratky pro SELECT dotazy -------------------------- - -Pro zjednodušení načítání dat nabízí `Connection` několik zkratek, které kombinují volání `query()` s následujícím `fetch*()`. Tyto metody přijímají stejné parametry jako `query()`, tedy SQL dotaz a volitelné parametry. Plnohodnotný popis metod `fetch*()` najdete [níže |#fetch]. +Transakce +========= -| `fetch($sql, ...$params): ?Row` | Provede dotaz a vrátí první řádek jako objekt `Row` nebo `null` -| `fetchAll($sql, ...$params): array` | Provede dotaz a vrátí všechny řádky jako pole objektů `Row` -| `fetchPairs($sql, ...$params): array` | Provede dotaz a vrátí asociativní pole, kde první sloupec představuje klíč a druhý hodnotu -| `fetchField($sql, ...$params): mixed` | Provede dotaz a vrátí hodnotu prvního políčka z prvního řádku -| `fetchList($sql, ...$params): ?array` | Provede dotaz a vrátí první řádek jako indexované pole nebo `null` +Transakce zaručují, že se buď provedou všechny operace v rámci transakce, nebo se neprovede žádná. Jsou užitečné pro zajištění konzistence dat při složitějších operacích. -Příklad: +Nejjednodušší způsob použití transakcí vypadá takto: ```php -// fetchField() - vrátí hodnotu první buňky -$count = $database->query('SELECT COUNT(*) FROM articles') - ->fetchField(); +$db->beginTransaction(); +try { + $db->query('DELETE FROM articles WHERE id = ?', $id); + $db->query('INSERT INTO audit_log', [ + 'article_id' => $id, + 'action' => 'delete' + ]); + $db->commit(); +} catch (\Exception $e) { + $db->rollBack(); + throw $e; +} ``` - -`foreach` - iterace přes řádky ------------------------------- - -Po vykonání dotazu se vrací objekt [ResultSet|api:Nette\Database\ResultSet], který umožňuje procházet výsledky několika způsoby. Nejsnazší způsob, jak vykonat dotaz a získat řádky, je iterování v cyklu `foreach`. Tento způsob je paměťově nejúspornější, neboť vrací data postupně a neukládá si je do paměti najednou. +Mnohem elegantněji můžete to samé zapsat pomocí metody `transaction()`. Jako parametr přijímá callback, který vykoná v transakci. Pokud callback proběhne bez výjimky, transakce se automaticky potvrdí. Pokud dojde k výjimce, transakce se zruší (rollback) a výjimka se šíří dál. ```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; - // ... -} +$db->transaction(function ($db) use ($id) { + $db->query('DELETE FROM articles WHERE id = ?', $id); + $db->query('INSERT INTO audit_log', [ + 'article_id' => $id, + 'action' => 'delete' + ]); +}); ``` -.[note] -`ResultSet` lze iterovat pouze jednou. Pokud potřebujete iterovat opakovaně, musíte nejprve načíst data do pole, například pomocí metody `fetchAll()`. - - -fetch(): ?Row .[method] ------------------------ - -Vrací řádek jako objekt `Row`. Pokud už neexistují další řádky, vrací `null`. Posune interní ukazatel na další řádek. +Metoda `transaction()` může také vracet hodnoty: ```php -$result = $database->query('SELECT * FROM users'); -$row = $result->fetch(); // načte první řádek -if ($row) { - echo $row->name; -} +$count = $db->transaction(function ($db) { + $result = $db->query('UPDATE users SET active = ?', true); + return $result->getRowCount(); // vrátí počet aktualizovaných řádků +}); ``` -fetchAll(): array .[method] ---------------------------- +Informace o struktuře +===================== -Vrací všechny zbývající řádky z `ResultSetu` jako pole objektů `Row`. +Nette Database umožňuje pracovat se strukturou databáze. Pro její zjištění použijeme `getReflection()`. ```php -$result = $database->query('SELECT * FROM users'); -$rows = $result->fetchAll(); // načte všechny řádky -foreach ($rows as $row) { - echo $row->name; -} +$reflection = $db->getReflection(); +$tables = $reflection->getTables(); // výpis všech tabulek ``` +Viz [podrobná dokumentace Reflection |reflection]. -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Vrátí výsledky jako asociativní pole. První argument určuje název sloupce, který se použije jako klíč v poli, druhý argument určuje název sloupce, který se použije jako hodnota: +ResultSet nabízí informace o typech sloupců: ```php -$result = $database->query('SELECT id, name FROM users'); -$names = $result->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] +$result = $db->query('SELECT * FROM articles'); +$types = $result->getColumnTypes(); + +foreach ($types as $column => $type) { + echo "$column je typu $type->type"; // např. 'id je typu int' +} ``` -Pokud uvedeme pouze první parametr, bude hodnotou celý řádek, tedy objekt `Row`: -```php -$rows = $result->fetchPairs('id'); -// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...] -``` +Výjimky +======= -V případě duplicitních klíčů se použije hodnota z posledního řádku. Při použití `null` jako klíče bude pole indexováno numericky od nuly (pak ke kolizím nedochází): +Nette Database používá hierarchii výjimek. Základní třídou je `Nette\Database\DriverException`, která dědí z `PDOException` a poskytuje rozšířené možnosti pro práci s chybami databáze: -```php -$names = $result->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` +- Metoda `getDriverCode()` vrací kód chyby od databázového driveru +- Metoda `getSqlState()` vrací SQLSTATE kód +- Metody `getQueryString()` a `getParameters()` umožňují získat původní dotaz a jeho parametry +Z `DriverException` dědí následující specializované výjimky: -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- +- `ConnectionException` - signalizuje selhání připojení k databázovému serveru +- `ConstraintViolationException` - základní třída pro porušení databázových omezení, ze které dědí: + - `ForeignKeyConstraintViolationException` - porušení cizího klíče + - `NotNullConstraintViolationException` - porušení NOT NULL omezení + - `UniqueConstraintViolationException` - porušení unikátnosti hodnoty -Alternativně můžete jako parametr uvést callback, který bude pro každý řádek vracet buď samotnou hodnotu, nebo dvojici klíč-hodnota. + +Příklad zachytávání výjimky `UniqueConstraintViolationException`, která nastane, když se snažíme vložit uživatele s emailem, který už v databázi existuje (za předpokladu, že sloupec email má unikátní index). ```php -$result = $database->query('SELECT * FROM users'); -$items = $result->fetchPairs(fn($row) => "$row->id - $row->name"); -// ['1 - John', '2 - Jane', ...] +try { + $db->query('INSERT INTO users', [ + 'email' => 'john@example.com', + 'name' => 'John Doe', + 'password' => $hashedPassword, + ]); +} catch (Nette\Database\UniqueConstraintViolationException $e) { + echo 'Uživatel s tímto emailem již existuje.'; -// Callback může také vracet pole s dvojicí klíč & hodnota: -$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); -// ['John' => 46, 'Jane' => 21, ...] +} catch (Nette\Database\DriverException $e) { + echo 'Došlo k chybě při registraci: ' . $e->getMessage(); +} ``` -fetchField(): mixed .[method] ------------------------------ +Správa připojení +================ -Vrací hodnotu prvního políčka z aktuálního řádku. Pokud už neexistují další řádky, vrací `null`. Posune interní ukazatel na další řádek. +Při vytvoření objektu `Connection` dojde automnaticky k připojení. Pokud chcete připojení odložit, použijte lazy režim - ten zapnete v [konfiguracI|configuration] nastavením `lazy`, nebo takto: ```php -$result = $database->query('SELECT name FROM users'); -$name = $result->fetchField(); // načte jméno z prvního řádku +$db = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => true]); ``` +Pro správu připojení k databázi slouží metody `connect()`, `disconnect()` a `reconnect()`. Metoda `connect()` naváže spojení s databází, pokud ještě není navázáno. Může vyhodit výjimku `Nette\Database\ConnectionException`. Metoda `disconnect()` odpojí se od databáze. Metoda `reconnect()` odpojí se a znovu připojí k databázi. Může vyhodit výjimku `Nette\Database\ConnectionException`. -fetchList(): ?array .[method] ------------------------------ - -Vrací řádek jako indexované pole. Pokud už neexistují další řádky, vrací `null`. Posune interní ukazatel na další řádek. +Kromě toho můžete sledovat události spojené s připojením pomocí události `onConnect`, což je pole callbacků, které se zavolají po navázání spojení s databází. ```php -$result = $database->query('SELECT name, email FROM users'); -$row = $result->fetchList(); // ['John', 'john@example.com'] +// proběhne po připojení k databázi +$db->onConnect[] = function($db) { + echo "Připojeno k databázi"; +}; ``` -getRowCount(): ?int .[method] ------------------------------ +Ladění a výkon +============== -Vrací počet řádků ovlivněných posledním dotazem `UPDATE` nebo `DELETE`. Pro `SELECT` je to počet vrácených řádků, ale ten nemusí být znám - v takovém případě metoda vrátí `null`. +Nette Database poskytuje několik užitečných nástrojů pro ladění a optimalizaci výkonu. -getColumnCount(): ?int .[method] --------------------------------- +Tracy Debug Bar +--------------- -Vrací počet sloupců v `ResultSetu`. +Pokud používáte [Tracy |tracy:], aktivuje se automaticky panel Database v Debug baru, který zobrazuje všechny provedené dotazy, jejich parametry, dobu vykonání a místo v kódu, kde byly zavolány. +[* db-panel.webp *] -Informace o dotazech -==================== + +Informace o dotazu +------------------ Pro ladicí účely můžeme získat informace o posledním provedeném dotazu: ```php -echo $database->getLastQueryString(); // vypíše SQL dotaz +$result = $db->query('SELECT * FROM articles'); -$result = $database->query('SELECT * FROM articles'); +echo $db->getLastQueryString(); // vypíše SQL dotaz echo $result->getQueryString(); // vypíše SQL dotaz echo $result->getTime(); // vypíše dobu vykonání v sekundách ``` @@ -480,21 +690,10 @@ echo $result->getTime(); // vypíše dobu vykonání v sekundách Pro zobrazení výsledku jako HTML tabulky lze použít: ```php -$result = $database->query('SELECT * FROM articles'); +$result = $db->query('SELECT * FROM articles'); $result->dump(); ``` -ResultSet nabízí informace o typech sloupců: - -```php -$result = $database->query('SELECT * FROM articles'); -$types = $result->getColumnTypes(); - -foreach ($types as $column => $type) { - echo "$column je typu $type"; // např. 'id je typu int' -} -``` - Logování dotazů --------------- @@ -502,7 +701,7 @@ Logování dotazů Můžeme implementovat vlastní logování dotazů. Událost `onQuery` je pole callbacků, které se zavolají po každém provedeném dotazu: ```php -$database->onQuery[] = function ($database, $result) use ($logger) { +$db->onQuery[] = function ($db, $result) use ($logger) { $logger->info('Query: ' . $result->getQueryString()); $logger->info('Time: ' . $result->getTime()); @@ -511,6 +710,3 @@ $database->onQuery[] = function ($database, $result) use ($logger) { } }; ``` - -{{maintitle: SQL přístup v Nette Database: plná kontrola nad dotazy}} -{{description: Pište SQL dotazy sami a Nette Database se postará o jejich bezpečnost: parametry proti SQL injection, techniky dotazování, získání dat a informace o dotazech.}} diff --git a/dev/cs/db/explorer-2.texy b/dev/cs/db/explorer-2.texy new file mode 100644 index 0000000000..f1c3b42c09 --- /dev/null +++ b/dev/cs/db/explorer-2.texy @@ -0,0 +1,65 @@ +Database Explorer +***************** + +
    + +Nette Database Explorer je výkonná vrstva, která zásadním způsobem zjednodušuje získávání dat z databáze bez nutnosti psát SQL dotazy. + +- Práce s daty je přirozená a snadno pochopitelná +- Generuje optimalizované SQL dotazy, které načítají pouze potřebná data +- Umožňuje snadný přístup k souvisejícím datům bez nutnosti psát JOIN dotazy +- Funguje okamžitě bez jakékoliv konfigurace či generování entit + +
    + +Nette Database Explorer je nadstavbou nad nízkoúrovňovou vrstou [Nette Database Core |core], která přidává komfortní objektově-orientovaný přístup k databázi. + +Práce s Explorerem začíná voláním metody `table()` nad objektem [api:Nette\Database\Explorer] (jak ho získat je [popsáno tady |core#Připojení a konfigurace]): + +```php +$books = $explorer->table('book'); // 'book' je jméno tabulky +``` + +Metoda vrací objekt [Selection |api:Nette\Database\Table\Selection], který představuje SQL dotaz. Na tento objekt můžeme navazovat další metody pro filtrování a řazení výsledků. Dotaz se sestaví a spustí až ve chvíli, kdy začneme požadovat data. Například procházením cyklem `foreach`. Každý řádek je reprezentován objektem [ActiveRow |api:Nette\Database\Table\ActiveRow]: + +```php +foreach ($books as $book) { + echo $book->title; // výpis sloupce 'title' + echo $book->author_id; // výpis sloupce 'author_id' +} +``` + +Explorer zásadním způsobem usnadňuje práci s [vazbami mezi tabulkami |#Vazby mezi tabulkami]. Následující příklad ukazuje, jak snadno můžeme vypsat data z provázaných tabulek (knihy a jejich autoři). Všimněte si, že nemusíme psát žádné JOIN dotazy, Nette je vytvoří za nás: + +```php +$books = $explorer->table('book'); + +foreach ($books as $book) { + echo 'Kniha: ' . $book->title; + echo 'Autor: ' . $book->author->name; // vytvoří JOIN na tabulku 'author' +} +``` + +Nette Database Explorer optimalizuje dotazy, aby byly co nejefektivnější. Výše uvedený příklad provede pouze dva SELECT dotazy, bez ohledu na to, jestli zpracováváme 10 nebo 10 000 knih. + +Navíc Explorer sleduje, které sloupce se v kódu používají, a načítá z databáze pouze ty, čímž šetří další výkon. Toto chování je plně automatické a adaptivní. Pokud později upravíte kód a začnete používat další sloupce, Explorer automaticky upraví dotazy. Nemusíte nic nastavovat, ani přemýšlet nad tím, které sloupce budete potřebovat - nechte to na Nette. + + +Ruční vytvoření Explorer +======================== + +Pokud nepoužíváte Nette DI kontejner, můžete instanci `Nette\Database\Explorer` vytvořit ručně: + +```php +use Nette\Database; + +// $storage implementuje Nette\Caching\Storage, např.: +$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); +// připojení k databázi +$connection = new Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); +// stará se o reflexi databázové struktury +$structure = new Database\Structure($connection, $storage); +// nebo jiná implementace rozhraní Nette\Database\Conventions; definuje pravidla pro mapování názvů tabulek, sloupců a cizích klíčů +$conventions = new Database\Conventions\DiscoveredConventions($structure); +$explorer = new Database\Explorer($connection, $structure, $conventions, $storage); +``` diff --git a/dev/cs/db/explorer-dram.texy b/dev/cs/db/explorer-dram.texy new file mode 100644 index 0000000000..cd4f676f85 --- /dev/null +++ b/dev/cs/db/explorer-dram.texy @@ -0,0 +1,360 @@ +Explorer: Data Retrieval And Manipulation +***************************************** + + +Data Retrieval +============== + +Pro čtení dat z databáze máme k dispozici několik užitečných metod: + +.[language-php] +| `foreach ($table as $key => $row)` | Iteruje přes všechny řádky, `$key` je hodnota primárního klíče, `$row` je objekt ActiveRow +| `$row = $table->get($key)` | Vrátí jeden řádek podle primárního klíče +| `$row = $table->fetch()` | Vrátí aktuální řádek a posune ukazatel na další +| `$array = $table->fetchPairs()` | Vytvoří asociativní pole z výsledků +| `$array = $table->fetchAll()` | Vráti všechny řádky jako pole +| `count($table)` | Vrátí počet řádků v objektu Selection + +Objekt [ActiveRow |api:Nette\Database\Table\ActiveRow] je určen pouze pro čtení. To znamená, že nelze měnit hodnoty jeho properties. Toto omezení zajišťuje konzistenci dat a zabraňuje neočekávaným vedlejším efektům. Data se načítají z databáze a jakákoliv změna by měla být provedena explicitně a kontrolovaně. + + +`foreach` - iterace přes všechny řádky +-------------------------------------- + +Nejsnazší způsob, jak vykonat dotaz a získat řádky, je iterováním v cyklu `foreach`. Automaticky spouští SQL dotaz. + +```php +$books = $explorer->table('book'); +foreach ($books as $key => $book) { + // $key je hodnota primárního klíče, $book je ActiveRow + echo "$book->title ({$book->author->name})"; +} +``` + + +get($key): ?ActiveRow .[method] +------------------------------- + +Vykoná SQL dotaz a vrátí řádek podle primárního klíče, nebo `null`, pokud neexistuje. + +```php +$book = $explorer->table('book')->get(123); // vrátí ActiveRow s ID 123 nebo null +if ($book) { + echo $book->title; +} +``` + + +fetch(): ?ActiveRow .[method] +----------------------------- + +Vrací jeden řádek a posune interní ukazatel na další. Pokud už neexistují další řádky, vrací `null`. + +```php +$books = $explorer->table('book'); +while ($book = $books->fetch()) { + $this->processBook($book); +} +``` + + +fetchPairs(): array .[method] +----------------------------- + +Vrátí výsledky jako asociativní pole. První argument určuje název sloupce, který se použije jako klíč v poli, druhý argument určuje název sloupce, který se použije jako hodnota: + +```php +$authors = $explorer->table('author')->fetchPairs('id', 'name'); +// [1 => 'John Doe', 2 => 'Jane Doe', ...] +``` + +Pokud je zadán pouze název sloupce pro klíč, bude hodnotou celý řadek, tedy objekt `ActiveRow`: + +```php +$authors = $explorer->table('author')->fetchPairs('id'); +// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] +``` + +Pokud jako klíč uvedeme `null`, bude pole indexováno numericky od nuly: + +```php +$authors = $explorer->table('author')->fetchPairs(null, 'name'); +// [0 => 'John Doe', 1 => 'Jane Doe', ...] +``` + +Jako parametr můžeme také uvést callback, který bude pro každý řádek vracet buď samotnou hodnotu, nebo dvojici klíč-hodnota. Pokud callback vrací pouze hodnotu, klíčem bude primární klíč řádku: + +```php +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); +// [1 => 'První kniha (Jan Novák)', ...] + +// Callback může také vracet pole s dvojicí klíč & hodnota: +$titles = $explorer->table('book') + ->fetchPairs(fn($row) => [$row->title, $row->author->name]); +// ['První kniha' => 'Jan Novák', ...] +``` + + +fetchAll(): array .[method] +--------------------------- + +Vrátí všechny řádky jako asociativní pole objektů `ActiveRow`, kde klíče jsou hodnoty primárních klíčů. + +```php +$allBooks = $explorer->table('book')->fetchAll(); +// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] +``` + + +count(): int .[method] +---------------------- + +Metoda `count()` bez parametru vrací počet řádků v objektu `Selection`: + +```php +$table->where('category', 1); +$count = $table->count(); +$count = count($table); // alternativa +``` + +Pozor, `count()` s parametrem provádí agregační funkci COUNT v databázi, viz níže. + + +ActiveRow::toArray(): array .[method] +------------------------------------- + +Převede objekt `ActiveRow` na asociativní pole, kde klíče jsou názvy sloupců a hodnoty jsou odpovídající data. + +```php +$book = $explorer->table('book')->get(1); +$bookArray = $book->toArray(); +// $bookArray bude ['id' => 1, 'title' => '...', 'author_id' => ..., ...] +``` + + +Agregace +======== + +Třída `Selection` poskytuje metody pro snadné provádění agregačních funkcí (COUNT, SUM, MIN, MAX, AVG atd.). + +.[language-php] +| `count($expr)` | Spočítá počet řádků +| `min($expr)` | Vrátí minimální hodnotu ve sloupci +| `max($expr)` | Vrátí maximální hodnotu ve sloupci +| `sum($expr)` | Vrátí součet hodnot ve sloupci +| `aggregation($function)` | Umožňuje provést libovolnou agregační funkci. Např. `AVG()`, `GROUP_CONCAT()` + + +count(string $expr): int .[method] +---------------------------------- + +Provede SQL dotaz s funkcí COUNT a vrátí výsledek. Metoda se používá k zjištění, kolik řádků odpovídá určité podmínce: + +```php +$count = $table->count('*'); // SELECT COUNT(*) FROM `table` +$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` +``` + +Pozor, [#count()] bez parametru pouze vrací počet řádků v objektu `Selection`. + + +min(string $expr) a max(string $expr) .[method] +----------------------------------------------- + +Metody `min()` a `max()` vrací minimální a maximální hodnotu ve specifikovaném sloupci nebo výrazu: + +```php +// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 +$maxPrice = $products->where('active', true) + ->max('price'); +``` + + +sum(string $expr) .[method] +--------------------------- + +Vrací součet hodnot ve specifikovaném sloupci nebo výrazu: + +```php +// SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 +$totalPrice = $products->where('active', true) + ->sum('price * items_in_stock'); +``` + + +aggregation(string $function, ?string $groupFunction = null) .[method] +---------------------------------------------------------------------- + +Umožňuje provést libovolnou agregační funkci. + +```php +// průměrná cena produktů v kategorii +$avgPrice = $products->where('category_id', 1) + ->aggregation('AVG(price)'); + +// spojí štítky produktu do jednoho řetězce +$tags = $products->where('id', 1) + ->aggregation('GROUP_CONCAT(tag.name) AS tags') + ->fetch() + ->tags; +``` + +Pokud potřebujeme agregovat výsledky, které už samy o sobě vzešly z nějaké agregační funkce a seskupení (např. `SUM(hodnota)` přes seskupené řádky), jako druhý argument uvedeme agregační funkci, která se má na tyto mezivýsledky aplikovat: + +```php +// Vypočítá celkovou cenu produktů na skladě pro jednotlivé kategorie a poté sečte tyto ceny dohromady. +$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') + ->group('category_id') + ->aggregation('SUM(category_total)', 'SUM'); +``` + +V tomto příkladu nejprve vypočítáme celkovou cenu produktů v každé kategorii (`SUM(price * stock) AS category_total`) a seskupíme výsledky podle `category_id`. Poté použijeme `aggregation('SUM(category_total)', 'SUM')` k sečtení těchto mezisoučtů `category_total`. Druhý argument `'SUM'` říká, že se má na mezivýsledky aplikovat funkce SUM. + + +Insert, Update & Delete +======================= + +Nette Database Explorer zjednodušuje vkládání, aktualizaci a mazání dat. Všechny uvedené metody v případě vyhodí výjimku `Nette\Database\DriverException`. + + +Selection::insert(iterable $data) .[method] +------------------------------------------- + +Vloží nové záznamy do tabulky. + +**Vkládání jednoho záznamu:** + +Nový záznam předáme jako asociativní pole nebo iterable objekt (například ArrayHash používaný ve [formulářích |forms:]), kde klíče odpovídají názvům sloupců v tabulce. + +Pokud má tabulka definovaný primární klíč, metoda vrací objekt `ActiveRow`, který se znovunačte z databáze, aby se zohlednily případné změny provedené na úrovni databáze (triggery, výchozí hodnoty sloupců, výpočty auto-increment sloupců). Tím je zajištěna konzistence dat a objekt vždy obsahuje aktuální data z databáze. Pokud jednoznačný primární klíč nemá, vrací předaná data ve formě pole. + +```php +$row = $explorer->table('users')->insert([ + 'name' => 'John Doe', + 'email' => 'john.doe@example.com', +]); +// $row je instance ActiveRow a obsahuje kompletní data vloženého řádku, +// včetně automaticky generovaného ID a případných změn provedených triggery +echo $row->id; // Vypíše ID nově vloženého uživatele +echo $row->created_at; // Vypíše čas vytvoření, pokud je nastaven triggerem +``` + +**Vkládání více záznamů najednou:** + +Metoda `insert()` umožňuje vložit více záznamů pomocí jednoho SQL dotazu. V tomto případě vrací počet vložených řádků. + +```php +$insertedRows = $explorer->table('users')->insert([ + [ + 'name' => 'John', + 'year' => 1994, + ], + [ + 'name' => 'Jack', + 'year' => 1995, + ], +]); +// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) +// $insertedRows bude 2 +``` + +Jako parametr lze také předat objekt `Selection` s výběrem dat. + +```php +$newUsers = $explorer->table('potential_users') + ->where('approved', 1) + ->select('name, email'); + +$insertedRows = $explorer->table('users')->insert($newUsers); +``` + +**Vkládání speciálních hodnot:** + +Jako hodnoty můžeme předávat i soubory, objekty DateTime nebo SQL literály: + +```php +$explorer->table('users')->insert([ + 'name' => 'John', + 'created_at' => new DateTime, // převede na databázový formát + 'avatar' => fopen('image.jpg', 'rb'), // vloží binární obsah souboru + 'uuid' => $explorer::literal('UUID()'), // zavolá funkci UUID() +]); +``` + + +Selection::update(iterable $data): int .[method] +------------------------------------------------ + +Aktualizuje řádky v tabulce podle zadaného filtru. Vrací počet skutečně změněných řádků. + +Měněné sloupce předáme jako asociativní pole nebo iterable objekt (například ArrayHash používaný ve [formulářích |forms:]), kde klíče odpovídají názvům sloupců v tabulce: + +```php +$affected = $explorer->table('users') + ->where('id', 10) + ->update([ + 'name' => 'John Smith', + 'year' => 1994, + ]); +// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 +``` + +Pro změnu číselných hodnot můžeme použít operátory `+=` a `-=`: + +```php +$explorer->table('users') + ->where('id', 10) + ->update([ + 'points+=' => 1, // zvýší hodnotu sloupce 'points' o 1 + 'coins-=' => 1, // sníží hodnotu sloupce 'coins' o 1 + ]); +// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 +``` + + +Selection::delete(): int .[method] +---------------------------------- + +Maže řádky z tabulky podle zadaného filtru. Vrací počet smazaných řádků. + +```php +$count = $explorer->table('users') + ->where('id', 10) + ->delete(); +// DELETE FROM `users` WHERE `id` = 10 +``` + +.[caution] +Při volání `update()` a `delete()` nezapomeňte pomocí `where()` specifikovat řádky, které se mají upravit/smazat. Pokud `where()` nepoužijete, operace se provede na celé tabulce! + + +ActiveRow::update(iterable $data): bool .[method] +------------------------------------------------- + +Aktualizuje data v databázovém řádku reprezentovaném objektem `ActiveRow`. Jako parametr přijímá iterable s daty, která se mají aktualizovat (klíče jsou názvy sloupců). Pro změnu číselných hodnot můžeme použít operátory `+=` a `-=`: + +Po provedení aktualizace se `ActiveRow` automaticky znovu načte z databáze, aby se zohlednily případné změny provedené na úrovni databáze (např. triggery). Metoda vrací true pouze pokud došlo ke skutečné změně dat. + +```php +$article = $explorer->table('article')->get(1); +$article->update([ + 'views += 1', // zvýšíme počet zobrazení +]); +echo $article->views; // Vypíše aktuální počet zobrazení +``` + +Tato metoda aktualizuje pouze jeden konkrétní řádek v databázi. Pro hromadnou aktualizaci více řádků použijte metodu [#Selection::update()]. + + +ActiveRow::delete() .[method] +----------------------------- + +Smaže řádek z databáze, který je reprezentován objektem `ActiveRow`. + +```php +$book = $explorer->table('book')->get(1); +$book->delete(); // Smaže knihu s ID 1 +``` + +Tato metoda maže pouze jeden konkrétní řádek v databázi. Pro hromadné smazání více řádků použijte metodu [#Selection::delete()]. diff --git a/dev/cs/db/explorer-query.texy b/dev/cs/db/explorer-query.texy new file mode 100644 index 0000000000..84defa8008 --- /dev/null +++ b/dev/cs/db/explorer-query.texy @@ -0,0 +1,329 @@ +Explorer: Filtrování a řazení +***************************** + +Třída `Selection` poskytuje metody pro filtrování a řazení výběru dat. + +.[language-php] +| `where($condition, ...$params)` | Přidá podmínku WHERE. Více podmínek je spojeno operátorem AND +| `whereOr(array $conditions)` | Přidá skupinu podmínek WHERE spojených operátorem OR +| `wherePrimary($value)` | Přidá podmínku WHERE podle primárního klíče +| `order($columns, ...$params)` | Nastaví řazení ORDER BY +| `select($columns, ...$params)` | Specifikuje sloupce, které se mají načíst +| `limit($limit, $offset = null)` | Omezí počet řádků (LIMIT) a volitelně nastaví OFFSET +| `page($page, $itemsPerPage, &$total = null)` | Nastaví stránkování +| `group($columns, ...$params)` | Seskupí řádky (GROUP BY) +| `having($condition, ...$params)` | Přidá podmínku HAVING pro filtrování seskupených řádků + +Metody lze řetězit (tzv. [fluent interface|nette:introduction-to-object-oriented-programming#fluent-interfaces]): `$table->where(...)->order(...)->limit(...)`. + +V těchto metodách můžete také používat speciální notaci pro přístup k [datům ze souvisejících tabulek|#Dotazování přes související tabulky]. + + +Escapování a identifikátory +--------------------------- + +Metody automaticky escapují parametry a uvozují identifikátory (názvy tabulek a sloupců), čímž zabraňuje SQL injection. Pro správné fungování je nutné dodržovat několik pravidel: + +- Klíčová slova, názvy funkcí, procedur apod. pište **velkými písmeny**. +- Názvy sloupců a tabulek pište **malými písmeny**. +- Řetězce vždy dosazujte přes **parametry**. + +```php +where('name = ' . $name); // KRITICKÁ ZRANITELNOST: SQL injection +where('name LIKE "%search%"'); // ŠPATNĚ: komplikuje automatické uvozování +where('name LIKE ?', '%search%'); // SPRÁVNĚ: hodnota dosazená přes parametr + +where('name like ?', $name); // ŠPATNĚ: vygeneruje: `name` `like` ? +where('name LIKE ?', $name); // SPRÁVNĚ: vygeneruje: `name` LIKE ? +where('LOWER(name) = ?', $value);// SPRÁVNĚ: LOWER(`name`) = ? +``` + + +where(string|array $condition, ...$parameters): static .[method] +---------------------------------------------------------------- + +Filtruje výsledky pomocí podmínek WHERE. Její silnou stránkou je inteligentní práce s různými typy hodnot a automatická volba SQL operátorů. + +Základní použití: + +```php +$table->where('id', $value); // WHERE `id` = 123 +$table->where('id > ?', $value); // WHERE `id` > 123 +$table->where('id = ? OR name = ?', $id, $name); // WHERE `id` = 1 OR `name` = 'Jon Snow' +``` + +Díky automatické detekci vhodných operátorů nemusíme řešit různé speciální případy. Nette je vyřeší za nás: + +```php +$table->where('id', 1); // WHERE `id` = 1 +$table->where('id', null); // WHERE `id` IS NULL +$table->where('id', [1, 2, 3]); // WHERE `id` IN (1, 2, 3) +// lze použít i zástupný otazník bez operátoru: +$table->where('id ?', 1); // WHERE `id` = 1 +``` + +Metoda správně zpracovává i záporné podmínky a prázdné pole: + +```php +$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- nic nenalezne +$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- nalezene vše +$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- nalezene vše +// $table->where('NOT id ?', $ids); Pozor - tato syntaxe není podporovaná +``` + +Jako parametr můžeme předat také výsledek z jiné tabulky - vytvoří se poddotaz: + +```php +// WHERE `id` IN (SELECT `id` FROM `tableName`) +$table->where('id', $explorer->table($tableName)); + +// WHERE `id` IN (SELECT `col` FROM `tableName`) +$table->where('id', $explorer->table($tableName)->select('col')); +``` + +Podmínky můžeme předat také jako pole, jehož položky se spojí pomocí AND: + +```php +// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) +$table->where([ + 'price_final < price_original', + 'stock_count > min_stock', +]); +``` + +V poli můžeme použít dvojice klíč => hodnota a Nette opět automaticky zvolí správné operátory: + +```php +// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3)) +$table->where([ + 'status' => 'active', + 'id' => [1, 2, 3], +]); +``` + +V poli můžeme kombinovat SQL výrazy se zástupnými otazníky a více parametry. To je vhodné pro komplexní podmínky s přesně definovanými operátory: + +```php +// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) +$table->where([ + 'age > ?' => 18, + 'ROUND(score, ?) > ?' => [2, 75.5], // dva parametry předáme jako pole +]); +``` + +Vícenásobné volání `where()` podmínky automaticky spojuje pomocí AND. + + +whereOr(array $parameters): static .[method] +-------------------------------------------- + +Podobně jako `where()` přidává podmínky, ale s tím rozdílem, že je spojuje pomocí OR: + +```php +// WHERE (`status` = 'active') OR (`deleted` = 1) +$table->whereOr([ + 'status' => 'active', + 'deleted' => true, +]); +``` + +I zde můžeme použít komplexnější výrazy: + +```php +// WHERE (`price` > 1000) OR (`price_with_tax` > 1500) +$table->whereOr([ + 'price > ?' => 1000, + 'price_with_tax > ?' => 1500, +]); +``` + + +wherePrimary(mixed $key): static .[method] +------------------------------------------ + +Přidá podmínku pro primární klíč tabulky: + +```php +// WHERE `id` = 123 +$table->wherePrimary(123); + +// WHERE `id` IN (1, 2, 3) +$table->wherePrimary([1, 2, 3]); +``` + +Pokud má tabulka kompozitní primární klíč (např. `foo_id`, `bar_id`), předáme jej jako pole: + +```php +// WHERE `foo_id` = 1 AND `bar_id` = 5 +$table->wherePrimary(['foo_id' => 1, 'bar_id' => 5])->fetch(); + +// WHERE (`foo_id`, `bar_id`) IN ((1, 5), (2, 3)) +$table->wherePrimary([ + ['foo_id' => 1, 'bar_id' => 5], + ['foo_id' => 2, 'bar_id' => 3], +])->fetchAll(); +``` + + +order(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- + +Určuje pořadí, v jakém budou řádky vráceny. Můžeme řadit podle jednoho či více sloupců, v sestupném či vzestupném pořadí, nebo podle vlastního výrazu: + +```php +$table->order('created'); // ORDER BY `created` +$table->order('created DESC'); // ORDER BY `created` DESC +$table->order('priority DESC, created'); // ORDER BY `priority` DESC, `created` +$table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC +``` + + +select(string $columns, ...$parameters): static .[method] +--------------------------------------------------------- + +Specifikuje sloupce, které se mají vrátit z databáze. Ve výchozím stavu Nette Database Explorer vrací pouze ty sloupce, které se reálně použijí v kódu. Metodu `select()` tak používáme v případech, kdy potřebujeme vrátit specifické výrazy: + +```php +// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` +$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); +``` + +Aliasy definované pomocí `AS` jsou pak dostupné jako vlastnosti objektu ActiveRow: + +```php +foreach ($table as $row) { + echo $row->formatted_date; // přístup k aliasu +} +``` + + +limit(?int $limit, ?int $offset = null): static .[method] +--------------------------------------------------------- + +Omezuje počet vrácených řádků (LIMIT) a volitelně umožňuje nastavit offset: + +```php +$table->limit(10); // LIMIT 10 (vrátí prvních 10 řádků) +$table->limit(10, 20); // LIMIT 10 OFFSET 20 +``` + +Pro stránkování je vhodnější použít metodu `page()`. + + +page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] +------------------------------------------------------------------------- + +Usnadňuje stránkování výsledků. Přijímá číslo stránky (počítané od 1) a počet položek na stránku. Volitelně lze předat referenci na proměnnou, do které se uloží celkový počet stránek: + +```php +$numOfPages = null; +$table->page(page: 3, itemsPerPage: 10, $numOfPages); +echo "Celkem stránek: $numOfPages"; +``` + + +group(string $columns, ...$parameters): static .[method] +-------------------------------------------------------- + +Seskupuje řádky podle zadaných sloupců (GROUP BY). Používá se obvykle ve spojení s agregačními funkcemi: + +```php +// Spočítá počet produktů v každé kategorii +$table->select('category_id, COUNT(*) AS count') + ->group('category_id'); +``` + + +having(string $having, ...$parameters): static .[method] +-------------------------------------------------------- + +Nastavuje podmínku pro filtrování seskupených řádků (HAVING). Lze ji použít ve spojení s metodou `group()` a agregačními funkcemi: + +```php +// Nalezne kategorie, které mají více než 100 produktů +$table->select('category_id, COUNT(*) AS count') + ->group('category_id') + ->having('count > ?', 100); +``` + + +Dotazování přes související tabulky +----------------------------------- + +V metodách `where()`, `select()`, `order()` a `group()` můžeme používat speciální notace pro přístup k sloupcům z jiných tabulek. Explorer automaticky vytvoří potřebné JOINy. + +**Tečková notace** (`nadřazená_tabulka.sloupec`) se používá pro vztah 1:N z pohledu podřízené tabulky: + +```php +$books = $explorer->table('book'); + +// Najde knihy, jejichž autor má jméno začínající na 'Jon' +$books->where('author.name LIKE ?', 'Jon%'); + +// Seřadí knihy podle jména autora sestupně +$books->order('author.name DESC'); + +// Vypíše název knihy a jméno autora +$books->select('book.title, author.name'); +``` + +**Dvojtečková notace** (`:podřízená_tabulka.sloupec`) se používá pro vztah 1:N z pohledu nadřazené tabulky: + +```php +$authors = $explorer->table('author'); + +// Najde autory, kteří napsali knihu s 'PHP' v názvu +$authors->where(':book.title LIKE ?', '%PHP%'); + +// Spočítá počet knih pro každého autora +$authors->select('*, COUNT(:book.id) AS book_count') + ->group('author.id'); +``` + +Ve výše uvedeném příkladu s dvojtečkovou notací (`:book.title`) není specifikován sloupec s cizím klíčem. Explorer automaticky detekuje správný sloupec na základě názvu nadřazené tabulky. V tomto případě se spojuje přes sloupec `book.author_id`, protože název zdrojové tabulky je `author`. Pokud by existovalo více možných spojení, Explorer vyhodí výjimku [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. + +Spojovací sloupec lze explicitně uvést v závorce: + +```php +// Najde autory, kteří přeložili knihu s 'PHP' v názvu +$authors->where(':book(translator).title LIKE ?', '%PHP%'); +``` + +Notace lze řetězit pro přístup přes více tabulek: + +```php +// Najde autory knih označených tagem 'PHP' +$authors->where(':book:book_tag.tag.name', 'PHP') + ->group('author.id'); +``` + + +Rozšíření podmínek pro JOIN +--------------------------- + +Metoda `joinWhere()` rozšiřuje podmínky, které se uvádějí při propojování tabulek v SQL za klíčovým slovem `ON`. + +Dejme tomu, že chceme najít knihy přeložené konkrétním překladatelem: + +```php +// Najde knihy přeložené překladatelem jménem 'David' +$books = $explorer->table('book') + ->joinWhere('translator', 'translator.name', 'David'); +// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') +``` + +V podmínce `joinWhere()` můžeme používat stejné konstrukce jako v metodě `where()` - operátory, zástupné otazníky, pole hodnot či SQL výrazy. + +Pro složitější dotazy s více JOINy můžeme definovat aliasy tabulek: + +```php +$tags = $explorer->table('tag') + ->joinWhere(':book_tag.book.author', 'book_author.born < ?', 1950) + ->alias(':book_tag.book.author', 'book_author'); +// LEFT JOIN `book_tag` ON `tag`.`id` = `book_tag`.`tag_id` +// LEFT JOIN `book` ON `book_tag`.`book_id` = `book`.`id` +// LEFT JOIN `author` `book_author` ON `book`.`author_id` = `book_author`.`id` +// AND (`book_author`.`born` < 1950) +``` + +Všimněte si, že zatímco metoda `where()` přidává podmínky do klauzule `WHERE`, metoda `joinWhere()` rozšiřuje podmínky v klauzuli `ON` při spojování tabulek. diff --git a/database/cs/explorer.texy b/dev/cs/db/explorer.texy similarity index 86% rename from database/cs/explorer.texy rename to dev/cs/db/explorer.texy index 595bd2ab36..00d8278cb4 100644 --- a/database/cs/explorer.texy +++ b/dev/cs/db/explorer.texy @@ -3,7 +3,7 @@ Database Explorer
    -Explorer nabízí intuitivní a efektivní způsob práce s databází. Stará se automaticky o vazby mezi tabulkami a optimalizaci dotazů, takže se můžete soustředit na svou aplikaci. Funguje ihned bez nastavování. Pokud potřebujete plnou kontrolu nad SQL dotazy, můžete využít [SQL přístup |SQL way]. +Nette Database Explorer je výkonná vrstva, která zásadním způsobem zjednodušuje získávání dat z databáze bez nutnosti psát SQL dotazy. - Práce s daty je přirozená a snadno pochopitelná - Generuje optimalizované SQL dotazy, které načítají pouze potřebná data @@ -12,14 +12,15 @@ Explorer nabízí intuitivní a efektivní způsob práce s databází. Stará s
    +Nette Database Explorer je nadstavbou nad nízkoúrovňovou vrstou [Nette Database Core |core], která přidává komfortní objektově-orientovaný přístup k databázi. -S Explorerem začnete voláním metody `table()` objektu [api:Nette\Database\Explorer] (detaily k připojení najdete v kapitole [Připojení a konfigurace |guide#Připojení a konfigurace]): +Práce s Explorerem začíná voláním metody `table()` nad objektem [api:Nette\Database\Explorer] (jak ho získat je [popsáno tady |core#Připojení a konfigurace]): ```php $books = $explorer->table('book'); // 'book' je jméno tabulky ``` -Metoda vrací objekt [Selection |api:Nette\Database\Table\Selection], který představuje SQL dotaz. Na tento objekt můžeme navazovat další metody pro filtrování a řazení výsledků. Dotaz se sestaví a spustí až ve chvíli, kdy začneme požadovat data, například procházením cyklem `foreach`. Každý řádek je reprezentován objektem [ActiveRow |api:Nette\Database\Table\ActiveRow]: +Metoda vrací objekt [Selection |api:Nette\Database\Table\Selection], který představuje SQL dotaz. Na tento objekt můžeme navazovat další metody pro filtrování a řazení výsledků. Dotaz se sestaví a spustí až ve chvíli, kdy začneme požadovat data. Například procházením cyklem `foreach`. Každý řádek je reprezentován objektem [ActiveRow |api:Nette\Database\Table\ActiveRow]: ```php foreach ($books as $book) { @@ -56,19 +57,19 @@ Třída `Selection` poskytuje metody pro filtrování a řazení výběru dat. | `order($columns, ...$params)` | Nastaví řazení ORDER BY | `select($columns, ...$params)` | Specifikuje sloupce, které se mají načíst | `limit($limit, $offset = null)` | Omezí počet řádků (LIMIT) a volitelně nastaví OFFSET -| `page($page, $itemsPerPage, &$numOfPages = null)` | Nastaví stránkování +| `page($page, $itemsPerPage, &$total = null)` | Nastaví stránkování | `group($columns, ...$params)` | Seskupí řádky (GROUP BY) | `having($condition, ...$params)` | Přidá podmínku HAVING pro filtrování seskupených řádků -Metody lze řetězit (tzv. [fluent interface |nette:introduction-to-object-oriented-programming#Fluent Interfaces]): `$table->where(...)->order(...)->limit(...)`. +Metody lze řetězit (tzv. [fluent interface|nette:introduction-to-object-oriented-programming#fluent-interfaces]): `$table->where(...)->order(...)->limit(...)`. -V těchto metodách můžete také používat speciální notaci pro přístup k [datům ze souvisejících tabulek |#Dotazování přes související tabulky]. +V těchto metodách můžete také používat speciální notaci pro přístup k [datům ze souvisejících tabulek|#Dotazování přes související tabulky]. Escapování a identifikátory --------------------------- -Metody automaticky escapují parametry a uvozují identifikátory (názvy tabulek a sloupců), čímž zabraňují SQL injection. Pro správné fungování je nutné dodržovat několik pravidel: +Metody automaticky escapují parametry a uvozují identifikátory (názvy tabulek a sloupců), čímž zabraňuje SQL injection. Pro správné fungování je nutné dodržovat několik pravidel: - Klíčová slova, názvy funkcí, procedur apod. pište **velkými písmeny**. - Názvy sloupců a tabulek pište **malými písmeny**. @@ -112,8 +113,8 @@ Metoda správně zpracovává i záporné podmínky a prázdné pole: ```php $table->where('id', []); // WHERE `id` IS NULL AND FALSE -- nic nenalezne -$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- nalezne vše -$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- nalezne vše +$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- nalezene vše +$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- nalezene vše // $table->where('NOT id ?', $ids); Pozor - tato syntaxe není podporovaná ``` @@ -263,7 +264,7 @@ Usnadňuje stránkování výsledků. Přijímá číslo stránky (počítané o ```php $numOfPages = null; -$table->page(page: 3, itemsPerPage: 10, numOfPages: $numOfPages); +$table->page(page: 3, itemsPerPage: 10, $numOfPages); echo "Celkem stránek: $numOfPages"; ``` @@ -303,7 +304,7 @@ Pro čtení dat z databáze máme k dispozici několik užitečných metod: | `$row = $table->get($key)` | Vrátí jeden řádek podle primárního klíče | `$row = $table->fetch()` | Vrátí aktuální řádek a posune ukazatel na další | `$array = $table->fetchPairs()` | Vytvoří asociativní pole z výsledků -| `$array = $table->fetchAll()` | Vrátí všechny řádky jako pole +| `$array = $table->fetchAll()` | Vráti všechny řádky jako pole | `count($table)` | Vrátí počet řádků v objektu Selection Objekt [ActiveRow |api:Nette\Database\Table\ActiveRow] je určen pouze pro čtení. To znamená, že nelze měnit hodnoty jeho properties. Toto omezení zajišťuje konzistenci dat a zabraňuje neočekávaným vedlejším efektům. Data se načítají z databáze a jakákoliv změna by měla být provedena explicitně a kontrolovaně. @@ -312,7 +313,7 @@ Objekt [ActiveRow |api:Nette\Database\Table\ActiveRow] je určen pouze pro čten `foreach` - iterace přes všechny řádky -------------------------------------- -Nejsnazší způsob, jak vykonat dotaz a získat řádky, je iterování v cyklu `foreach`. Automaticky spustí SQL dotaz. +Nejsnazší způsob, jak vykonat dotaz a získat řádky, je iterováním v cyklu `foreach`. Automaticky spouští SQL dotaz. ```php $books = $explorer->table('book'); @@ -359,14 +360,14 @@ $authors = $explorer->table('author')->fetchPairs('id', 'name'); // [1 => 'John Doe', 2 => 'Jane Doe', ...] ``` -Pokud uvedeme pouze první parametr, bude hodnotou celý řádek, tedy objekt `ActiveRow`: +Pokud uvedeme pouze první parametr, bude hodnotou celý řadek, tedy objekt `ActiveRow`: ```php $authors = $explorer->table('author')->fetchPairs('id'); // [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] ``` -V případě duplicitních klíčů se použije hodnota z posledního řádku. Při použití `null` jako klíče bude pole indexováno numericky od nuly (pak ke kolizím nedochází): +Pokud jako klíč uvedeme `null`, bude pole indexováno numericky od nuly: ```php $authors = $explorer->table('author')->fetchPairs(null, 'name'); @@ -444,7 +445,7 @@ Třída `Selection` poskytuje metody pro snadné provádění agregačních funk count(string $expr): int .[method] ---------------------------------- -Provede SQL dotaz s funkcí COUNT a vrátí výsledek. Metoda se používá ke zjištění, kolik řádků odpovídá určité podmínce: +Provede SQL dotaz s funkcí COUNT a vrátí výsledek. Metoda se používá k zjištění, kolik řádků odpovídá určité podmínce: ```php $count = $table->count('*'); // SELECT COUNT(*) FROM `table` @@ -457,7 +458,7 @@ Pozor, [#count()] bez parametru pouze vrací počet řádků v objektu `Selectio min(string $expr) a max(string $expr) .[method] ----------------------------------------------- -Metody `min()` a `max()` vrací minimální a maximální hodnotu v zadaném sloupci nebo výrazu: +Metody `min()` a `max()` vrací minimální a maximální hodnotu ve specifikovaném sloupci nebo výrazu: ```php // SELECT MAX(`price`) FROM `products` WHERE `active` = 1 @@ -466,10 +467,10 @@ $maxPrice = $products->where('active', true) ``` -sum(string $expr): mixed .[method] ----------------------------------- +sum(string $expr) .[method] +--------------------------- -Vrací součet hodnot v zadaném sloupci nebo výrazu: +Vrací součet hodnot ve specifikovaném sloupci nebo výrazu: ```php // SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 @@ -478,8 +479,8 @@ $totalPrice = $products->where('active', true) ``` -aggregation(string $function, ?string $groupFunction = null): mixed .[method] ------------------------------------------------------------------------------ +aggregation(string $function, ?string $groupFunction = null) .[method] +---------------------------------------------------------------------- Umožňuje provést libovolnou agregační funkci. @@ -510,7 +511,7 @@ V tomto příkladu nejprve vypočítáme celkovou cenu produktů v každé kateg Insert, Update & Delete ======================= -Nette Database Explorer zjednodušuje vkládání, aktualizaci a mazání dat. Všechny uvedené metody v případě chyby vyhodí výjimku `Nette\Database\DriverException`. +Nette Database Explorer zjednodušuje vkládání, aktualizaci a mazání dat. Všechny uvedené metody v případě vyhodí výjimku `Nette\Database\DriverException`. Selection::insert(iterable $data) .[method] @@ -522,7 +523,7 @@ Vloží nové záznamy do tabulky. Nový záznam předáme jako asociativní pole nebo iterable objekt (například ArrayHash používaný ve [formulářích |forms:]), kde klíče odpovídají názvům sloupců v tabulce. -Pokud má tabulka definovaný primární klíč, metoda vrací objekt `ActiveRow`, který se znovunačte z databáze, aby se zohlednily případné změny provedené na úrovni databáze (triggery, výchozí hodnoty sloupců, výpočty auto-increment sloupců). Tím je zajištěna konzistence dat a objekt vždy obsahuje aktuální data z databáze. Pokud tabulka nemá primární klíč, neexistuje identifikovatelný řádek a metoda vrací `null`. +Pokud má tabulka definovaný primární klíč, metoda vrací objekt `ActiveRow`, který se znovunačte z databáze, aby se zohlednily případné změny provedené na úrovni databáze (triggery, výchozí hodnoty sloupců, výpočty auto-increment sloupců). Tím je zajištěna konzistence dat a objekt vždy obsahuje aktuální data z databáze. Pokud jednoznačný primární klíč nemá, vrací předaná data ve formě pole. ```php $row = $explorer->table('users')->insert([ @@ -621,7 +622,7 @@ $count = $explorer->table('users') ``` .[caution] -Při volání `update()` a `delete()` nezapomeňte pomocí `where()` určit řádky, které se mají upravit/smazat. Pokud `where()` nepoužijete, operace se provede na celé tabulce! +Při volání `update()` a `delete()` nezapomeňte pomocí `where()` specifikovat řádky, které se mají upravit/smazat. Pokud `where()` nepoužijete, operace se provede na celé tabulce! ActiveRow::update(iterable $data): bool .[method] @@ -629,7 +630,7 @@ ActiveRow::update(iterable $data): bool .[method] Aktualizuje data v databázovém řádku reprezentovaném objektem `ActiveRow`. Jako parametr přijímá iterable s daty, která se mají aktualizovat (klíče jsou názvy sloupců). Pro změnu číselných hodnot můžeme použít operátory `+=` a `-=`: -Po provedení aktualizace se `ActiveRow` automaticky znovu načte z databáze, aby se zohlednily případné změny provedené na úrovni databáze (např. triggery). Metoda vrací `true` pouze tehdy, pokud došlo ke skutečné změně dat. +Po provedení aktualizace se `ActiveRow` automaticky znovu načte z databáze, aby se zohlednily případné změny provedené na úrovni databáze (např. triggery). Metoda vrací true pouze pokud došlo ke skutečné změně dat. ```php $article = $explorer->table('article')->get(1); @@ -642,10 +643,10 @@ echo $article->views; // Vypíše aktuální počet zobrazení Tato metoda aktualizuje pouze jeden konkrétní řádek v databázi. Pro hromadnou aktualizaci více řádků použijte metodu [#Selection::update()]. -ActiveRow::delete(): int .[method] ----------------------------------- +ActiveRow::delete() .[method] +----------------------------- -Smaže řádek z databáze, který je reprezentován objektem `ActiveRow`. Vrací počet smazaných řádků, což by mělo být 1. +Smaže řádek z databáze, který je reprezentován objektem `ActiveRow`. ```php $book = $explorer->table('book')->get(1); @@ -671,10 +672,10 @@ Pro ilustraci práce s vazbami použijeme příklad databáze knih ([najdete jej V našem příkladu databáze knih najdeme několik typů vztahů (byť model je zjednodušený oproti realitě): -- **One-to-many (1:N)** - každá kniha **má jednoho** autora, autor může napsat **několik** knih -- **Zero-to-many (0:N)** - kniha **může mít** překladatele, překladatel může přeložit **několik** knih -- **Zero-to-one (0:1)** - kniha **může mít** další díl -- **Many-to-many (M:N)** - kniha **může mít několik** tagů a tag může být přiřazen **několika** knihám +- One-to-many 1:N – každá kniha **má jednoho** autora, autor může napsat **několik** knih +- Zero-to-many 0:N – kniha **může mít** překladatele, překladatel může přeložit **několik** knih +- Zero-to-one 0:1 – kniha **může mít** další díl +- Many-to-many M:N – kniha **může mít několik** tagů a tag může být přiřazen **několika** knihám V těchto vztazích vždy existuje tabulka nadřazená a podřízená. Například ve vztahu mezi autorem a knihou je tabulka `author` nadřazená a `book` podřízená - můžeme si to představit tak, že kniha vždy "patří" nějakému autorovi. To se projevuje i ve struktuře databáze: podřízená tabulka `book` obsahuje cizí klíč `author_id`, který odkazuje na nadřazenou tabulku `author`. @@ -716,14 +717,14 @@ echo $book->translator?->name; // najde překladatele podle translator_id Když přistoupíme k property `$book->author`, Explorer v tabulce `book` hledá sloupec, jehož název obsahuje řetězec `author` (tedy `author_id`). Podle hodnoty v tomto sloupci načte odpovídající záznam z tabulky `author` a vrátí jej jako `ActiveRow`. Podobně funguje i `$book->translator`, který využije sloupec `translator_id`. Protože sloupec `translator_id` může obsahovat `null`, použijeme v kódu operátor `?->`. -Alternativní cestu nabízí metoda `ref()`, která přijímá dva argumenty (název cílové tabulky a název spojovacího sloupce) a vrací instanci `ActiveRow` nebo `null`: +Alternativní cestu nabízí metoda `ref()`, která přijímá dva argumenty, název cílové tabulky a název spojovacího sloupce, a vrací instanci `ActiveRow` nebo `null`: ```php echo $book->ref('author', 'author_id')->name; // vazba na autora echo $book->ref('author', 'translator_id')->name; // vazba na překladatele ``` -Metoda `ref()` se hodí, pokud nelze použít přístup přes property, protože tabulka obsahuje sloupec se stejným názvem (tj. `author`). V ostatních případech doporučujeme používat přístup přes property, který je čitelnější. +Metoda `ref()` se hodí, pokud nelze použít přístup přes property, protože tabulka obsahuje sloupec se stejným názvem (tj. `author`). V ostatních případech je doporučeno používat přístup přes property, který je čitelnější. Explorer automaticky optimalizuje databázové dotazy. Když procházíme knihy v cyklu a přistupujeme k jejich souvisejícím záznamům (autorům, překladatelům), Explorer negeneruje dotaz pro každou knihu zvlášť. Místo toho provede pouze jeden SELECT pro každý typ vazby, čímž výrazně snižuje zátěž databáze. Například: @@ -751,7 +752,7 @@ Logika dohledávání spojovacího sloupce je dána implementací [Conventions | Přístup k podřízené tabulce --------------------------- -Přístup k podřízené tabulce funguje v opačném směru. Nyní se ptáme, *jaké knihy napsal tento autor* nebo *přeložil tento překladatel*. Pro tento typ dotazu používáme metodu `related()`, která vrátí `Selection` se souvisejícími záznamy. Podívejme se na příklad: +Přístup k podřízené tabulce funguje v opačném směru. Nyní se ptáme *jaké knihy napsal tento autor* nebo *přeložil tento překladatel*. Pro tento typ dotazu používáme metodu `related()`, která vrátí `Selection` se souvisejícími záznamy. Podívejme se na příklad: ```php $author = $explorer->table('author')->get(1); @@ -805,7 +806,7 @@ SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- id vybraných autorů Vazba Many-to-many ------------------ -Pro vazbu many-to-many (M:N) je potřeba vazební tabulka (v našem případě `book_tag`), která obsahuje dva sloupce s cizími klíči (`book_id`, `tag_id`). Každý z těchto sloupců odkazuje na primární klíč jedné z propojovaných tabulek. Pro získání souvisejících dat nejprve získáme záznamy z vazební tabulky pomocí `related('book_tag')` a dále pokračujeme k cílovým datům: +Pro vazbu many-to-many (M:N) je potřeba existence vazební tabulky (v našem případě `book_tag`), která obsahuje dva sloupce s cizími klíči (`book_id`, `tag_id`). Každý z těchto sloupců odkazuje na primární klíč jedné z propojovaných tabulek. Pro získání souvisejících dat nejprve získáme záznamy z vazební tabulky pomocí `related('book_tag')` a dále pokračujeme k cílovým datům: ```php $book = $explorer->table('book')->get(1); @@ -833,7 +834,7 @@ SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- id tag Dotazování přes související tabulky ----------------------------------- -V metodách `where()`, `select()`, `order()` a `group()` můžeme používat speciální notace pro přístup ke sloupcům z jiných tabulek. Explorer automaticky vytvoří potřebné JOINy. +V metodách `where()`, `select()`, `order()` a `group()` můžeme používat speciální notace pro přístup k sloupcům z jiných tabulek. Explorer automaticky vytvoří potřebné JOINy. **Tečková notace** (`nadřazená_tabulka.sloupec`) se používá pro vztah 1:N z pohledu podřízené tabulky: @@ -863,13 +864,13 @@ $authors->select('*, COUNT(:book.id) AS book_count') ->group('author.id'); ``` -Ve výše uvedeném příkladu s dvojtečkovou notací (`:book.title`) není uveden sloupec s cizím klíčem. Explorer automaticky detekuje správný sloupec na základě názvu nadřazené tabulky. V tomto případě se spojuje přes sloupec `book.author_id`, protože název zdrojové tabulky je `author`. Pokud by existovalo více možných spojení, Explorer vyhodí výjimku [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. +Ve výše uvedeném příkladu s dvojtečkovou notací (`:book.title`) není specifikován sloupec s cizím klíčem. Explorer automaticky detekuje správný sloupec na základě názvu nadřazené tabulky. V tomto případě se spojuje přes sloupec `book.author_id`, protože název zdrojové tabulky je `author`. Pokud by existovalo více možných spojení, Explorer vyhodí výjimku [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. Spojovací sloupec lze explicitně uvést v závorce: ```php // Najde autory, kteří přeložili knihu s 'PHP' v názvu -$authors->where(':book(translator_id).title LIKE ?', '%PHP%'); +$authors->where(':book(translator).title LIKE ?', '%PHP%'); ``` Notace lze řetězit pro přístup přes více tabulek: @@ -911,5 +912,22 @@ $tags = $explorer->table('tag') Všimněte si, že zatímco metoda `where()` přidává podmínky do klauzule `WHERE`, metoda `joinWhere()` rozšiřuje podmínky v klauzuli `ON` při spojování tabulek. -{{maintitle: Nette Database Explorer: práce s tabulkami bez psaní SQL}} -{{description: Pracujte s databází bez psaní SQL: filtrujte, řaďte a agregujte data, vkládejte a upravujte řádky a vazby mezi tabulkami nechte na Exploreru.}} + +Ruční vytvoření Explorer +======================== + +Pokud nepoužíváte Nette DI kontejner, můžete instanci `Nette\Database\Explorer` vytvořit ručně: + +```php +use Nette\Database; + +// $storage implementuje Nette\Caching\Storage, např.: +$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); +// připojení k databázi +$connection = new Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); +// stará se o reflexi databázové struktury +$structure = new Database\Structure($connection, $storage); +// nebo jiná implementace rozhraní Nette\Database\Conventions; definuje pravidla pro mapování názvů tabulek, sloupců a cizích klíčů +$conventions = new Database\Conventions\DiscoveredConventions($structure); +$explorer = new Database\Explorer($connection, $structure, $conventions, $storage); +``` diff --git a/dev/cs/db/home.texy b/dev/cs/db/home.texy new file mode 100644 index 0000000000..8d4f5752ad --- /dev/null +++ b/dev/cs/db/home.texy @@ -0,0 +1,291 @@ +.[perex] +Nette Database je výkonná a elegantní databázová vrstva pro PHP, která vyniká svou jednoduchostí použití a chytrými funkcemi. Nevyžaduje žádnou složitou konfiguraci nebo generování entit, s Nette Database můžete začít pracovat okamžitě. + +Nette Database nabízí dva hlavní přístupy k práci s databází: + +
    +
    + +Database Core +===== +- Nízkoúrovňová vrstva +- Bezpečné parametrizované dotazy +- Podobné PDO, ale s lepším API +- Vhodné když: + - Potřebujete maximální kontrolu nad SQL dotazy + - Píšete vlastní databázovou abstrakci + - Provádíte komplexní databázové operace + + +
    + +
    + +Database Explorer +============ +- Vysokoúrovňová vrstva +- Postavená nad Database Core +- Umožňuje pracovat s daty bez psaní SQL +- Automatická optimalizace dotazů +- Intuitivní práce s relacemi mezi tabulkami +- Vhodné když: + - Chcete rychle a pohodlně pracovat s databází + - Potřebujete automatickou optimalizaci dotazů + - Využíváte vztahy mezi tabulkami + + +
    + +
    + + +Instalace +========= + +Knihovnu stáhnete a nainstalujete pomocí nástroje [Composer|best-practices:composer]: + +```shell +composer require nette/database +``` + + +Připojení a konfigurace +======================= + +Pro připojení k databázi vytvořte instanci třídy [api:Nette\Database\Connection]: + +```php +$db = new Nette\Database\Connection($dsn, $user, $password); +``` + +Parametr `$dsn` (data source name) používá stejný formát [jako PDO|https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], např. `host=127.0.0.1;dbname=test`. + +Elegantněji můžete nastavit připojení pomocí [konfiguračního souboru|configuration]. Stačí přidat sekci `database` a framework se postará o vytvoření potřebných objektů včetně databázového panelu v [Tracy |tracy:]. + +```neon +database: + dsn: 'mysql:host=127.0.0.1;dbname=test' + user: root + password: password +``` + +Objekt pro připojení pak [získáte jako službu z DI kontejneru|dependency-injection:passing-dependencies]: + +```php +class Model +{ + // pro práci s vrstvou Database Explorer si předáme Nette\Database\Explorer + public function __construct( + private Nette\Database\Connection $db, + ) { + } +} +``` + +Více informací o [konfiguraci databáze|configuration]. + + +Výběr vrstvy +==== + +**Database Core** je ideální pro přímou práci s SQL dotazy. Poskytuje bezpečné parametrizované dotazy a lepší API než PDO: + +```php +$database = new Nette\Database\Connection($dsn, $user, $password); + +// SELECT dotazy +$result = $database->query('SELECT * FROM users WHERE role = ?', 'admin'); +foreach ($result as $row) { + echo $row->name; +} + +// INSERT s pomocí pole +$database->query('INSERT INTO users', [ + 'name' => 'John Doe', + 'email' => 'john@example.com', + 'created_at' => new DateTime, +]); +``` + +**Database Explorer** nabízí intuitivní práci s databází bez nutnosti psát SQL dotazy: + +```php +// Vytvoření instance Exploreru +$explorer = new Nette\Database\Explorer($connection, $structure, $conventions); + +// Načtení dat +// - automaticky optimalizuje dotazy +// - načítá pouze použité sloupce +$users = $explorer->table('users'); + +// Jednoduché filtrování +$users->where([ + 'role' => 'admin', + 'active' => true, +]); + +// Práce s relacemi +foreach ($users as $user) { + // Automaticky vytvoří efektivní JOIN + echo $user->name; // jméno uživatele + echo $user->role->description; // popis role z propojené tabulky + + // Výpis všech článků uživatele + foreach ($user->related('articles') as $article) { + echo $article->title; + } +} + +// Vložení dat +$users->insert([ + 'name' => 'John Doe', + 'email' => 'john@example.com', +]); +``` + +Obě vrstvy lze v aplikaci kombinovat - pro běžné operace použít Explorer a pro specifické případy přepnout na Core. + + +--- + +
    +
    + + +Automatická optimalizace výkonu +------------------------------- +- Inteligentní načítání souvisejících dat +- Adaptivní načítání pouze potřebných sloupců +- Minimalizace počtu databázových dotazů + +
    + +
    + + +Bezpečnost na prvním místě +-------------------------- +- Vestavěná ochrana proti SQL injection +- Parametrizované dotazy +- Bezpečné zpracování vstupních dat + +
    + +
    + + +Intuitivní práce s relacemi +--------------------------- +- Přirozený přístup k propojeným datům +- Podpora všech typů vazeb (1:1, 1:N, M:N) +- Bez nutnosti psát JOIN dotazy + +
    + +
    + + +Pohodlné debuggování +-------------------- +- Panel do [Tracy|tracy:] +- Všechny provedené dotazy s časy +- Vysvětlení dotazů (EXPLAIN) + +
    + +
    + + +Nejjednodušší parametrické dotazy +--------------------------------- + +Stačí jen čárka a hodnota: + +
    +```php .[dark] +$database->query(' + SELECT * + FROM users + WHERE name =', $name +); +``` +
    + +Žádné `?`, `:param`, `@param` nebo jiné speciální syntaxe - prostě jen otazník. + +
    + +
    + + +Chytrá detekce vazeb +-------------------- +Nepotřebujete konfigurovat entity ani mapování: + +
    +```php .[dark] +$book = $explorer->table('book')->get(1); +// automaticky nalezne vazbu přes book.author_id +echo $book->author->name; +``` +
    + +
    + +
    + + +Adaptivní načítání dat +---------------------- + +Automaticky načítá jen sloupce, které skutečně používáte v kódu + +
    +```php .[dark] +foreach ($books as $book) { + // načte z databáze jen sloupec 'title' + echo $book->title; +} +``` +
    + +
    + +
    + + +Přes 18 let vývoje +================== +Nette vyvíjíme přes 18 let - a číslo stále roste! Knihovny, které poskytujeme, jsou proto **velmi zralé, stabilní a široce používané**. Věří jim řada globálních korporací a pohání mnoho významných webových stránek. Kdo používá a důvěřuje Nette? + +
    +
    + + +Instalace +========= + +Knihovnu stáhnete a nainstalujete pomocí nástroje [Composer|best-practices:composer]: + +```shell .[dark] +composer require nette/database +``` + + +Podporované databáze +-------------------- + +Nette podporuje následující databáze: + +|* Databázový server |* DSN jméno |* Podpora v Core |* Podpora v Explorer +| MySQL (>= 5.1) | mysql | ANO | ANO +| PostgreSQL (>= 9.0) | pgsql | ANO | ANO +| Sqlite 3 (>= 3.8) | sqlite | ANO | ANO +| Oracle | oci | ANO | - +| MS SQL (PDO_SQLSRV) | sqlsrv | ANO | ANO +| MS SQL (PDO_DBLIB) | mssql | ANO | - +| ODBC | odbc | ANO | - + + +{{title: Nette Database}} +{{description: Nette Database zásadním způsobem zjednodušuje získávání dat z databáze bez nutnosti psát SQL dotazy. Pokládá efektivní dotazy a nepřenáší zbytečná data.}} diff --git a/dev/cs/db/reflection.texy b/dev/cs/db/reflection.texy new file mode 100644 index 0000000000..eb2a063055 --- /dev/null +++ b/dev/cs/db/reflection.texy @@ -0,0 +1,173 @@ +Reflexe +******* + +.{data-version:3.2.4} +Nette Database poskytuje nástroje pro introspekci databázové struktury pomocí třídy [api:Nette\Database\Reflection\Reflection]. Ta umožňuje získávat informace o tabulkách, sloupcích, indexech a cizích klíčích. Reflexi můžete využít ke generování schémat, vytváření flexibilních aplikací pracujících s databází nebo obecných databázových nástrojů. + +Objekt reflexe získáme z instance připojení k databázi: + +```php +$reflection = $connection->getReflection(); +``` + + +Práce s tabulkami +================= + +Pomocí reflexe můžeme procházet všechny tabulky v databázi: + + +getTables(): Nette\Database\Reflection\Table[] .[method] +-------------------------------------------------------- +Vrací asocitivní pole, kde klíčem je název tabulky a hodnotou pole s metadaty tabulky. + +```php +// Výpis názvů všech tabulek +foreach ($reflection->getTables() as $table) { + echo $table['name'] . "\n"; +} +``` + + +hasTable(string $name): bool .[method] +-------------------------------------- +Vrací `true`, pokud tabulka existuje, jinak `false`. + +```php +// Ověření existence tabulky +if ($reflection->hasTable('users')) { + echo "Tabulka users existuje"; +} +``` + + +getTable(string $name): Nette\Database\Reflection\Table .[method] +----------------------------------------------------------------- +Vrací objekt `Nette\Database\Reflection\Table` reprezentující danou tabulku. Pokud tabulka neexistuje, vyhodí výjimku `Nette\Database\Exception\MissingTableException`. + +```php +// Získání konkrétní tabulky +$table = $reflection->getTable('users'); +``` + + +Informace o sloupcích +===================== + +Objekt [api:Nette\Database\Reflection\Table], který získáme voláním `getTable()`, nám umožňuje získat detailní informace o sloupcích tabulky. + + +getColumns(): Nette\Database\Reflection\Column[] .[method] +---------------------------------------------------------- +Vrací pole objektů `Nette\Database\Reflection\Column` reprezentujících sloupce tabulky. + + +getColumn(string $name): Nette\Database\Reflection\Column .[method] +------------------------------------------------------------------- +Vrací objekt [api:Nette\Database\Reflection\Column] reprezentující daný sloupec. Pokud sloupec neexistuje, vyhodí výjimku `Nette\Database\Exception\MissingColumnException`. + +Objekt `Column` poskytuje tyto vlastnosti: + +- `name`: Název sloupce. +- `nativeType`: Datový typ sloupce specifický pro danou databázi. +- `type`: Normalizovaný datový typ sloupce (viz konstanty `Nette\Utils\Type`). +- `nullable`: `true`, pokud sloupec může obsahovat hodnotu `NULL`, jinak `false`. +- `primary`: `true`, pokud je sloupec součástí primárního klíče, jinak `false`. +- `autoIncrement`: `true`, pokud je sloupec auto-increment, jinak `false`. +- `default`: Výchozí hodnota sloupce, nebo `null`, pokud není definována. +- `vendor`: Pole s dalšími informacemi specifickými pro danou databázi. + +```php +// Procházení všech sloupců tabulky users +$table = $reflection->getTable('users'); +foreach ($table->getColumns() as $column) { + echo "Sloupec: " . $column->name . "\n"; + echo "Typ: " . $column->nativeType . "\n"; + echo "Může být NULL: " . ($column->nullable ? 'Ano' : 'Ne') . "\n"; + echo "Výchozí hodnota: " . ($column->default ?? 'Není') . "\n"; + echo "Je primární klíč: " . ($column->primary ? 'Ano' : 'Ne') . "\n"; + echo "Je auto-increment: " . ($column->autoIncrement ? 'Ano' : 'Ne') . "\n"; +} + +// Získání konkrétního sloupce +$idColumn = $table->getColumn('id'); +``` + + +Indexy a primární klíče +======================= + + +getIndexes(): Nette\Database\Reflection\Index[] .[method] +--------------------------------------------------------- +Vrací pole objektů `Nette\Database\Reflection\Index` reprezentujících indexy tabulky. + + +getIndex(string $name): Nette\Database\Reflection\Index .[method] +----------------------------------------------------------------- +Vrací objekt [api:Nette\Database\Reflection\Index] reprezentující daný index. Pokud index neexistuje, vyhodí výjimku `Nette\Database\Exception\MissingIndexException`. + + +getPrimaryKey(): ?Nette\Database\Reflection\Index .[method] +----------------------------------------------------------- +Vrací objekt `Nette\Database\Reflection\Index` reprezentující primární klíč tabulky, nebo `null`, pokud tabulka nemá primární klíč. + +Objekt `Index` poskytuje tyto vlastnosti: + +- `name`: Název indexu. +- `columns`: Pole objektů `Nette\Database\Reflection\Column` reprezentujících sloupce, které jsou součástí indexu. +- `unique`: `true`, pokud je index unikátní, jinak `false`. +- `primary`: `true`, pokud je index primárním klíčem, jinak `false`. + +```php +$table = $reflection->getTable('users'); + +$vypisNazvySloupcu = fn(array $columns) => implode(', ', array_map(fn($col) => $col->name, $columns)); + +// Výpis všech indexů +foreach ($table->getIndexes() as $index) { + echo "Index: " . ($index->name ?? 'Nepojmenovaný') . "\n"; + echo "Sloupce: " . $vypisNazvySloupcu($index->columns) . "\n"; + echo "Je unikátní: " . ($index->unique ? 'Ano' : 'Ne') . "\n"; + echo "Je primární klíč: " . ($index->primary ? 'Ano' : 'Ne') . "\n"; +} + +// Získání primárního klíče +if ($primaryKey = $table->getPrimaryKey()) { + echo "Primární klíč: " . $vypisNazvySloupcu($primaryKey->columns) . "\n"; +} +``` + + +Cizí klíče +========== + + +getForeignKeys(): Nette\Database\Reflection\ForeignKey[] .[method] +------------------------------------------------------------------ +Vrací pole objektů `Nette\Database\Reflection\ForeignKey` reprezentujících cizí klíče tabulky. + + +getForeignKey(string $name): Nette\Database\Reflection\ForeignKey .[method] +--------------------------------------------------------------------------- +Vrací objekt [api:Nette\Database\Reflection\ForeignKey] reprezentující daný cizí klíč. Pokud cizí klíč neexistuje, vyhodí výjimku `Nette\Database\Exception\MissingForeignKeyException`. + +Objekt `ForeignKey` poskytuje tyto vlastnosti: + +- `name`: Název cizího klíče. +- `localColumns`: Pole objektů `Nette\Database\Reflection\Column` reprezentujících lokální sloupce, které tvoří cizí klíč. +- `foreignTable`: Objekt `Nette\Database\Reflection\Table` reprezentující cizí tabulku, na kterou cizí klíč odkazuje. +- `foreignColumns`: Pole objektů `Nette\Database\Reflection\Column` reprezentujících cizí sloupce, na které cizí klíč odkazuje. + +```php +$table = $reflection->getTable('books'); + +$vypisNazvySloupcu = fn(array $columns) => implode(', ', array_map(fn($col) => $col->name, $columns)); + +foreach ($table->getForeignKeys() as $fk) { + echo "Cizí klíč: " . ($fk->name ?? 'Nepojmenovaný') . "\n"; + echo "Lokální sloupce: " . $vypisNazvySloupcu($fk->localColumns) . "\n"; + echo "Odkazuje na tabulku: {$fk->foreignTable->name}\n"; + echo "Odkazuje na sloupce: " . $vypisNazvySloupcu($fk->foreignColumns) . "\n"; +} +``` diff --git a/dev/cs/db/relations.texy b/dev/cs/db/relations.texy new file mode 100644 index 0000000000..7002fa0cbc --- /dev/null +++ b/dev/cs/db/relations.texy @@ -0,0 +1,177 @@ +Explorer: Vazby mezi tabulkami +****************************** + +V relačních databázích jsou data rozdělena do více tabulek a navzájem propojená pomocí cizích klíčů. Nette Database Explorer přináší revoluční způsob, jak s těmito vazbami pracovat - bez psaní JOIN dotazů a nutnosti cokoliv konfigurovat nebo generovat. + +Pro ilustraci práce s vazbami použijeme příklad databáze knih ([najdete jej na GitHubu |https://github.com/nette-examples/books]). V databázi máme tabulky: + +- `author` - spisovatelé a překladatelé (sloupce `id`, `name`, `web`, `born`) +- `book` - knihy (sloupce `id`, `author_id`, `translator_id`, `title`, `sequel_id`) +- `tag` - štítky (sloupce `id`, `name`) +- `book_tag` - vazební tabulka mezi knihami a štítky (sloupce `book_id`, `tag_id`) + +[* db-schema-1-.webp *] *** Struktura databáze .<> + +V našem příkladu databáze knih najdeme několik typů vztahů (byť model je zjednodušený oproti realitě): + +- One-to-many 1:N – každá kniha **má jednoho** autora, autor může napsat **několik** knih +- Zero-to-many 0:N – kniha **může mít** překladatele, překladatel může přeložit **několik** knih +- Zero-to-one 0:1 – kniha **může mít** další díl +- Many-to-many M:N – kniha **může mít několik** tagů a tag může být přiřazen **několika** knihám + +V těchto vztazích vždy existuje tabulka nadřazená a podřízená. Například ve vztahu mezi autorem a knihou je tabulka `author` nadřazená a `book` podřízená - můžeme si to představit tak, že kniha vždy "patří" nějakému autorovi. To se projevuje i ve struktuře databáze: podřízená tabulka `book` obsahuje cizí klíč `author_id`, který odkazuje na nadřazenou tabulku `author`. + +Potřebujeme-li vypsat knihy včetně jmen jejich autorů, máme dvě možnosti. Buď data získáme jediným SQL dotazem pomocí JOIN: + +```sql +SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id +``` + +Nebo načteme data ve dvou krocích - nejprve knihy a pak jejich autory - a potom je v PHP poskládáme: + +```sql +SELECT * FROM book; +SELECT * FROM author WHERE id IN (1, 2, 3); -- ids autorů získaných knih +``` + +Druhý přístup je ve skutečnosti efektivnější, i když to může být překvapivé. Data jsou načtena pouze jednou a mohou být lépe využita v cache. Právě tímto způsobem pracuje Nette Database Explorer - vše řeší pod povrchem a vám nabízí elegantní API: + +```php +$books = $explorer->table('book'); +foreach ($books as $book) { + echo 'title: ' . $book->title; + echo 'written by: ' . $book->author->name; // $book->author je záznam z tabulky 'author' + echo 'translated by: ' . $book->translator?->name; +} +``` + + +Přístup k nadřazené tabulce +--------------------------- + +Přístup k nadřazené tabulce je přímočarý. Jde o vztahy jako *kniha má autora* nebo *kniha může mít překladatele*. Související záznam získáme přes property objektu ActiveRow - její název odpovídá názvu sloupce s cizím klíčem bez `id`: + +```php +$book = $explorer->table('book')->get(1); +echo $book->author->name; // najde autora podle sloupce author_id +echo $book->translator?->name; // najde překladatele podle translator_id +``` + +Když přistoupíme k property `$book->author`, Explorer v tabulce `book` hledá sloupec, jehož název obsahuje řetězec `author` (tedy `author_id`). Podle hodnoty v tomto sloupci načte odpovídající záznam z tabulky `author` a vrátí jej jako `ActiveRow`. Podobně funguje i `$book->translator`, který využije sloupec `translator_id`. Protože sloupec `translator_id` může obsahovat `null`, použijeme v kódu operátor `?->`. + +Alternativní cestu nabízí metoda `ref()`, která přijímá dva argumenty, název cílové tabulky a název spojovacího sloupce, a vrací instanci `ActiveRow` nebo `null`: + +```php +echo $book->ref('author', 'author_id')->name; // vazba na autora +echo $book->ref('author', 'translator_id')->name; // vazba na překladatele +``` + +Metoda `ref()` se hodí, pokud nelze použít přístup přes property, protože tabulka obsahuje sloupec se stejným názvem (tj. `author`). V ostatních případech je doporučeno používat přístup přes property, který je čitelnější. + +Explorer automaticky optimalizuje databázové dotazy. Když procházíme knihy v cyklu a přistupujeme k jejich souvisejícím záznamům (autorům, překladatelům), Explorer negeneruje dotaz pro každou knihu zvlášť. Místo toho provede pouze jeden SELECT pro každý typ vazby, čímž výrazně snižuje zátěž databáze. Například: + +```php +$books = $explorer->table('book'); +foreach ($books as $book) { + echo $book->title . ': '; + echo $book->author->name; + echo $book->translator?->name; +} +``` + +Tento kód zavolá pouze tyto tři bleskové dotazy do databáze: + +```sql +SELECT * FROM `book`; +SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- id ze sloupce author_id vybraných knih +SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- id ze sloupce translator_id vybraných knih +``` + +Viz tečková notace. + +.[note] +Logika dohledávání spojovacího sloupce je dána implementací [Conventions |api:Nette\Database\Conventions]. Doporučujeme použití [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], které analyzuje cizí klíče a umožňuje jednoduše pracovat s existujícími vztahy mezi tabulkami. + + +Přístup k podřízené tabulce +--------------------------- + +Přístup k podřízené tabulce funguje v opačném směru. Nyní se ptáme *jaké knihy napsal tento autor* nebo *přeložil tento překladatel*. Pro tento typ dotazu používáme metodu `related()`, která vrátí `Selection` se souvisejícími záznamy. Podívejme se na příklad: + +```php +$author = $explorer->table('author')->get(1); + +// Vypíše všechny knihy od autora +foreach ($author->related('book.author_id') as $book) { + echo "Napsal: $book->title"; +} + +// Vypíše všechny knihy, které autor přeložil +foreach ($author->related('book.translator_id') as $book) { + echo "Přeložil: $book->title"; +} +``` + +Metoda `related()` přijímá popis spojení jako jeden argument s tečkovou notací nebo jako dva samostatné argumenty: + +```php +$author->related('book.translator_id'); // jeden argument +$author->related('book', 'translator_id'); // dva argumenty +``` + +Explorer dokáže automaticky detekovat správný spojovací sloupec na základě názvu nadřazené tabulky. V tomto případě se spojuje přes sloupec `book.author_id`, protože název zdrojové tabulky je `author`: + +```php +$author->related('book'); // použije book.author_id +``` + +Pokud by existovalo více možných spojení, Explorer vyhodí výjimku [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. + +Metodu `related()` můžeme samozřejmě použít i při procházení více záznamů v cyklu a Explorer i v tomto případě automaticky optimalizuje dotazy: + +```php +$authors = $explorer->table('author'); +foreach ($authors as $author) { + echo $author->name . ' napsal:'; + foreach ($author->related('book') as $book) { + echo $book->title; + } +} +``` + +Tento kód vygeneruje pouze dva bleskové SQL dotazy: + +```sql +SELECT * FROM `author`; +SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- id vybraných autorů +``` + +Viz dvojtečková notace. + + +Vazba Many-to-many +------------------ + +Pro vazbu many-to-many (M:N) je potřeba existence vazební tabulky (v našem případě `book_tag`), která obsahuje dva sloupce s cizími klíči (`book_id`, `tag_id`). Každý z těchto sloupců odkazuje na primární klíč jedné z propojovaných tabulek. Pro získání souvisejících dat nejprve získáme záznamy z vazební tabulky pomocí `related('book_tag')` a dále pokračujeme k cílovým datům: + +```php +$book = $explorer->table('book')->get(1); +// vypíše názvy tagů přiřazených ke knize +foreach ($book->related('book_tag') as $bookTag) { + echo $bookTag->tag->name; // vypíše název tagu přes vazební tabulku +} + +$tag = $explorer->table('tag')->get(1); +// nebo opačně: vypíše názvy knih označených tímto tagem +foreach ($tag->related('book_tag') as $bookTag) { + echo $bookTag->book->title; // vypíše název knihy +} +``` + +Explorer opět optimalizuje SQL dotazy do efektivní podoby: + +```sql +SELECT * FROM `book`; +SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- id vybraných knih +SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- id tagů nalezených v book_tag +``` diff --git a/database/cs/security.texy b/dev/cs/db/security.texy similarity index 59% rename from database/cs/security.texy rename to dev/cs/db/security.texy index cd27787347..01a6ef8242 100644 --- a/database/cs/security.texy +++ b/dev/cs/db/security.texy @@ -38,10 +38,10 @@ $table->where("name = '$_GET[name]'"); ``` -Parametrizované dotazy -====================== +Bezpečné parametrizované dotazy +=============================== -Základní obranou proti SQL injection jsou parametrizované dotazy. Nette Database nabízí několik způsobů jejich použití. +Bezpečným způsobem vkládání hodnot do SQL dotazů jsou parametrizované dotazy. Nette Database nabízí několik způsobů jejich použití. Nejjednodušší způsob je použití **zástupných otazníků**: @@ -55,7 +55,7 @@ $table->where('name = ?', $name); Tohle platí pro všechny další metody v [Database Explorer|explorer], které umožňují vkládat výrazy se zástupnými otazníky a parametry. -Pro příkazy INSERT, UPDATE nebo klauzuli WHERE můžeme předat hodnoty v poli: +Pro příkazy INSERT, UPDATE nebo klauzuli WHERE můžeme bezpečně předat hodnoty v poli: ```php // ✅ Bezpečný INSERT @@ -71,56 +71,23 @@ $table->insert([ ]); ``` +.[warning] +Musíme však zajistit [správný datový typ parametrů|#Validace vstupních dat]. -Validace hodnot parametrů -========================= - -Parametrizované dotazy jsou základním stavebním kamenem bezpečné práce s databází. Avšak hodnoty, které do nich vkládáme, musí projít několika úrovněmi kontrol: - - -Typová kontrola ---------------- - -**Nejdůležitější je zajistit správný datový typ parametrů** - to je nutná podmínka pro bezpečné použití Nette Database. Databáze předpokládá, že všechna vstupní data mají správný datový typ odpovídající danému sloupci. - -Například pokud by `$name` v předchozích příkladech bylo neočekávaně pole místo řetězce, Nette Database by se pokusila vložit všechny jeho prvky do SQL dotazu, což by vedlo k chybě. Proto **nikdy nepoužívejte** nevalidovaná data z `$_GET`, `$_POST` nebo `$_COOKIE` přímo v databázových dotazech. - - -Formátová kontrola ------------------- - -Na druhé úrovni kontrolujeme formát dat - například zda jsou řetězce v UTF-8 kódování a jejich délka odpovídá definici sloupce, nebo zda jsou číselné hodnoty v povoleném rozsahu pro daný datový typ sloupce. - -U této úrovně validace se můžeme částečně spolehnout i na databázi samotnou - mnoho databází odmítne nevalidní data. Nicméně chování se může lišit, některé mohou dlouhé řetězce tiše zkrátit nebo čísla mimo rozsah oříznout. +Klíče polí nejsou bezpečné API +------------------------------ -Doménová kontrola ------------------ - -Třetí úroveň představují logické kontroly specifické pro vaši aplikaci. Například ověření, že hodnoty ze select boxů odpovídají nabízeným možnostem, že čísla jsou v očekávaném rozsahu (např. věk 0-150 let) nebo že vzájemné závislosti mezi hodnotami dávají smysl. - - -Doporučené způsoby validace ---------------------------- - -- Používejte [Nette Formuláře|forms:], které automaticky zajistí správnou validaci všech vstupů -- Používejte [Presentery|application:] a uvádějte u parametrů v `action*()` a `render*()` metodách datové typy -- Nebo implementujte vlastní validační vrstvu pomocí standardních PHP nástrojů jako `filter_var()` - - -Bezpečná práce se sloupci -========================= - -V předchozí sekci jsme si ukázali, jak správně validovat hodnoty parametrů. Při použití polí v SQL dotazech však musíme věnovat stejnou pozornost i jejich klíčům. +Zatímco hodnoty v polích jsou bezpečné, o klíčích to neplatí! ```php // ❌ NEBEZPEČNÝ KÓD - nejsou ošetřené klíče v poli $database->query('INSERT INTO users', $_POST); ``` -U příkazů INSERT a UPDATE je to zásadní bezpečnostní chyba - útočník může do databáze vložit nebo změnit jakýkoliv sloupec. Mohl by si například nastavit `is_admin = 1` nebo vložit libovolná data do citlivých sloupců (tzv. Mass Assignment Vulnerability). +U příkazů INSERT a UPDATE je to zásadní bezpečnostní chyba - útočník může do databáze vložit nebo změnit jakýkoliv sloupec. Mohl by si například nastavit `is_admin = 1` nebo vložit libovolná data do citlivých sloupců (tzv Mass Assignment Vulnerability). -Ve WHERE podmínkách je to ještě nebezpečnější, protože mohou obsahovat operátory: +Ve WHERE podmínkách je to ještě nebezpečnější, protože mohou obsahovat oprátory: ```php // ❌ NEBEZPEČNÝ KÓD - nejsou ošetřené klíče v poli @@ -129,41 +96,51 @@ $database->query('SELECT * FROM users WHERE', $_POST); // vykoná dotaz WHERE (`salary` > 100000) ``` -Útočník může tento přístup využít k systematickému zjišťování platů zaměstnanců. Začne například dotazem na platy nad 100 000, pak pod 50 000 a postupným zužováním rozsahu může odhalit přibližné platy všech zaměstnanců. Tento typ útoku se nazývá SQL enumeration. +Útočník může tento přístup využít k systematickému zjišťování platů zaměstnanců. Začne například dotazem na platy nad 100.000, pak pod 50.000 a postupným zužováním rozsahu může odhalit přibližné platy všech zaměstnanců. Tento typ útoku se nazývá SQL enumeration. -Metody `where()` a `whereOr()` jsou ještě [mnohem flexibilnější |explorer#where] a podporují v klíčích a hodnotách SQL výrazy včetně operátorů a funkcí. To dává útočníkovi možnost provést SQL injection: +Metoda `where()` podporuje v klíčích SQL výrazy včetně operátorů a funkcí. To dává útočníkovi možnost provést komplexní SQL injection: ```php // ❌ NEBEZPEČNÝ KÓD - útočník může vložit vlastní SQL -$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1']; +$_POST['0) UNION SELECT name, salary FROM users WHERE (?'] = 1; $table->where($_POST); // vykoná dotaz WHERE (0) UNION SELECT name, salary FROM users WHERE (1) ``` -Tento útok ukončí původní podmínku pomocí `0)`, připojí vlastní `SELECT` pomocí `UNION`, aby získal citlivá data z tabulky `users` a uzavře syntakticky správný dotaz pomocí `WHERE (1)`. +Tento útok ukončí původní podmínku pomocí `0)`, připojí vlastní `SELECT` pomocí `UNION` aby získal citlivá data z tabulky `users` a uzavře syntakticky správný dotaz pomocí `WHERE (1)`. Whitelist sloupců ----------------- -Pro bezpečnou práci s názvy sloupců potřebujeme mechanismus, který zajistí, že uživatel může pracovat pouze s povolenými sloupci a nemůže přidat vlastní. Mohli bychom se pokusit detekovat a blokovat nebezpečné názvy sloupců (blacklist), ale tento přístup je nespolehlivý - útočník může vždy přijít s novým způsobem, jak nebezpečný název sloupce zapsat, který jsme nepředvídali. - -Proto je mnohem bezpečnější obrátit logiku a definovat explicitní seznam povolených sloupců (whitelist): +Pokud chcete uživateli umožnit volbu sloupců, vždy použijte whitelist: ```php -// Sloupce, které může uživatel upravovat +// ✅ Bezpečné zpracování - pouze povolené sloupce $allowedColumns = ['name', 'email', 'active']; +$values = array_intersect_key($_POST, array_flip($allowedColumns)); -// Odstraníme všechny nepovolené sloupce ze vstupu -$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); - -// ✅ Nyní můžeme bezpečně použít v dotazech, jako například: -$database->query('INSERT INTO users', $filteredData); -$table->update($filteredData); -$table->where($filteredData); +$database->query('INSERT INTO users', $values); ``` +Validace vstupních dat +====================== + +**Nejdůležitější je zajistit správný datový typ parametrů** - to je nutná podmínka pro bezpečné použití Nette Database. Databáze předpokládá, že všechna vstupní data mají správný datový typ odpovídající danému sloupci. + +Například pokud by `$name` v předchozích příkladech bylo neočekávaně pole místo řetězce, Nette Database by se pokusilo vložit všechny jeho prvky do SQL dotazu, což by vedlo k chybě. Proto **nikdy nepoužívejte** nevalidovaná data z `$_GET`, `$_POST` nebo `$_COOKIE` přímo v databázových dotazech. + +Na druhé úrovni kontrolujeme technickou validitu dat - například zda jsou řetězce v UTF-8 kódování a jejich délka odpovídá definici sloupce, nebo zda jsou číselné hodnoty v povoleném rozsahu pro daný datový typ sloupce. U této úrovně validace se můžeme částečně spolehnout i na databázi samotnou - mnoho databází odmítne nevalidní data. Nicméně chování se může lišit, některé mohou dlouhé řetězce tiše zkrátit nebo čísla mimo rozsah oříznout. + +Třetí úroveň představují logické kontroly specifické pro vaši aplikaci. Například ověření, že hodnoty ze select boxů odpovídají nabízeným možnostem, že čísla jsou v očekávaném rozsahu (např. věk 0-150 let) nebo že vzájemné závislosti mezi hodnotami dávají smysl. + +Doporučené způsoby implementace validace: +- Používejte [Nette Formuláře|forms:], které automaticky zajistí správnou validaci všech vstupů +- Používejte [Presentery|application:] a uvádějte u parametrů v `action*()` a `render*()` metodách datové typy +- Nebo implementujte vlastní validační vrstvu pomocí standardních PHP nástrojů jako `filter_var()` + + Dynamické identifikátory ======================== @@ -175,14 +152,9 @@ $table = 'users'; $column = 'name'; $database->query('SELECT ?name FROM ?name', $column, $table); // Výsledek v MySQL: SELECT `name` FROM `users` -``` -Důležité: symbol `?name` používejte pouze pro důvěryhodné hodnoty definované v kódu aplikace. Pro hodnoty od uživatele použijte opět [whitelist |#Whitelist sloupců]. Jinak se vystavujete bezpečnostním rizikům: - -```php // ❌ NEBEZPEČNÉ - nikdy nepoužívejte vstup od uživatele $database->query('SELECT ?name FROM users', $_GET['column']); ``` -{{maintitle: Ochrana před SQL injection v PHP a Nette Database}} -{{description: Co je SQL injection a jak mu v Nette Database předejít: parametrizované dotazy, validace vstupů, bezpečná práce se sloupci a dynamické identifikátory.}} +Důležité: symbol `?name` používejte pouze pro důvěryhodné hodnoty definované v kódu aplikace. Pro hodnoty od uživatele použijte opět whitelist. Jinak se vystavujete bezpečnostním rizikům, jako například dříve uvedený SQL enumeration nebo Mass Assignment Vulnerability. diff --git a/dev/cs/db/transactions.texy b/dev/cs/db/transactions.texy new file mode 100644 index 0000000000..abb48b1e2b --- /dev/null +++ b/dev/cs/db/transactions.texy @@ -0,0 +1,106 @@ +Transakce +********* + +.[perex] +Nette Database nabízí několik způsobů pro práci s transakcemi. Můžete použít klasické metody `beginTransaction()`, `commit()` a `rollBack()`, nebo elegantnější metodu `transaction()`. Nechybí ani podpora zanořených transakcí využívajících savepointy. + + +Základní použití +================ + +Nejjednodušší způsob použití transakcí vypadá takto: + +```php +$db->beginTransaction(); +try { + $db->query('DELETE FROM articles WHERE id = ?', $id); + $db->query('INSERT INTO audit_log', [ + 'article_id' => $id, + 'action' => 'delete' + ]); + $db->commit(); +} catch (\Exception $e) { + $db->rollBack(); + throw $e; +} +``` + +Mnohem elegantněji můžete to samé zapsat pomocí metody `transaction()`: + +```php +$db->transaction(function ($db) use ($id) { + $db->query('DELETE FROM articles WHERE id = ?', $id); + $db->query('INSERT INTO audit_log', [ + 'article_id' => $id, + 'action' => 'delete' + ]); +}); +``` + +Metoda `transaction()` může také vracet hodnoty: + +```php +$count = $db->transaction(function ($db) { + $result = $db->query('UPDATE users SET active = ?', true); + return $result->getRowCount(); // vrátí počet aktualizovaných řádků +}); +``` + + +Zanořené transakce +================== + +Nette Database podporuje zanořování transakcí pomocí SQL savepointů. To znamená, že můžete spustit transakci uvnitř jiné transakce. Zde je jednoduchý příklad: + +```php +$db->transaction(function ($db) { + // hlavní transakce + $db->query('INSERT INTO users', ['name' => 'John']); + + // vnořená transakce + $db->transaction(function ($db) { + $db->query('UPDATE users SET role = ?', 'admin'); + // pokud zde nastane chyba, vrátí se zpět jen vnořená transakce + // hlavní transakce pokračuje dál + }); + + // pokračování hlavní transakce + $db->query('INSERT INTO user_log', ['action' => 'user created']); +}); +``` + +.[note] +Podkladový mechanismus využívá ve skutečnosti jen jednu transakci na úrovni databáze a vnořené transakce emuluje pomocí savepointů. Toto chování je stejné pro všechny databáze a je zcela transparentní. + +.[warning] +Nikdy nevolejte přímo PDO metody pro transakce (`PDO::beginTransaction()`, `PDO::commit()` nebo `PDO::rollBack()`). Obešli byste tím správu vnořených transakcí v Nette a mohlo by dojít k chybám. + + +Auto-commit režim +================= + +Auto-commit určuje, zda se každý dotaz automaticky provede v samostatné transakci. Ve výchozím nastavení je auto-commit zapnutý, což znamená, že každý dotaz tvoří samostatnou transakci. + +Auto-commit můžete vypnout v konfiguraci: + +```neon +database: + dsn: 'mysql:host=127.0.0.1;dbname=test' + user: root + password: secret + options: + autoCommit: false # vypne auto-commit +``` + +nebo v kódu: + +```php +$db->setAutoCommit(false); +``` + +Při vypnutém auto-commitu se automaticky spustí nová transakce v těchto případech: +- při připojení k databázi +- po dokončení předchozí transakce (commit nebo rollback) + +.[note] +Pokud změníte nastavení auto-commitu během aktivní transakce, transakce se automaticky potvrdí. diff --git a/dev/cs/db/upgrading.texy b/dev/cs/db/upgrading.texy new file mode 100644 index 0000000000..20bb26d94d --- /dev/null +++ b/dev/cs/db/upgrading.texy @@ -0,0 +1,14 @@ +Upgrade +******* + + +Přechod z verze 3.1 na 3.2 +========================== + +Minimální požadovaná verze PHP je 8.1. + +Kód byl pečlivě vyladěn pro PHP 8.1. Byly doplněny všechny nové typehinty u metod a properties. Změny jsou jen drobné: + +- MySQL: nulové datum `0000-00-00` vrací jako `null` +- MySQL: decimal bez desetinných míst vrací jako int místo float +- typ `time` vrací jako DateTime s datumem `1. 1. 0001` místo aktuálního data diff --git a/dev/files/03-schema.webp b/dev/files/03-schema.webp new file mode 100644 index 0000000000..4c41bab2bd Binary files /dev/null and b/dev/files/03-schema.webp differ diff --git a/dev/files/04-debug-panel-sql.webp b/dev/files/04-debug-panel-sql.webp new file mode 100644 index 0000000000..98186506ad Binary files /dev/null and b/dev/files/04-debug-panel-sql.webp differ diff --git a/dev/files/04-debugger.webp b/dev/files/04-debugger.webp new file mode 100644 index 0000000000..ac04775c7a Binary files /dev/null and b/dev/files/04-debugger.webp differ diff --git a/dev/files/04-list-table.webp b/dev/files/04-list-table.webp new file mode 100644 index 0000000000..1b4d093032 Binary files /dev/null and b/dev/files/04-list-table.webp differ diff --git a/dev/files/04-not-found.webp b/dev/files/04-not-found.webp new file mode 100644 index 0000000000..eb3cc8b056 Binary files /dev/null and b/dev/files/04-not-found.webp differ diff --git a/dev/files/04-presenter-nav.webp b/dev/files/04-presenter-nav.webp new file mode 100644 index 0000000000..d7ea1b8ee2 Binary files /dev/null and b/dev/files/04-presenter-nav.webp differ diff --git a/dev/files/05-task-form-control.webp b/dev/files/05-task-form-control.webp new file mode 100644 index 0000000000..7d6a264b2e Binary files /dev/null and b/dev/files/05-task-form-control.webp differ diff --git a/dev/files/05-task-form-manual.webp b/dev/files/05-task-form-manual.webp new file mode 100644 index 0000000000..e7a4a59adb Binary files /dev/null and b/dev/files/05-task-form-manual.webp differ diff --git a/dev/files/06-tasklist.webp b/dev/files/06-tasklist.webp new file mode 100644 index 0000000000..cf9b8ec4ed Binary files /dev/null and b/dev/files/06-tasklist.webp differ diff --git a/dev/files/07-logged-in.webp b/dev/files/07-logged-in.webp new file mode 100644 index 0000000000..777a5024c9 Binary files /dev/null and b/dev/files/07-logged-in.webp differ diff --git a/ai/meta.json b/dev/meta.json similarity index 100% rename from ai/meta.json rename to dev/meta.json diff --git a/forms/cs/@home.texy b/forms/cs/@home.texy deleted file mode 100644 index 6a7af6791a..0000000000 --- a/forms/cs/@home.texy +++ /dev/null @@ -1,35 +0,0 @@ -Nette Forms -*********** - -
    - -Nette Forms přinesly revoluci do tvorby webových formulářů. Najednou stačilo napsat několik srozumitelných řádků kódu a měli jste hotový formulář včetně vykreslení, JavaScriptové i serverové validace a navíc špičkově zabezpečený. Ukážeme si, jak - -- vytvářet přívětivé formuláře -- validovat odeslaná data -- vykreslovat prvky přesně podle potřeby - -
    - - -Použitím Nette Forms se vyhnete celé řadě rutinních úkolů, jako je třeba psaní validace (navíc dvojí, na straně serveru a klienta), minimalizujete pravděpodobnost vzniku chyb a bezpečnostních děr. - -Formuláře můžete používat buď jako součást Nette Aplikace (tedy v presenterech), nebo úplně samostatně. Protože v obou případech se používání trošku liší, připravili jsme pro vás dva návody: - -
    -
    "Formuláře v presenterech .[wiki-button]":in-presenter
    -
    "Formuláře samostatně .[wiki-button]":standalone
    -
    - - -Instalace ---------- - -Knihovnu stáhnete a nainstalujete pomocí nástroje [Composer|best-practices:composer]: - -```shell -composer require nette/forms -``` - -{{maintitle: Nette Forms: knihovna pro formuláře v PHP s validací}} -{{description: Bezpečné webové formuláře v PHP na pár řádků kódu. Nette Forms se postarají o vykreslení, validaci v prohlížeči i na serveru a ochranu před útoky.}} diff --git a/forms/cs/@left-menu.texy b/forms/cs/@left-menu.texy deleted file mode 100644 index 9c52ad98b6..0000000000 --- a/forms/cs/@left-menu.texy +++ /dev/null @@ -1,16 +0,0 @@ -Nette Forms -*********** -- [Úvod |@home] -- [Formuláře v presenterech|in-presenter] -- [Formuláře samostatně|standalone] -- [Formulářové prvky |controls] -- [Validace |validation] -- [Vykreslování |rendering] -- [Vlastní prvky |custom-controls] -- [Konfigurace |configuration] -- [Upgrade |upgrading] - - -Další četba -*********** -- [Návody a postupy |best-practices:] diff --git a/forms/cs/@meta.texy b/forms/cs/@meta.texy deleted file mode 100644 index 462d9add80..0000000000 --- a/forms/cs/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokumentace}} diff --git a/forms/cs/configuration.texy b/forms/cs/configuration.texy deleted file mode 100644 index 3c313d6e3b..0000000000 --- a/forms/cs/configuration.texy +++ /dev/null @@ -1,66 +0,0 @@ -Konfigurace formulářů -********************* - -.[perex] -V konfiguraci lze změnit výchozí [chybové hlášky formulářů|validation]. - -```neon -forms: - messages: - Equal: 'Please enter %s.' - NotEqual: 'This value should not be %s.' - Filled: 'This field is required.' - Blank: 'This field should be blank.' - MinLength: 'Please enter at least %d characters.' - MaxLength: 'Please enter no more than %d characters.' - Length: 'Please enter a value between %d and %d characters long.' - Email: 'Please enter a valid email address.' - URL: 'Please enter a valid URL.' - Integer: 'Please enter a valid integer.' - Numeric: 'Please enter a non-negative integer.' - Float: 'Please enter a valid number.' - Min: 'Please enter a value greater than or equal to %d.' - Max: 'Please enter a value less than or equal to %d.' - Range: 'Please enter a value between %d and %d.' - MaxFileSize: 'The size of the uploaded file can be up to %d bytes.' - MaxPostSize: 'The uploaded data exceeds the limit of %d bytes.' - MimeType: 'The uploaded file is not in the expected format.' - Image: 'The uploaded file must be image in format JPEG, GIF, PNG or WebP.' - Nette\Forms\Controls\SelectBox::Valid: 'Please select a valid option.' - Nette\Forms\Controls\UploadControl::Valid: 'An error occurred during file upload.' - Nette\Forms\Controls\CsrfProtection::Protection: 'Your session has expired. Please return to the home page and try again.' -``` - -Zde je český překlad: - -```neon -forms: - messages: - Equal: 'Zadejte %s.' - NotEqual: 'Tato hodnota by neměla být %s.' - Filled: 'Toto pole je povinné.' - Blank: 'Toto pole by mělo být prázdné.' - MinLength: 'Zadejte prosím alespoň %d znaků.' - MaxLength: 'Zadejte prosím maximálně %d znaků.' - Length: 'Zadejte prosím hodnotu o délce %d až %d znaků.' - Email: 'Zadejte platnou e-mailovou adresu.' - URL: 'Zadejte prosím platné URL.' - Integer: 'Zadejte platné celé číslo.' - Numeric: 'Zadejte nezáporné celé číslo.' - Float: 'Zadejte platné číslo.' - Min: 'Zadejte prosím hodnotu větší nebo rovnou %d.' - Max: 'Zadejte prosím hodnotu menší nebo rovnou %d.' - Range: 'Zadejte hodnotu mezi %d a %d.' - MaxFileSize: 'Velikost nahraného souboru může být nejvýše %d bajtů.' - MaxPostSize: 'Nahraná data překračují limit %d bajtů.' - MimeType: 'Nahraný soubor není v očekávaném formátu.' - Image: 'Nahraný soubor musí být obrázek ve formátu JPEG, GIF, PNG nebo WebP.' - Nette\Forms\Controls\SelectBox::Valid: 'Vyberte prosím platnou možnost.' - Nette\Forms\Controls\UploadControl::Valid: 'Při nahrávání souboru došlo k chybě.' - Nette\Forms\Controls\CsrfProtection::Protection: 'Vaše relace vypršela. Vraťte se na domovskou stránku a zkuste to znovu.' -``` - -Pokud nepoužíváte celý framework a tedy ani konfigurační soubory, můžete změnit výchozí chybové hlášky přímo v poli `Nette\Forms\Validator::$messages`. - -{{maintitle: Konfigurace Nette Forms: výchozí chybové hlášky}} -{{description: Výchozí chybové hlášky Nette Forms změníte na jednom místě v konfiguraci NEON, takže je nemusíte nastavovat u každého formuláře nebo prvku zvlášť.}} diff --git a/forms/cs/controls.texy b/forms/cs/controls.texy deleted file mode 100644 index 93876d3fbe..0000000000 --- a/forms/cs/controls.texy +++ /dev/null @@ -1,554 +0,0 @@ -Formulářové prvky -***************** - -.[perex] -Přehled standardních formulářových prvků. - - -addText(string $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -============================================================================================== - -Přidá jednořádkové textové políčko (třída [TextInput |api:Nette\Forms\Controls\TextInput]). Pokud uživatel pole nevyplní, vrací prázdný řetězec `''`, nebo pomocí `setNullable()` lze určit, aby vracel `null`. - -```php -$form->addText('name', 'Jméno:') - ->setRequired() - ->setNullable(); -``` - -Automaticky validuje UTF-8, ořezává levo- a pravostranné mezery a odstraňuje odřádkování, které by mohl odeslat útočník. - -Maximální délku lze omezit pomocí `setMaxLength()`. Pozměnit uživatelem vloženou hodnotu umožňuje [addFilter() |validation#Úprava vstupu]. - -Pomocí `setHtmlType()` lze změnit vizuální charakter textového pole na typy jako `search`, `tel` nebo `url`, viz [specifikace|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]. Pamatujte, že změna typu je pouze vizuální a nezastupuje funkci validace. Pro typ `url` je vhodné přidat specifické validační [pravidlo URL |validation#Textové vstupy]. - -.[note] -Pro další typy vstupů, jako `number`, `range`, `email`, `date`, `datetime-local`, `time` a `color`, použijte specializované metody jako [#addInteger], [#addFloat], [#addEmail], [#addDate], [#addTime], [#addDateTime] a [#addColor], které zajišťují serverovou validaci. Typy `month` a `week` zatím nejsou plně podporovány ve všech prohlížečích. - -Prvku lze nastavit tzv. empty-value, což je něco jako výchozí hodnota, ale pokud ji uživatel nezmění, vrátí prvek prázdný řetězec či `null`. - -```php -$form->addText('phone', 'Telefon:') - ->setHtmlType('tel') - ->setEmptyValue('+420'); -``` - - -addTextArea(string $name, $label=null): TextArea .[method] -========================================================== - -Přidá pole pro zadání víceřádkového textu (třída [TextArea |api:Nette\Forms\Controls\TextArea]). Pokud uživatel pole nevyplní, vrací prázdný řetězec `''`, nebo pomocí `setNullable()` lze určit, aby vracel `null`. - -```php -$form->addTextArea('note', 'Poznámka:') - ->addRule($form::MaxLength, 'Poznámka je příliš dlouhá', 10000); -``` - -Automaticky validuje UTF-8 a normalizuje oddělovače řádků na `\n`. Na rozdíl od jednořádkového vstupního políčka k žádnému ořezávání mezer nedochází. - -Maximální délku lze omezit pomocí `setMaxLength()`. Pozměnit uživatelem vloženou hodnotu umožňuje [addFilter() |validation#Úprava vstupu]. Lze nastavit tzv. empty-value pomocí `setEmptyValue()`. - - -addInteger(string $name, $label=null): TextInput .[method] -========================================================== - -Přidá políčko pro zadání celého čísla (třída [TextInput |api:Nette\Forms\Controls\TextInput]). Vrací buď integer, nebo `null`, pokud uživatel nic nezadá. - -```php -$form->addInteger('year', 'Rok:') - ->addRule($form::Range, 'Rok musí být v rozsahu od %d do %d.', [1900, 2023]); -``` - -Prvek se vykresluje jako ``. Použitím metody `setHtmlType()` lze změnit typ na `range` pro zobrazení v podobě posuvníku, nebo na `text`, pokud preferujete standardní textové pole bez speciálního chování typu `number`. - - -addFloat(string $name, $label=null): TextInput .[method]{data-version:3.1.12} -============================================================================= - -Přidá políčko pro zadání desetinného čísla (třída [TextInput |api:Nette\Forms\Controls\TextInput]). Vrací buď float, nebo `null`, pokud uživatel nic nezadá. - -```php -$form->addFloat('level', 'Úroveň:') - ->setDefaultValue(0) - ->addRule($form::Range, 'Úroveň musí být v rozsahu od %d do %d.', [0, 100]); -``` - -Prvek se vykresluje jako ``. Použitím metody `setHtmlType()` lze změnit typ na `range` pro zobrazení v podobě posuvníku, nebo na `text`, pokud preferujete standardní textové pole bez speciálního chování typu `number`. - -Nette a prohlížeč Chrome akceptují jako oddělovač desetinných míst jak čárku, tak tečku. Aby byla tato funkcionalita dostupná i ve Firefoxu, je doporučeno nastavit atribut `lang` buď pro daný prvek, nebo pro celou stránku, například ``. - - -addEmail(string $name, $label=null, int $maxLength=255): TextInput .[method] -============================================================================ - -Přidá políčko pro zadání e-mailové adresy (třída [TextInput |api:Nette\Forms\Controls\TextInput]). Pokud uživatel pole nevyplní, vrací prázdný řetězec `''`, nebo pomocí `setNullable()` lze určit, aby vracel `null`. - -```php -$form->addEmail('email', 'E-mail:'); -``` - -Ověří, zda je hodnota platná e-mailová adresa. Neověřuje se, zda doména skutečně existuje, ověřuje se pouze syntaxe. Automaticky validuje UTF-8, ořezává levo- a pravostranné mezery. - -Maximální délku lze omezit pomocí `setMaxLength()`. Pozměnit uživatelem vloženou hodnotu umožňuje [addFilter() |validation#Úprava vstupu]. Lze nastavit tzv. empty-value pomocí `setEmptyValue()`. - - -addPassword(string $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -================================================================================================== - -Přidá políčko pro zadání hesla (třída [TextInput |api:Nette\Forms\Controls\TextInput]). - -```php -$form->addPassword('password', 'Heslo:') - ->setRequired() - ->addRule($form::MinLength, 'Heslo musí mít alespoň %d znaků', 8) - ->addRule($form::Pattern, 'Musí obsahovat číslici', '.*[0-9].*'); -``` - -Při znovuzobrazení formuláře bude políčko prázdné. Automaticky validuje UTF-8, ořezává levo- a pravostranné mezery a odstraňuje odřádkování, které by mohl odeslat útočník. - - -addCheckbox(string $name, $caption=null): Checkbox .[method] -============================================================ - -Přidá zaškrtávací políčko (třída [Checkbox |api:Nette\Forms\Controls\Checkbox]). Vrací hodnotu buď `true`, nebo `false`, podle toho, zda je zaškrtnuté. - -```php -$form->addCheckbox('agree', 'Souhlasím s podmínkami') - ->setRequired('Je potřeba souhlasit s podmínkami'); -``` - - -addCheckboxList(string $name, $label=null, ?array $items=null): CheckboxList .[method] -====================================================================================== - -Přidá zaškrtávací políčka pro výběr více položek (třída [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Vrací pole klíčů vybraných položek. Metoda `getSelectedItems()` vrací vybrané položky jako dvojice klíč-hodnota. - -```php -$form->addCheckboxList('colors', 'Barvy:', [ - 'r' => 'červená', - 'g' => 'zelená', - 'b' => 'modrá', -]); -``` - -Pole nabízených položek předáme jako třetí parametr nebo metodou `setItems()`. Předáním `false` jako druhého argumentu metody `setItems()` se hodnoty použijí zároveň i jako klíče. - -Pomocí `setDisabled(['r', 'g'])` lze deaktivovat jednotlivé položky. - -Prvek automaticky kontroluje, že nedošlo k podvržení a že vybrané položky jsou skutečně jedněmi z nabízených a nebyly deaktivované. Metodou `getRawValue()` lze získat odeslané položky bez této důležité kontroly. - -Při nastavení výchozích vybraných položek také kontroluje, že jde o jedny z nabízených, jinak vyhodí výjimku. Tuto kontrolu lze vypnout pomocí `checkDefaultValue(false)`. - -Pokud odesíláte formulář metodou `GET`, můžete zvolit kompaktnější způsob přenosu dat, který šetří velikost query stringu. Aktivuje se nastavením HTML atributu formuláře: - -```php -$form->setHtmlAttribute('data-nette-compact'); -``` - - -addRadioList(string $name, $label=null, ?array $items=null): RadioList .[method] -================================================================================ - -Přidá přepínací tlačítka (třída [RadioList |api:Nette\Forms\Controls\RadioList]). Vrací klíč vybrané položky, nebo `null`, pokud uživatel nic nevybral. Metoda `getSelectedItem()` vrací hodnotu místo klíče. - -```php -$sex = [ - 'm' => 'muž', - 'f' => 'žena', - 'o' => 'jiné', -]; -$form->addRadioList('gender', 'Pohlaví:', $sex); -``` - -Pole nabízených položek předáme jako třetí parametr nebo metodou `setItems()`. - -Pomocí `setDisabled(['m'])` lze deaktivovat jednotlivé položky. - -Prvek automaticky kontroluje, že nedošlo k podvržení a že vybraná položka je skutečně jednou z nabízených a nebyla deaktivovaná. Metodou `getRawValue()` lze získat odeslanou položku bez této důležité kontroly. - -Při nastavení výchozí vybrané položky také kontroluje, že jde o jednu z nabízených, jinak vyhodí výjimku. Tuto kontrolu lze vypnout pomocí `checkDefaultValue(false)`. - - -addSelect(string $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] -============================================================================================== - -Přidá select box (třída [SelectBox |api:Nette\Forms\Controls\SelectBox]). Vrací klíč vybrané položky, nebo `null`, pokud uživatel nic nevybral. Metoda `getSelectedItem()` vrací hodnotu místo klíče. - -```php -$countries = [ - 'CZ' => 'Česká republika', - 'SK' => 'Slovensko', - 'GB' => 'Velká Británie', -]; - -$form->addSelect('country', 'Země:', $countries) - ->setDefaultValue('SK'); -``` - -Pole nabízených položek předáme jako třetí parametr nebo metodou `setItems()`. Položky mohou být i dvourozměrné pole: - -```php -$countries = [ - 'Europe' => [ - 'CZ' => 'Česká republika', - 'SK' => 'Slovensko', - 'GB' => 'Velká Británie', - ], - 'CA' => 'Kanada', - 'US' => 'USA', - '?' => 'jiná', -]; -``` - -U select boxů má často první položka speciální význam, slouží jako výzva k akci. K přidání takové položky slouží metoda `setPrompt()`. - -```php -$form->addSelect('country', 'Země:', $countries) - ->setPrompt('Zvolte zemi'); -``` - -Pomocí `setDisabled(['CZ', 'SK'])` lze deaktivovat jednotlivé položky. - -Prvek automaticky kontroluje, že nedošlo k podvržení a že vybraná položka je skutečně jednou z nabízených a nebyla deaktivovaná. Metodou `getRawValue()` lze získat odeslanou položku bez této důležité kontroly. - -Při nastavení výchozí vybrané položky také kontroluje, že jde o jednu z nabízených, jinak vyhodí výjimku. Tuto kontrolu lze vypnout pomocí `checkDefaultValue(false)`. - - -addMultiSelect(string $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] -======================================================================================================== - -Přidá select box pro výběr více položek (třída [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Vrací pole klíčů vybraných položek. Metoda `getSelectedItems()` vrací vybrané položky jako dvojice klíč-hodnota. - -```php -$form->addMultiSelect('countries', 'Země:', $countries); -``` - -Pole nabízených položek předáme jako třetí parametr nebo metodou `setItems()`. Položky mohou být i dvourozměrné pole. - -Pomocí `setDisabled(['CZ', 'SK'])` lze deaktivovat jednotlivé položky. - -Prvek automaticky kontroluje, že nedošlo k podvržení a že vybrané položky jsou skutečně jedněmi z nabízených a nebyly deaktivované. Metodou `getRawValue()` lze získat odeslané položky bez této důležité kontroly. - -Při nastavení výchozích vybraných položek také kontroluje, že jde o jedny z nabízených, jinak vyhodí výjimku. Tuto kontrolu lze vypnout pomocí `checkDefaultValue(false)`. - - -addUpload(string $name, $label=null): UploadControl .[method] -============================================================= - -Přidá políčko pro upload souboru (třída [UploadControl |api:Nette\Forms\Controls\UploadControl]). Vrací objekt [FileUpload |http:request#FileUpload] a to i v případě, že uživatel žádný soubor neodeslal, což lze zjistit metodou `FileUpload::hasFile()`. Pomocí `setNullable()` lze určit, aby prvek místo objektu `FileUpload` vracel `null`, když není nahrán žádný soubor. - -```php -$form->addUpload('avatar', 'Avatar:') - ->addRule($form::Image, 'Avatar musí být JPEG, PNG, GIF, WebP nebo AVIF.') - ->addRule($form::MaxFileSize, 'Maximální velikost je 1 MB.', 1024 * 1024); -``` - -Pokud se soubor nepodaří korektně nahrát, formulář není úspěšně odeslaný a zobrazí se chyba. Tj. při úspěšném odeslání není potřeba ověřovat metodu `FileUpload::isOk()`. - -Nikdy nevěřte originálnímu názvu souboru vráceného metodou `FileUpload::getName()`, klient mohl odeslat škodlivý název souboru s úmyslem poškodit nebo hacknout vaši aplikaci. - -Pravidla `MimeType` a `Image` detekují požadovaný typ na základě signatury souboru a neověřují jeho integritu. Zda není obrázek poškozený, lze zjistit například pokusem o jeho [načtení |http:request#toImage]. - - -addMultiUpload(string $name, $label=null): UploadControl .[method] -================================================================== - -Přidá políčko pro upload více souborů najednou (třída [UploadControl |api:Nette\Forms\Controls\UploadControl]). Vrací pole objektů [FileUpload |http:request#FileUpload]. Metoda `FileUpload::hasFile()` u každého z nich bude vracet `true`. - -```php -$form->addMultiUpload('files', 'Soubory:') - ->addRule($form::MaxLength, 'Maximálně lze nahrát %d souborů', 10); -``` - -Pokud se některý soubor nepodaří korektně nahrát, formulář není úspěšně odeslaný a zobrazí se chyba. Tj. při úspěšném odeslání není potřeba ověřovat metodu `FileUpload::isOk()`. - -Nikdy nevěřte originálním názvům souborů vráceným metodou `FileUpload::getName()`, klient mohl odeslat škodlivý název souboru s úmyslem poškodit nebo hacknout vaši aplikaci. - -Pravidla `MimeType` a `Image` detekují požadovaný typ na základě signatury souboru a neověřují jeho integritu. Zda není obrázek poškozený, lze zjistit například pokusem o jeho [načtení |http:request#toImage]. - - -addDate(string $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} -================================================================================== - -Přidá políčko, které umožní uživateli snadno zadat datum skládající se z roku, měsíce a dne (třída [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Jako výchozí hodnotu akceptuje buď objekty implementující rozhraní `DateTimeInterface`, řetězec s časem, nebo číslo představující UNIX timestamp. Totéž platí pro argumenty pravidel `Min`, `Max` nebo `Range`, jež definují minimální a maximální povolené datum. - -```php -$form->addDate('date', 'Datum:') - ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'Datum musí být minimálně měsíc staré.', new DateTime('-1 month')); -``` - -Standardně vrací objekt `DateTimeImmutable`, metodou `setFormat()` můžete specifikovat [textový formát|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] či timestamp: - -```php -$form->addDate('date', 'Datum:') - ->setFormat('Y-m-d'); -``` - - -addTime(string $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=========================================================================================================== - -Přidá políčko, které umožní uživateli snadno zadat čas skládající se z hodin, minut a volitelně i sekund (třída [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Jako výchozí hodnotu akceptuje buď objekty implementující rozhraní `DateTimeInterface`, řetězec s časem, nebo číslo představující UNIX timestamp. Z těchto vstupů je využita pouze časová informace, datum je ignorováno. Totéž platí pro argumenty pravidel `Min`, `Max` nebo `Range`, jež definují minimální a maximální povolený čas. Pokud je nastavená minimální hodnota vyšší než maximální, vytvoří se časový rozsah přesahující půlnoc. - -```php -$form->addTime('time', 'Čas:', withSeconds: true) - ->addRule($form::Range, 'Čas musí být v rozsahu od %d do %d.', ['12:30', '13:30']); -``` - -Standardně vrací objekt `DateTimeImmutable` (s datem 1. ledna roku 1), metodou `setFormat()` můžete specifikovat [textový formát|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]: - -```php -$form->addTime('time', 'Čas:') - ->setFormat('H:i'); -``` - - -addDateTime(string $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=============================================================================================================== - -Přidá políčko, které umožní uživateli snadno zadat datum a čas skládající se z roku, měsíce, dne, hodin, minut a volitelně i sekund (třída [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Jako výchozí hodnotu akceptuje buď objekty implementující rozhraní `DateTimeInterface`, řetězec s časem, nebo číslo představující UNIX timestamp. Totéž platí pro argumenty pravidel `Min`, `Max` nebo `Range`, jež definují minimální a maximální povolené datum. - -```php -$form->addDateTime('datetime', 'Datum a čas:') - ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'Datum musí být minimálně měsíc staré.', new DateTime('-1 month')); -``` - -Standardně vrací objekt `DateTimeImmutable`, metodou `setFormat()` můžete specifikovat [textový formát|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] či timestamp: - -```php -$form->addDateTime('datetime') - ->setFormat(DateTimeControl::FormatTimestamp); -``` - - -addColor(string $name, $label=null): ColorPicker .[method]{data-version:3.1.14} -=============================================================================== - -Přidá políčko pro výběr barvy (třída [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). Barva je řetězec ve tvaru `#rrggbb`. Pokud uživatel volbu neprovede, vrátí se černá barva `#000000`. - -```php -$form->addColor('color', 'Barva:') - ->setDefaultValue('#3C8ED7'); -``` - - -addHidden(string $name, mixed $default=null): HiddenField .[method] -=================================================================== - -Přidá skryté pole (třída [HiddenField |api:Nette\Forms\Controls\HiddenField]). - -```php -$form->addHidden('userid'); -``` - -Pomocí `setNullable()` lze nastavit, aby vracel `null` místo prázdného řetězce. Pozměnit odeslanou hodnotu umožňuje [addFilter() |validation#Úprava vstupu]. - -Ačkoli je prvek skrytý, je **důležité si uvědomit**, že hodnota může být stále modifikována nebo podvržena útočníkem. Vždy důkladně ověřujte a validujte všechny přijaté hodnoty na serverové straně, aby se předešlo bezpečnostním rizikům spojeným s manipulací dat. - - -addSubmit(string $name, $caption=null): SubmitButton .[method] -============================================================== - -Přidá odesílací tlačítko (třída [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). - -```php -$form->addSubmit('submit', 'Odeslat'); -``` - -.{data-version:3.3.0} -Obslužnou metodu lze tlačítku předat rovnou třetím parametrem `$onSubmit` namísto navěšení na událost `onClick`: - -```php -$form->addSubmit('submit', 'Odeslat', function (SubmitButton $button, $data): void { - // ... -}); -``` - -Ve formuláři je možné mít i více odesílacích tlačítek: - -```php -$form->addSubmit('register', 'Register'); -$form->addSubmit('cancel', 'Cancel'); -``` - -Ke zjištění, na které z nich bylo kliknuto, použijte: - -```php -if ($form['register']->isSubmittedBy()) { - // ... -} -``` - -Pokud nechcete validovat celý formulář při stisknutí tlačítka (například u tlačítek *Zrušit* nebo *Náhled*), použijte [setValidationScope() |validation#Vypnutí validace]. - - -addButton(string $name, $caption=null): Button .[method] -======================================================== - -Přidá tlačítko (třída [Button |api:Nette\Forms\Controls\Button]), které nemá odesílací funkci. Lze ho tedy využít na nějakou jinou funkci, např. zavolání JavaScriptové funkce při kliknutí. - -```php -$form->addButton('raise', 'Zvýšit plat') - ->setHtmlAttribute('onclick', 'raiseSalary()'); -``` - - -addImageButton(string $name, ?string $src=null, ?string $alt=null): ImageButton .[method] -========================================================================================= - -Přidá odesílací tlačítko v podobě obrázku (třída [ImageButton |api:Nette\Forms\Controls\ImageButton]). - -```php -$form->addImageButton('submit', '/path/to/image.png', 'Odeslat'); -``` - -Při použití více odesílacích tlačítek lze zjistit, na které bylo kliknuto, pomocí `$form['submit']->isSubmittedBy()`. - - -addContainer(string|int $name): Container .[method] -=================================================== - -Přidá podformulář (třída [Container|api:Nette\Forms\Container]), neboli kontejner, do kterého lze přidávat další prvky stejným způsobem, jako je přidáváme do formuláře. Fungují i metody `setDefaults()` nebo `getValues()`. - -```php -$sub1 = $form->addContainer('first'); -$sub1->addText('name', 'Your name:'); -$sub1->addEmail('email', 'Email:'); - -$sub2 = $form->addContainer('second'); -$sub2->addText('name', 'Your name:'); -$sub2->addEmail('email', 'Email:'); -``` - -Odeslaná data pak vrací jako vícerozměrnou strukturu: - -```php -[ - 'first' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], - 'second' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], -] -``` - - -Přehled nastavení -================= - -U všech prvků můžeme volat následující metody (kompletní přehled v [API dokumentaci|https://api.nette.org/forms/master/Nette/Forms/Controls.html]): - -.[table-form-methods language-php] -| `setDefaultValue($value)` | nastaví výchozí hodnotu -| `getValue()` | získá aktuální hodnotu -| `setOmitted()` | [#vynechání hodnoty] -| `setDisabled()` | [#deaktivace prvků] - -Vykreslování: -.[table-form-methods language-php] -| `setCaption($caption)` | změní popisku prvku -| `setTranslator($translator)` | nastaví [překladač |rendering#Překládání] -| `setHtmlAttribute($name, $value)` | nastaví [HTML atribut |rendering#HTML atributy] elementu -| `setHtmlId($id)` | nastaví HTML atribut `id` -| `setOption($key, $value)` | [nastavení pro vykreslování |rendering#Options] - -Validace: -.[table-form-methods language-php] -| `setRequired()` | [povinný prvek |validation] -| `addRule()` | nastaví [validační pravidlo |validation#Pravidla] -| `addCondition()`, `addConditionOn()` | nastaví [validační podmínku |validation#Podmínky] -| `addError($message)` | [předání chybové zprávy |validation#Chyby při zpracování] - -U prvků `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()` lze volat následující metody: - -.[table-form-methods language-php] -| `setNullable()` | nastaví, zda getValue() vrátí `null` místo prázdného řetězce -| `setEmptyValue($value)` | nastaví speciální hodnotu, která se považuje za prázdný řetězec -| `setMaxLength($length)` | nastaví maximální počet povolených znaků -| `addFilter($filter)` | [úprava vstupu |validation#Úprava vstupu] - - -Vynechání hodnoty -================= - -Pokud nás uživatelem vyplněná hodnota nezajímá, můžeme ji pomocí `setOmitted()` vynechat z výsledku metody `$form->getValues()` nebo z dat předávaných do handlerů. To se hodí pro různá hesla pro kontrolu, antispamové prvky atd. - -```php -$form->addPassword('passwordVerify', 'Heslo pro kontrolu:') - ->setRequired('Zadejte prosím heslo ještě jednou pro kontrolu') - ->addRule($form::Equal, 'Hesla se neshodují', $form['password']) - ->setOmitted(); -``` - - -Deaktivace prvků -================ - -Prvky lze deaktivovat pomocí `setDisabled()`. Takový prvek nemůže uživatel editovat. - -```php -$form->addText('username', 'Uživatelské jméno:') - ->setDisabled(); -``` - -Disablované prvky prohlížeč vůbec neodesílá na server, tedy je ani nenajdete v datech vrácených funkcí `$form->getValues()`. Pokud však nastavíte `setOmitted(false)`, Nette do těchto dat zahrne jejich výchozí hodnotu. - -Při volání `setDisabled()` se z bezpečnostních důvodů **smaže hodnota prvku**. Pokud nastavujete výchozí hodnotu, je tak nutné učinit až po jeho deaktivaci: - -```php -$form->addText('username', 'Uživatelské jméno:') - ->setDisabled() - ->setDefaultValue($userName); -``` - -Alternativou disablovaných prvků jsou prvky s HTML atributem `readonly`, které prohlížeč na server posílá. Ačkoliv je prvek pouze pro čtení, je **důležité si uvědomit**, že jeho hodnota může být stále modifikována nebo podvržena útočníkem. - - -Vlastní prvky -============= - -Vedle široké škály vestavěných formulářových prvků můžete do formuláře přidávat vlastní prvky: - -```php -$form->addComponent(new DateInput('Datum:'), 'date'); -// alternativní syntax: $form['date'] = new DateInput('Datum:'); -``` - -Jak takový prvek napsat, včetně čtení odeslaných dat, validace a vykreslování, popisuje [samostatná kapitola |custom-controls]. Dozvíte se v ní i o extension methods, kterými si vytvoříte vlastní přidávací metodu jako `$form->addZip()`. - - -Low-level prvky -=============== - -Lze používat i prvky, které zapíšeme pouze v šabloně a nepřidáme je do formuláře některou z metod `$form->addXyz()`. Když například vypisujeme záznamy z databáze a dopředu nevíme, kolik jich bude a jaké budou mít ID, a chceme u každého řádku zobrazit checkbox nebo radio button, stačí jej nakódovat v šabloně: - -```latte -{foreach $items as $item} -

    id}> {$item->name}

    -{/foreach} -``` - -A po odeslání hodnotu zjistíme: - -```php -$data = $form->getHttpData($form::DataText, 'sel[]'); -$data = $form->getHttpData($form::DataText | $form::DataKeys, 'sel[]'); -``` - -kde první parametr je typ elementu (`DataFile` pro `type=file`, `DataLine` pro jednořádkové vstupy jako `text`, `password`, `email` apod. a `DataText` pro všechny ostatní) a druhý parametr `sel[]` odpovídá HTML atributu name. Typ elementu můžeme kombinovat s hodnotou `DataKeys`, která zachová klíče prvků. To se hodí zejména pro `select`, `radioList` a `checkboxList`. - -Podstatné je, že `getHttpData()` vrací sanitizovanou hodnotu, v tomto případě to bude vždy pole validních UTF-8 řetězců, ať už by se pokusil útočník serveru podstrčit cokoliv. Jde o obdobu přímé práce s `$_POST` nebo `$_GET`, avšak s tím podstatným rozdílem, že vždy vrací čistá data, tak, jak jste zvyklí u standardních prvků Nette formulářů. - -{{maintitle: Formulářové prvky v Nette: text, select, upload, datum}} -{{description: Všechny standardní prvky Nette Forms a jejich nastavení: textová a číselná pole, checkboxy, selectboxy, nahrávání souborů, datum a čas i tlačítka.}} diff --git a/forms/cs/custom-controls.texy b/forms/cs/custom-controls.texy deleted file mode 100644 index 284f5d67f1..0000000000 --- a/forms/cs/custom-controls.texy +++ /dev/null @@ -1,271 +0,0 @@ -Vlastní formulářové prvky -************************* - -.[perex] -Nette nabízí širokou paletu [vestavěných formulářových prvků |controls]. Když ale narazíte na požadavek, který mezi nimi není, nemusíte nic obcházet ani slepovat: napíšete si prvek vlastní. Bude umět všechno, co ty vestavěné - validovat, překládat se, vykreslovat - a používat se bude úplně stejně. - -Ukážeme si to na praktickém příkladu: prvku pro zadání data pomocí tří políček, den, měsíc a rok. Cestou se seznámíte se vším, co k psaní prvků potřebujete vědět. - - -Kdy vlastní prvek psát a kdy ne -=============================== - -Vlastní prvek je nejsilnější nástroj, který formuláře nabízejí. A jako každý silný nástroj má být tou poslední volbou, ne první. Řadu situací totiž vyřeší jednodušší prostředky: - -- **Úpravu hodnoty** zvládne [addFilter() |validation#Úprava vstupu]. Chcete tolerovat mezery v PSČ nebo malá písmena v kódu? Filtr je pár řádků. -- **Opakovanou konfiguraci** zabalí vlastní přidávací metoda. Přidáváte na deseti místech políčko na PSČ se stejnou validací? Vytvořte si pro ně pojmenovanou zkratku, [ukážeme si to na konci |#Vlastní přidávací metoda]. -- **Skupinu souvisejících polí** obslouží [kontejner |controls#addContainer]. Adresa složená z ulice, města a PSČ nepotřebuje vlastní prvek, stačí kontejner se třemi textovými políčky. -- **Jiný vzhled** zařídí [setHtmlType() |controls#addText] a HTML atributy, případně [prototypy |rendering#Prototypy]. - -Vlastní prvek dává smysl ve chvíli, kdy potřebujete **vlastní hodnotu**: když navenek vystupuje jako jediné pole s jedinou hodnotou, ale uvnitř se skládá z několika inputů nebo hodnotu ukládá jinak, než jak ji zobrazuje. Datum ze tří políček. Souřadnice vybrané kliknutím do mapy. Tag input s našeptávačem. - - -Anatomie prvku -============== - -Každý vlastní prvek dědí od abstraktní třídy [api:Nette\Forms\Controls\BaseControl]. Z ní zdědí obrovské množství hotové funkcionality: uchovávání hodnoty, validační pravidla a podmínky, chybové zprávy, překlady, HTML atributy, popisku i napojení na vykreslování. Vy dopíšete jen to, čím se váš prvek liší. - -Minimální funkční prvek je překvapivě krátký: - -```php -use Nette\Forms\Form; -use Nette\Forms\Helpers; -use Nette\Utils\Html; - -class SimpleInput extends Nette\Forms\Controls\BaseControl -{ - public function loadHttpData(): void - { - $this->setValue($this->getHttpData(Form::DataLine)); - } - - public function getControl(): Html - { - return Html::el('input', [ - 'type' => 'text', - 'name' => $this->getHtmlName(), - 'id' => $this->getHtmlId(), - 'value' => $this->getValue(), - 'data-nette-rules' => Helpers::exportRules($this->getRules()) ?: null, - ]); - } -} -``` - -Dvě metody: jedna říká, jak z odeslaných dat získat hodnotu, druhá jak prvek vykreslit. Obě si hned podrobně rozebereme. Všechno ostatní - `setRequired()`, `addRule()`, `setDefaultValue()`, překlady - už funguje samo. - -Do formuláře prvek přidáte metodou `addComponent()`, nebo stručněji přes hranaté závorky: - -```php -$form['nickname'] = new SimpleInput('Přezdívka:'); -``` - - -Životní cyklus prvku -==================== - -Než se pustíme do zajímavějšího prvku, je dobré vědět, co se s ním děje a kdy. Formulář i jeho prvky jsou [komponenty |component-model:], které tvoří strom. To má jeden příjemný důsledek: prvek nemusí nic zjišťovat sám, o všechno podstatné se postará framework v pravou chvíli: - -1) V okamžiku, kdy prvek připojíte k odeslanému formuláři, formulář na něm sám zavolá `loadHttpData()`. V ní si prvek přečte svou odeslanou hodnotu, jak si ukážeme za chvíli. Nikdy nepracuje přímo s `$_POST` a nemusí vůbec řešit, zda je zanořený v kontejnerech. - -2) Při odeslání formuláře proběhne validace: vyhodnotí se pravidla přidaná přes `addRule()`, která pracují s hodnotou z `getValue()`. - -3) Kdo pak zavolá `$form->getValues()` nebo `getValue()` na prvku, dostane už čistou, typovanou hodnotu - třeba objekt `DateTimeImmutable`, nikoliv trojici řetězců z formuláře. - -A při vykreslování se zavolá `getControl()`, respektive `getLabel()` pro popisku. - - -Čtení odeslané hodnoty -====================== - -V metodě `loadHttpData()` si prvek řekne o svou odeslanou hodnotu metodou `getHttpData()`. Jejím parametrem je typ, který určuje, jak se má hodnota očistit: - -| typ | význam -|------- -| `Form::DataLine` | jednořádkový text: nahradí odřádkování mezerami, ořeže mezery -| `Form::DataText` | víceřádkový text: znormalizuje konce řádků na `\n` -| `Form::DataFile` | upload, instance `Nette\Http\FileUpload` - -Ať se útočník snaží sebevíc, výsledkem je vždy validní UTF-8 řetězec bez kontrolních znaků (nebo objekt uploadu či `null`). Právě proto hodnotu nikdy nečteme přímo z `$_POST` - přišli bychom o všechny tyto záruky. - -Prvek skládající se z více inputů, jako naše datum, předá druhým parametrem část HTML jména a přečte si tak jednotlivé pod-hodnoty. Ukládá si je do vlastních properties `$day`, `$month` a `$year` typu string: - -```php -public function loadHttpData(): void -{ - $this->day = $this->getHttpData(Form::DataLine, '[day]') ?? ''; - $this->month = $this->getHttpData(Form::DataLine, '[month]') ?? ''; - $this->year = $this->getHttpData(Form::DataLine, '[year]') ?? ''; -} -``` - -Pokud HTML jméno končí na `[]`, vrátí se pole hodnot. Kombinací s typem `Form::DataKeys` (tedy `Form::DataLine | Form::DataKeys`) navíc zachováte jeho klíče: - -```php -$tags = $this->getHttpData(Form::DataLine, '[tags][]'); -``` - -Chybějící hodnota je `null` (u polí prázdné pole). Požadavek totiž nemusí data prvku vůbec obsahovat, útočníkovi nic nebrání poslat, co se mu zlíbí - proto v ukázce doplňujeme `?? ''` a proto vždy počítejte i s touto variantou. - - -Hodnota prvku -============= - -Prvek uchovává svou hodnotu a navenek ji zpřístupňuje trojicí metod, jejichž kontrakt je dobré dodržet. - -Metoda `setValue()` přijímá hodnotu od programátora - touto cestou přichází i `setDefaultValue()` a `$form->setDefaults()`. Měla by akceptovat vše, co dává smysl, hodnotu si převést do vnitřní podoby a na nesmyslný vstup vyhodit výjimku, aby se chyba projevila hned a ne až záhadným chováním formuláře. Naše datum přijme `DateTimeInterface`, řetězec, timestamp nebo `null` a rozloží je do tří políček: - -```php -public function setValue(mixed $value): static -{ - if ($value === null) { - $this->day = $this->month = $this->year = ''; - } else { - $date = Nette\Utils\DateTime::from($value); // nesmysl vyhodí výjimku - $this->day = $date->format('j'); - $this->month = $date->format('n'); - $this->year = $date->format('Y'); - } - return $this; -} -``` - -Metoda `getValue()` naopak skládá čistou, typovanou hodnotu - to jediné, co uvidí uživatel vašeho prvku. Pokud hodnota není platná, vrací `null`. Statická metoda `validateDate()` prostě zkontroluje, že trojice políček dává dohromady existující datum: - -```php -public function getValue(): ?DateTimeImmutable -{ - return self::validateDate($this) - ? (new DateTimeImmutable)->setDate((int) $this->year, (int) $this->month, (int) $this->day)->setTime(0, 0) - : null; -} -``` - -A metoda `isFilled()` říká, zda uživatel prvek vyplnil - používá ji pravidlo `setRequired()`. Výchozí implementace (neprázdná hodnota) často stačí, u složeného prvku ji ale přepište podle jeho logiky: - -```php -public function isFilled(): bool -{ - return $this->day !== '' || $this->year !== ''; -} -``` - - -Vykreslování -============ - -Metoda `getControl()` vrací HTML podobu prvku, obvykle jako objekt [Html |utils:html-elements], klidně ale i jako řetězec - na tom nezáleží. Po objektu Html sáhneme hlavně při skládání kódu, protože s ním výsledné HTML sestavíme bezpečně a s příjemným API. K dispozici máte několik pomocníků: - -- `getHtmlName()` vrací HTML atribut `name`, včetně případného zanoření do kontejnerů (např. `invoice[date]`). U složeného prvku k němu připojíte části jmen jednotlivých inputů: `$name . '[day]'`. -- `getHtmlId()` vrací atribut `id` provázaný s popiskou. -- `Helpers::exportRules($this->getRules())` vyexportuje validační pravidla pro atribut `data-nette-rules`, díky kterému bude fungovat [JavaScriptová validace |validation#JavaScriptová validace] i u vašeho prvku. Atribut patří na první input prvku. -- `Helpers::createSelectBox($items, $optionAttrs, $selected)` sestaví z pole položek element `` obalených v `